001/*-----------------------------------------------------------------------+ 002 | eu.cqse.check.abap 003 | | 004 $Id: OpenSqlStatement.java 12402 2016-08-05 06:23:07Z roehm $ 005 | | 006 | Copyright (c) 2009-2016 CQSE GmbH | 007 +-----------------------------------------------------------------------*/ 008package eu.cqse.check.abap; 009 010import static eu.cqse.check.framework.scanner.ETokenType.DELETE; 011import static eu.cqse.check.framework.scanner.ETokenType.FROM; 012import static eu.cqse.check.framework.scanner.ETokenType.INSERT; 013import static eu.cqse.check.framework.scanner.ETokenType.INTO; 014import static eu.cqse.check.framework.shallowparser.TokenStreamUtils.startsWith; 015 016import java.util.EnumSet; 017import java.util.List; 018 019import org.conqat.lib.commons.collections.UnmodifiableList; 020import org.conqat.lib.commons.string.StringUtils; 021 022import eu.cqse.check.framework.scanner.ETokenType; 023import eu.cqse.check.framework.scanner.ETokenType.ETokenClass; 024import eu.cqse.check.framework.scanner.IToken; 025import eu.cqse.check.framework.shallowparser.TokenStreamUtils; 026import eu.cqse.check.framework.shallowparser.framework.ShallowEntity; 027import eu.cqse.check.framework.typetracker.ITypeResolution; 028import eu.cqse.check.framework.util.abap.AbapCheckUtils; 029import eu.cqse.check.framework.util.abap.AbapLanguageFeatureParser; 030 031/** 032 * Representation of a SQL-like ABAP statement that modifies database table's 033 * contents. This offers more detailed analysis, e.g. finding out whether the 034 * targeted table is in the standard namespace. 035 */ 036public abstract class OpenSqlWriteStatement { 037 038 /** The tokens that constitute the statement. */ 039 protected List<IToken> tokens; 040 041 /** 042 * An entity representing an OpenSQL statement directly or an entity 043 * representing a macro which contains an OpenSQL statement. 044 */ 045 protected ShallowEntity entity; 046 047 /** Type resolution of OpenSQL statement. */ 048 protected ITypeResolution typeResolution; 049 050 /** The token types for '->' and '=>', which indicate class member access. */ 051 private static final EnumSet<ETokenType> MEMBER_ARROWS = EnumSet.of(ETokenType.ARROW, ETokenType.EQGT); 052 053 /** 054 * Constructor. 055 */ 056 private OpenSqlWriteStatement(ShallowEntity entity, List<IToken> tokens, ITypeResolution typeResolution) { 057 this.entity = entity; 058 this.tokens = new UnmodifiableList<>(tokens); 059 this.typeResolution = typeResolution; 060 } 061 062 /** 063 * Determines the token index at which the table name starts. Returns 1 by 064 * default. 065 */ 066 protected int getTableTokenIndex() { 067 return 1; 068 } 069 070 /** 071 * Checks whether the statement is targeted at an internal table. Returns true 072 * if it is sure from the statement syntax alone that an iTab is targeted. 073 */ 074 protected abstract boolean hasSureITabSyntax(); 075 076 /** Checks whether the target table of the statement is a variable. */ 077 public boolean targetsInternalTable() { 078 079 if (hasSureITabSyntax()) { 080 return true; 081 } 082 083 int nameStartIndex = getTableTokenIndex(); 084 if (isFieldSymbolStart(nameStartIndex) || isClassMemberStart(nameStartIndex)) { 085 return true; 086 } 087 088 // Anything longer than 16 characters cannot be a DB table. Also ignore 089 // representation of table names in variable or expression (i.e. the table name 090 // starts with '(') . 091 String tableName = tokens.get(nameStartIndex).getText(); 092 if (tableName.length() > 16 || tableName.startsWith("(")) { 093 return true; 094 } 095 096 return AbapCheckUtils.isVariable(tableName, entity, typeResolution); 097 } 098 099 /** Gets the statement start token, e.g. INSERT. */ 100 public IToken getStatementStartToken() { 101 return tokens.get(0); 102 } 103 104 /** Returns the name of the targeted table in upper case. */ 105 public String getTableName() { 106 return getTableNameToken().getText().toUpperCase(); 107 } 108 109 /** Returns the {@link IToken} holding the table name */ 110 public IToken getTableNameToken() { 111 return tokens.get(getTableTokenIndex()); 112 } 113 114 /** 115 * Whether the given token index is the start of a field symbol, i.e. it is a 116 * '<' followed by an identifier and a '>'. 117 */ 118 private boolean isFieldSymbolStart(int index) { 119 if (tokens.size() <= index + 2) { 120 return false; 121 } 122 return tokens.get(index).getType() == ETokenType.LT && tokens.get(index + 2).getType() == ETokenType.GT; 123 } 124 125 /** 126 * Whether the given token index is the start of a class member access, i.e. it 127 * is an identifier followed by either '->' or '=>' and another identifier. 128 */ 129 private boolean isClassMemberStart(int index) { 130 if (tokens.size() <= index + 2) { 131 return false; 132 } 133 return MEMBER_ARROWS.contains(tokens.get(index + 1).getType()); 134 } 135 136 /** 137 * Checks whether the statement targets a standard database table, i.e. a 138 * database table from an SAP namespace. Uses the {@link ITypeResolution} to 139 * distinguish local variables from standard tables. 140 */ 141 public boolean targetsStandardDatabaseTable() { 142 if (targetsInternalTable()) { 143 return false; 144 } 145 146 String currentTableName = AbapLanguageFeatureParser.normalizeVariable(getTableName()); 147 148 // Consider customer name space 149 if (StringUtils.startsWithOneOf(currentTableName, "/", "y", "z")) { 150 return false; 151 } 152 153 // Internal tables EXTRACT and TOTAL are treated as non-standard tables 154 // as they can be manipulated by custom code. 155 return !(currentTableName.equals("extract") || currentTableName.equals("total")); 156 } 157 158 /** 159 * Factory method. {@code tokens} must be one of INSERT, MODIFY, UPDATE, DELETE. 160 * Returns {@code null} if passed token list is not an OpenSQL statement 161 * targeting a database table. 162 * 163 * @throws IllegalArgumentException 164 * In case the statement has an unsupported type. 165 */ 166 public static OpenSqlWriteStatement create(ShallowEntity entity, List<IToken> tokens, 167 ITypeResolution typeResolution) { 168 ETokenType type = tokens.get(0).getType(); 169 if (tokens.size() < 2 || !EnumSet.of(ETokenClass.IDENTIFIER, ETokenClass.KEYWORD) 170 .contains(tokens.get(1).getType().getTokenClass())) { 171 return null; 172 } 173 switch (type) { 174 case INSERT: 175 if (TokenStreamUtils.startsWith(tokens, ETokenType.INSERT, ETokenType.REPORT) 176 || TokenStreamUtils.startsWith(tokens, ETokenType.INSERT, ETokenType.LPAREN) 177 || TokenStreamUtils.startsWith(tokens, ETokenType.INSERT, ETokenType.TEXTPOOL)) { 178 return null; 179 } 180 return filterInternal(new TableInsert(entity, tokens, typeResolution)); 181 case MODIFY: 182 if (TokenStreamUtils.startsWith(tokens, ETokenType.MODIFY, ETokenType.SCREEN) 183 || TokenStreamUtils.startsWith(tokens, ETokenType.MODIFY, ETokenType.LINE)) { 184 return null; 185 } 186 return filterInternal(new TableModify(entity, tokens, typeResolution)); 187 case UPDATE: 188 return filterInternal(new TableUpdate(entity, tokens, typeResolution)); 189 case DELETE: 190 if (TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.DATASET) 191 || TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.DYNPRO) 192 || TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.REPORT) 193 || TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.TEXTPOOL) 194 || TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.FROM, ETokenType.SHARED, 195 ETokenType.BUFFER) 196 || TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.FROM, ETokenType.SHARED, 197 ETokenType.MEMORY) 198 || TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.FROM, ETokenType.MEMORY)) { 199 return null; 200 } 201 return filterInternal(new TableDelete(entity, tokens, typeResolution)); 202 default: 203 throw new IllegalArgumentException("Unsupported statement type " + type); 204 } 205 } 206 207 /** 208 * Returns the provided statement only if it targets a database table, otherwise 209 * returns <code>null</code>. 210 */ 211 private static OpenSqlWriteStatement filterInternal(OpenSqlWriteStatement statement) { 212 if (statement.targetsInternalTable()) { 213 return null; 214 } 215 return statement; 216 } 217 218 /** DELETE */ 219 private static class TableDelete extends OpenSqlWriteStatement { 220 221 /** Constructor. */ 222 public TableDelete(ShallowEntity entity, List<IToken> tokens, ITypeResolution typeResolution) { 223 super(entity, tokens, typeResolution); 224 } 225 226 /** {@inheritDoc} */ 227 @Override 228 protected int getTableTokenIndex() { 229 if (startsWith(tokens, DELETE, FROM)) { 230 return 2; 231 } 232 return 1; 233 } 234 235 /** 236 * Checks whether a DELETE statement targets an internal table based on its 237 * syntax.<br> 238 * A DELETE statement is categorized as DELETE itab statement if it meets one of 239 * the following criteria: 240 * <ul> 241 * <li>has tokens DELETE TABLE directly after each other 242 * <li>has tokens DELETE ADJACENT DUPLICATES 243 * <li>has tokens DELETE and INDEX 244 * <li>has tokens DELETE and USING KEY 245 * <li>if it has a FROM token, the next token after FROM represents a number 246 * </ul> 247 */ 248 @Override 249 public boolean hasSureITabSyntax() { 250 if (TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.TABLE) 251 || TokenStreamUtils.startsWith(tokens, ETokenType.DELETE, ETokenType.ADJACENT, 252 ETokenType.DUPLICATES) 253 || TokenStreamUtils.containsAny(tokens, ETokenType.INDEX) || TokenStreamUtils 254 .containsSequence(tokens, 0, tokens.size() - 1, ETokenType.USING, ETokenType.KEY)) { 255 return true; 256 } 257 258 int indexOfFrom = TokenStreamUtils.firstTokenOfType(tokens, ETokenType.FROM); 259 if (indexOfFrom != TokenStreamUtils.NOT_FOUND) { 260 // if token after FROM is a number, it is the itab statement 261 // 'DELETE itab FROM idx1' 262 String text = tokens.get(indexOfFrom + 1).getText(); 263 return text.chars().allMatch(Character::isDigit); 264 } 265 266 return false; 267 } 268 } 269 270 /** INSERT */ 271 private static class TableInsert extends OpenSqlWriteStatement { 272 /** Constructor. */ 273 public TableInsert(ShallowEntity entity, List<IToken> tokens, ITypeResolution typeResolution) { 274 super(entity, tokens, typeResolution); 275 } 276 277 /** {@inheritDoc} */ 278 @Override 279 protected int getTableTokenIndex() { 280 if (startsWith(tokens, INSERT, INTO)) { 281 return 2; 282 } 283 return 1; 284 } 285 286 /** 287 * Checks whether an INSERT statement targets an internal table based on its 288 * syntax.<br> 289 * An insert statement is categorized as INSERT itab statement when it meets one 290 * of the following criteria: 291 * <ul> 292 * <li>If it has tokens 'INSERT' and 'INTO', but not consecutively. 293 * <li>has tokens INSERT and INDEX 294 * </ul> 295 * This also addresses an INSERT into a field-group, e.g. INSERT field_name INTO 296 * field_group_name. 297 */ 298 @Override 299 public boolean hasSureITabSyntax() { 300 boolean hasInsertIntoConsecutive = TokenStreamUtils.startsWith(tokens, ETokenType.INSERT, ETokenType.INTO); 301 boolean hasInsertIntoAnywhere = TokenStreamUtils.containsAll(tokens, 0, tokens.size() - 1, 302 ETokenType.INSERT, ETokenType.INTO); 303 return ((hasInsertIntoAnywhere && !hasInsertIntoConsecutive) 304 || TokenStreamUtils.containsAny(tokens, ETokenType.INDEX)); 305 } 306 } 307 308 /** MODIFY */ 309 private static class TableModify extends OpenSqlWriteStatement { 310 311 /** Constructor. */ 312 protected TableModify(ShallowEntity entity, List<IToken> tokens, ITypeResolution typeResolution) { 313 super(entity, tokens, typeResolution); 314 } 315 316 /** 317 * Checks whether a MODIFY statement targets an internal table based on its 318 * syntax.<br> 319 * A MODIFY statement is categorized as MODIFY itab statement if it meets one of 320 * the following criteria: 321 * <ul> 322 * <li>has tokens MODIFY TABLE directly after each other 323 * <li>has tokens MODIFY and INDEX 324 * <li>has tokens MODIFY and TRANSPORTING 325 * <li>has tokens MODIFY and ASSIGNING 326 * <li>has tokens MODIFY and WHERE 327 * <li>has tokens MODIFY and REFERENCE INTO 328 * <li>has tokens MODIFY and USING KEY 329 * </ul> 330 */ 331 @Override 332 public boolean hasSureITabSyntax() { 333 return TokenStreamUtils.startsWith(tokens, ETokenType.MODIFY, ETokenType.TABLE) 334 || TokenStreamUtils.containsAny(tokens, ETokenType.INDEX, ETokenType.TRANSPORTING, 335 ETokenType.ASSIGNING, ETokenType.WHERE) 336 || TokenStreamUtils.containsSequence(tokens, 0, tokens.size() - 1, ETokenType.REFERENCE, 337 ETokenType.INTO) 338 || TokenStreamUtils.containsSequence(tokens, 0, tokens.size() - 1, ETokenType.USING, 339 ETokenType.KEY); 340 } 341 } 342 343 /** UPDATE */ 344 private static class TableUpdate extends OpenSqlWriteStatement { 345 346 /** Constructor. */ 347 protected TableUpdate(ShallowEntity entity, List<IToken> tokens, ITypeResolution typeResolution) { 348 super(entity, tokens, typeResolution); 349 } 350 351 /** There is no update command for internal tables. */ 352 @Override 353 public boolean hasSureITabSyntax() { 354 return false; 355 } 356 357 } 358}