001/*-------------------------------------------------------------------------+ 002| | 003| Copyright 2005-2011 the ConQAT Project | 004| | 005| Licensed under the Apache License, Version 2.0 (the "License"); | 006| you may not use this file except in compliance with the License. | 007| You may obtain a copy of the License at | 008| | 009| http://www.apache.org/licenses/LICENSE-2.0 | 010| | 011| Unless required by applicable law or agreed to in writing, software | 012| distributed under the License is distributed on an "AS IS" BASIS, | 013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 014| See the License for the specific language governing permissions and | 015| limitations under the License. | 016+-------------------------------------------------------------------------*/ 017package eu.cqse.check.framework.util.abap; 018 019import static eu.cqse.check.framework.scanner.ETokenType.CHANGING; 020import static eu.cqse.check.framework.scanner.ETokenType.DEFAULT; 021import static eu.cqse.check.framework.scanner.ETokenType.DOT; 022import static eu.cqse.check.framework.scanner.ETokenType.EVENT; 023import static eu.cqse.check.framework.scanner.ETokenType.EXPORTING; 024import static eu.cqse.check.framework.scanner.ETokenType.FOR; 025import static eu.cqse.check.framework.scanner.ETokenType.IDENTIFIER; 026import static eu.cqse.check.framework.scanner.ETokenType.IMPORTING; 027import static eu.cqse.check.framework.scanner.ETokenType.LIKE; 028import static eu.cqse.check.framework.scanner.ETokenType.LPAREN; 029import static eu.cqse.check.framework.scanner.ETokenType.OPTIONAL; 030import static eu.cqse.check.framework.scanner.ETokenType.RAISING; 031import static eu.cqse.check.framework.scanner.ETokenType.RETURNING; 032import static eu.cqse.check.framework.scanner.ETokenType.RPAREN; 033import static eu.cqse.check.framework.scanner.ETokenType.STRUCTURE; 034import static eu.cqse.check.framework.scanner.ETokenType.TABLES; 035import static eu.cqse.check.framework.scanner.ETokenType.TESTING; 036import static eu.cqse.check.framework.scanner.ETokenType.TYPE; 037import static eu.cqse.check.framework.scanner.ETokenType.USING; 038 039import java.util.ArrayList; 040import java.util.Collections; 041import java.util.EnumSet; 042import java.util.List; 043import java.util.Optional; 044import java.util.regex.Pattern; 045 046import org.conqat.lib.commons.assertion.CCSMAssert; 047import org.conqat.lib.commons.collections.CollectionUtils; 048import org.conqat.lib.commons.collections.Pair; 049import org.conqat.lib.commons.string.StringUtils; 050 051import eu.cqse.check.framework.core.CheckException; 052import eu.cqse.check.framework.scanner.ELanguage; 053import eu.cqse.check.framework.scanner.ETokenType; 054import eu.cqse.check.framework.scanner.IToken; 055import eu.cqse.check.framework.shallowparser.SubTypeNames; 056import eu.cqse.check.framework.shallowparser.TokenStreamTextUtils; 057import eu.cqse.check.framework.shallowparser.TokenStreamUtils; 058import eu.cqse.check.framework.shallowparser.framework.EShallowEntityType; 059import eu.cqse.check.framework.shallowparser.framework.ShallowEntity; 060import eu.cqse.check.framework.shallowparser.framework.ShallowEntityTraversalUtils; 061import eu.cqse.check.framework.typetracker.TypedVariable; 062import eu.cqse.check.framework.util.ILanguageFeatureParser; 063 064/** 065 * Language feature parser for ABAP. 066 * 067 * Since ABAP is case-insensitive, all variables and types are lower-cased. 068 */ 069public class AbapLanguageFeatureParser implements ILanguageFeatureParser { 070 071 /** 072 * EnumSet containing all token types which can appear as section headers in a 073 * parameter declaration. 074 */ 075 public static final EnumSet<ETokenType> PARAMETER_SECTION_TOKENS = EnumSet.of(USING, CHANGING, IMPORTING, EXPORTING, 076 RETURNING, TABLES, TESTING, RAISING); 077 078 /** 079 * Pattern to match possible identifier names which were not parsed as 080 * identifiers but as keywords or operators. Note that this pattern does not 081 * comprise all valid identifiers namesn (which can contain some special 082 * characters), but only excludes keywords and operators which can not be used 083 * as identifiers. Thus the pattern does not match e.g. to the operator symbols 084 * like '=' or '<>' or keywords containing spaces. See also 085 * {@link #isPossiblyIdentifier(IToken)}. 086 */ 087 private static final Pattern KEYWORD_OR_OPERATOR_AS_IDENTIFIER_PATTERN = Pattern.compile("(?i)[A-Z][A-Z0-9_]*"); 088 089 /** {@inheritDoc} */ 090 @Override 091 public ELanguage getLanguage() { 092 return ELanguage.ABAP; 093 } 094 095 /** 096 * Gets the type name from the given list of tokens. 097 * 098 * @param tokens 099 * List of tokens. For example v1 TYPE t value(v2) LIKE c v TYPE t 100 * v3. 101 * 102 * @return Returns a tuple of the type name and the corresponding index of the 103 * last type token. The index is ensured to be bigger or equal to 104 * startIndex-1. If no further type is found, ("", startIndex-1) is 105 * returned. 106 */ 107 public Pair<String, Integer> getNextTypeName(List<IToken> tokens, int startIndex) { 108 int typeKeywordIndex = TokenStreamUtils.firstTokenOfType(tokens, startIndex, TYPE, LIKE, STRUCTURE); 109 if (typeKeywordIndex == TokenStreamUtils.NOT_FOUND) { 110 // No type is given 111 return new Pair<>(StringUtils.EMPTY_STRING, startIndex - 1); 112 } 113 114 // Find the beginning of the next type declaration and try to step 115 // backwards in order to find the end of the current variable type. This 116 // is done because you can put so many different expressions behind the 117 // type keyword, that it is nearly impossible to get an exhaustive list. 118 int endIndex = TokenStreamUtils.firstTokenOfType(tokens, typeKeywordIndex + 1, TYPE, LIKE, STRUCTURE); 119 if (endIndex == TokenStreamUtils.NOT_FOUND) { 120 endIndex = TokenStreamUtils.firstTokenOfType(tokens, typeKeywordIndex, DOT); 121 if (endIndex == TokenStreamUtils.NOT_FOUND) { 122 endIndex = tokens.size(); 123 } 124 } else if (endIndex >= 3 && tokens.get(endIndex - 3).getType() == LPAREN 125 && tokens.get(endIndex - 1).getType() == RPAREN) { 126 // If the next parameter is defined using a value(var2) or similar 127 // notation we have to skip those tokens in order to get to the 128 // type we are looking for. 129 endIndex -= 4; 130 } else { 131 // Normally the TYPE keyword is preceded by a parameter name, which 132 // we have to skip to reach the end of the previous parameter type. 133 endIndex--; 134 } 135 if (endIndex > 2 && endIndex <= tokens.size()) { 136 if (tokens.get(endIndex - 1).getType() == OPTIONAL) { 137 endIndex--; 138 } else if (tokens.get(endIndex - 2).getType() == DEFAULT) { 139 endIndex -= 2; 140 } 141 } 142 143 /* 144 * Type declarations may be ambiguous. Look at the following: I TYPE STRUCTURE 145 * TYPE STRUCTURE. This can either mean we have 5 variables (I, Type, STRUCTURE, 146 * ...) or (I of type STRUCTURE, TYPE and STRUCTURE) or (I, TYPE and STRUCTURE 147 * of type STRUCTURE) and so on. And we did not yet manage to find a reliable 148 * pattern on how the compiler parses those declarations. Especially because 149 * leaving out the type of a parameter is not officially documented in any ABAP 150 * book, but it is used throughout the whole MR project we used to test it. And 151 * the discussion is still open on how to deal with it. Currently we agreed on 152 * ignoring cases that we are not able to parse correctly and providing a 153 * workaround for cases where the above method fails. The workaround is what 154 * follows. 155 */ 156 if (typeKeywordIndex < endIndex) { 157 return new Pair<>(TokenStreamTextUtils 158 .concatTokenTexts(tokens.subList(typeKeywordIndex + 1, endIndex), StringUtils.SPACE).toLowerCase(), 159 endIndex - 1); 160 } 161 return new Pair<>(StringUtils.EMPTY_STRING, Math.max(startIndex, endIndex)); 162 } 163 164 /** 165 * Gets type information from a list of tokens holding a method's parameter 166 * declaration part, which may either use the 167 * <code>FOR EVENT ... OF ... IMPORTING</code> or the normal sectioned parameter 168 * style. 169 * 170 * The given {@link ShallowEntity} is stored as declaring entity in the returned 171 * {@link TypedVariable}s. 172 */ 173 public List<TypedVariable> getTypeInfoForMethodParameters(ShallowEntity entity, List<IToken> tokens) { 174 if (TokenStreamUtils.startsWith(tokens, FOR, EVENT)) { 175 return processMethodEventHandler(entity, tokens); 176 } 177 return processParameterList(entity, tokens); 178 } 179 180 /** 181 * Gets type information from a list of tokens holding a method's parameter 182 * declaration part, which may contain USING, CHANGING, IMPORTING, EXPORTING and 183 * RETURNING sections. 184 * 185 * For example <code>METHODS method IMPORTING v TYPE t value(v2) LIKE c v3 186 * CHANGING v4 TYPE t EXPORTING v5 TYPE t</code> 187 */ 188 private List<TypedVariable> processParameterList(ShallowEntity entity, List<IToken> tokens) { 189 List<TypedVariable> typeInfo = new ArrayList<>(); 190 List<Integer> positionsOfSectionTokens = TokenStreamUtils.findAll(tokens, PARAMETER_SECTION_TOKENS); 191 192 // Tokens before first parameter section token are irrelevant because 193 // they cannot declare parameters 194 for (int i = 0; i < positionsOfSectionTokens.size(); i++) { 195 int positionOfCurrentSectionToken = positionsOfSectionTokens.get(i); 196 IToken sectionName = tokens.get(positionOfCurrentSectionToken); 197 198 int endOfSection; 199 if (i < positionsOfSectionTokens.size() - 1) { 200 endOfSection = positionsOfSectionTokens.get(i + 1); 201 } else { 202 endOfSection = tokens.size(); 203 } 204 205 List<IToken> section = tokens.subList(positionOfCurrentSectionToken + 1, endOfSection); 206 processHeaderSection(entity, typeInfo, section, sectionName); 207 } 208 209 return typeInfo; 210 } 211 212 /** 213 * Processes a list of parameters that have been defined within the same 214 * parameter section. The detected parameters are added to the given list of 215 * typeInfos. 216 */ 217 private void processHeaderSection(ShallowEntity containingEntity, List<TypedVariable> typeInfo, 218 List<IToken> section, IToken sectionName) { 219 List<IToken> sectionModifiers = new ArrayList<>(); 220 sectionModifiers.add(sectionName); 221 222 int dotIndex = TokenStreamUtils.firstTokenOfType(section, DOT); 223 if (dotIndex != TokenStreamUtils.NOT_FOUND) { 224 section = section.subList(0, dotIndex); 225 } 226 int currentIndex = 0; 227 while (currentIndex < section.size()) { 228 List<IToken> currentParameterModifiers = new ArrayList<>(sectionModifiers); 229 String variableName; 230 // If the method parameter is marked as pass-by-value with 231 // <code>value(varName) TYPE t</code>, skip the parenthesized part 232 if (TokenStreamUtils.hasTokenTypeSequence(section, currentIndex + 1, LPAREN, IDENTIFIER, RPAREN)) { 233 variableName = section.get(currentIndex + 2).getText(); 234 currentIndex += 4; 235 } else { 236 variableName = section.get(currentIndex).getText(); 237 currentIndex++; 238 } 239 240 Pair<String, Integer> type = getNextTypeName(section, currentIndex); 241 Pair<List<IToken>, Integer> afterTypeModifiers = getAfterTypeModifiers(section, type.getSecond() + 1); 242 currentParameterModifiers.addAll(afterTypeModifiers.getFirst()); 243 244 typeInfo.add(new TypedVariable(normalizeVariable(variableName), type.getFirst().toLowerCase(), 245 currentParameterModifiers, containingEntity)); 246 currentIndex = afterTypeModifiers.getSecond() + 1; 247 } 248 } 249 250 /** 251 * Returns the modifier tokens which can be used after the type declaration of a 252 * parameter (e.g., OPTIONAL or DEFAULT 'x'). Returns the position of the last 253 * token of the modifiers as second return value. Returns startIndex-1 if no 254 * OPTIONAL or DEFAULT is found. 255 */ 256 private static Pair<List<IToken>, Integer> getAfterTypeModifiers(List<IToken> tokens, Integer startIndex) { 257 if (tokens.size() <= startIndex) { 258 return new Pair<>(Collections.emptyList(), startIndex - 1); 259 } 260 if (tokens.get(startIndex).getType() == OPTIONAL) { 261 return new Pair<>(Collections.singletonList(tokens.get(startIndex)), startIndex); 262 } else if (tokens.get(startIndex).getType() == DEFAULT) { 263 // also skip the default initialization value 264 return new Pair<>(Collections.singletonList(tokens.get(startIndex)), startIndex + 1); 265 } 266 return new Pair<>(Collections.emptyList(), startIndex - 1); 267 } 268 269 /** 270 * Gets type information from a list of tokens holding a method's parameter 271 * declaration part in the form of 272 * <code>([CLASS-]METHODS handler )FOR EVENT evt OF class|intf IMPORTING e1 e2 ...</code> 273 * The parameters that are returned are e1, e2, ... 274 */ 275 private static List<TypedVariable> processMethodEventHandler(ShallowEntity entity, List<IToken> tokens) 276 throws AssertionError { 277 int importingIndex = TokenStreamUtils.firstTokenOfType(tokens, IMPORTING); 278 if (importingIndex == TokenStreamUtils.NOT_FOUND) { 279 // Only happens in non standard conform code. 280 return CollectionUtils.emptyList(); 281 } 282 283 List<IToken> parameterTokens = tokens.subList(importingIndex + 1, tokens.size()); 284 return CollectionUtils.filterAndMap(parameterTokens, token -> token.getType() != DOT, 285 token -> new TypedVariable(normalizeVariable(token.getText()), StringUtils.EMPTY_STRING, 286 CollectionUtils.emptyList(), entity)); 287 } 288 289 /** 290 * Returns the method declaration which belongs to the given method 291 * implementation entity. May return <code>null</code> if the declaration was 292 * not found, because the rootEntities given are incomplete. 293 */ 294 public List<IToken> getDeclarationTokensForMethod(List<ShallowEntity> rootEntities, ShallowEntity methodEntity) { 295 ShallowEntity classDeclaration = getClassDeclaration(rootEntities, methodEntity.getParent()); 296 if (classDeclaration == null) { 297 return null; 298 } 299 List<List<IToken>> methodDeclarations = getMethodDeclarations(classDeclaration); 300 return methodDeclarations.stream() 301 .filter(declaration -> isMethodDeclarationFor(methodEntity.getName(), declaration)).findFirst() 302 .orElse(null); 303 } 304 305 /** 306 * Gets the corresponding class declaration for the given class implementation. 307 * This may return <code>null</code> if the rootEntities are incomplete. 308 */ 309 public static ShallowEntity getClassDeclaration(List<ShallowEntity> rootEntities, 310 ShallowEntity classImplementation) { 311 if (classImplementation == null || classImplementation.getName() == null) { 312 return null; 313 } 314 315 List<ShallowEntity> typeEntities = ShallowEntityTraversalUtils.listEntitiesOfType(rootEntities, 316 EShallowEntityType.TYPE); 317 for (ShallowEntity typeEntity : typeEntities) { 318 if (typeEntity.getSubtype().equals(SubTypeNames.CLASS_DEFINITION) 319 && classImplementation.getName().equals(typeEntity.getName())) { 320 return typeEntity; 321 } 322 } 323 return null; 324 } 325 326 /** 327 * Gets the corresponding method declaration for the given method implementation 328 * implementation. This may return <code>null</code> if the rootEntities are 329 * incomplete. 330 */ 331 public static ShallowEntity getMethodDeclaration(List<ShallowEntity> rootEntities, 332 ShallowEntity methodImplementation) { 333 ShallowEntity classDeclaration = null; 334 if (methodImplementation.getParent() != null) { 335 classDeclaration = getClassDeclaration(rootEntities, methodImplementation.getParent()); 336 } 337 if (classDeclaration == null) { 338 return null; 339 } 340 List<ShallowEntity> methodEntities = ShallowEntityTraversalUtils 341 .listEntitiesOfType(classDeclaration.getChildren(), EShallowEntityType.METHOD); 342 for (ShallowEntity methodEntity : methodEntities) { 343 if (methodEntity.getSubtype().equals(SubTypeNames.METHOD_DECLARATION) 344 && methodImplementation.getName().equals(methodEntity.getName())) { 345 return methodEntity; 346 } 347 } 348 return null; 349 } 350 351 /** 352 * Returns all method declarations which are defined inside the given class. 353 * Colon notations are already rolled out and removed in the returned list. 354 */ 355 private static List<List<IToken>> getMethodDeclarations(ShallowEntity classDeclaration) { 356 return CollectionUtils.filterAndMap(classDeclaration.getChildrenOfType(EShallowEntityType.METHOD), 357 method -> method.getSubtype().equals(SubTypeNames.METHOD_DECLARATION), ShallowEntity::includedTokens); 358 } 359 360 /** 361 * Returns whether the method name of the a given method declaration matches the 362 * given methodName. 363 * 364 * The declaration looks like <code>[CLASS-]METHODS name IMPORTING ...</code> 365 */ 366 private static boolean isMethodDeclarationFor(String methodName, List<IToken> declaration) { 367 CCSMAssert.isTrue(declaration.size() >= 2, "Method declaration is expected to always have 2 or more tokens."); 368 return declaration.get(1).getText().equalsIgnoreCase(methodName); 369 } 370 371 /** 372 * Parses a function call string. 373 * 374 * @return {@link Optional}, empty if the given entity is not a CALL FUNCTION 375 * statement otherwise containing a {@link FunctionCallInfo} 376 * @throws CheckException 377 * if the given statement can not be parsed 378 */ 379 public Optional<FunctionCallInfo> getFunctionCallInfo(ShallowEntity callFunctionStatement) throws CheckException { 380 if (callFunctionStatement.getType() != EShallowEntityType.STATEMENT) { 381 return Optional.empty(); 382 } 383 List<IToken> tokens = callFunctionStatement.ownStartTokens(); 384 if (tokens.size() < 3 || !TokenStreamUtils.startsWith(tokens, ETokenType.CALL, ETokenType.FUNCTION)) { 385 return Optional.empty(); 386 } 387 388 return Optional.of(new FunctionCallInfo(filterIllegalCharacterTokens(tokens))); 389 } 390 391 /** 392 * Filters illegal character tokens from the given token list. 393 */ 394 private static List<IToken> filterIllegalCharacterTokens(List<IToken> tokens) { 395 return CollectionUtils.filter(tokens, token -> token.getType() != ETokenType.ILLEGAL_CHARACTER); 396 } 397 398 /** 399 * Lower-cases and removes leading '!', since this does not belong to the 400 * variable name. 401 */ 402 public static String normalizeVariable(String name) { 403 return StringUtils.stripPrefix(name, "!").toLowerCase(); 404 } 405 406 /** 407 * 408 * Checks if a token is possibly an identifier. This also considers tokens which 409 * are wrongly parsed as keyword or operator. 410 * 411 * @return <code>true</code> if 1) the type of the given token is either an 412 * identifier (regardless if the text is a valid ABAP identifier name) 413 * 2) the type is a keyword or an operator and the token text is a 414 * possible variable name. <code>false</code> otherwise. 415 */ 416 public static boolean isPossiblyIdentifier(IToken token) { 417 ETokenType tokenType = token.getType(); 418 if (tokenType.isIdentifier()) { 419 return true; 420 } 421 if (tokenType.isKeyword() || tokenType.isOperator()) { 422 return KEYWORD_OR_OPERATOR_AS_IDENTIFIER_PATTERN.matcher(token.getText()).matches(); 423 } 424 return false; 425 } 426 427 // /** 428 // * Returns whether the given token can be an identifier in abap. Keywords are 429 // * valid identifiers in ABAP. Furthermore, teamscale parses some identifiers 430 // as 431 // * operators. The identifier "eq" is parsed as 432 // * {@link ETokenType#EQ}/{@link ETokenClass#OPERATOR}. 433 // */ 434 // public static boolean isValidAbapIdentifier(IToken token) { 435 // if (token.getType() == IDENTIFIER || token.getType().getTokenClass() == 436 // ETokenClass.KEYWORD) { 437 // return true; 438 // } 439 // // Operators are difficult since e.g., the identifier "eq" and the 440 // // operator "=" are both parsed as ETokenType.EQ. 441 // if (token.getType().getTokenClass() == ETokenClass.OPERATOR) { 442 // return token.getText().matches("[a-zA-Z0-9]*"); 443 // } 444 // return false; 445 // } 446}