001/*-------------------------------------------------------------------------+ 002| | 003| Copyright (c) 2009-2017 CQSE GmbH | 004| | 005+-------------------------------------------------------------------------*/ 006package eu.cqse.check.framework.util.abap; 007 008import static eu.cqse.check.framework.scanner.ETokenType.ARROW; 009import static eu.cqse.check.framework.scanner.ETokenType.CALL; 010import static eu.cqse.check.framework.scanner.ETokenType.CHANGING; 011import static eu.cqse.check.framework.scanner.ETokenType.DOT; 012import static eu.cqse.check.framework.scanner.ETokenType.EQ; 013import static eu.cqse.check.framework.scanner.ETokenType.EQGT; 014import static eu.cqse.check.framework.scanner.ETokenType.EXCEPTIONS; 015import static eu.cqse.check.framework.scanner.ETokenType.EXPORTING; 016import static eu.cqse.check.framework.scanner.ETokenType.FUNCTION; 017import static eu.cqse.check.framework.scanner.ETokenType.GT; 018import static eu.cqse.check.framework.scanner.ETokenType.IDENTIFIER; 019import static eu.cqse.check.framework.scanner.ETokenType.IMPORTING; 020import static eu.cqse.check.framework.scanner.ETokenType.LPAREN; 021import static eu.cqse.check.framework.scanner.ETokenType.LT; 022import static eu.cqse.check.framework.scanner.ETokenType.METHOD; 023import static eu.cqse.check.framework.scanner.ETokenType.PERFORM; 024import static eu.cqse.check.framework.scanner.ETokenType.RECEIVING; 025import static eu.cqse.check.framework.scanner.ETokenType.RPAREN; 026import static eu.cqse.check.framework.scanner.ETokenType.TABLES; 027import static eu.cqse.check.framework.scanner.ETokenType.USING; 028 029import java.util.ArrayList; 030import java.util.Collections; 031import java.util.EnumSet; 032import java.util.HashMap; 033import java.util.List; 034import java.util.Map; 035import java.util.Optional; 036 037import org.conqat.lib.commons.assertion.CCSMAssert; 038import org.conqat.lib.commons.collections.CollectionUtils; 039import org.conqat.lib.commons.collections.Pair; 040import org.conqat.lib.commons.string.StringUtils; 041 042import eu.cqse.check.framework.scanner.ETokenType; 043import eu.cqse.check.framework.scanner.IToken; 044import eu.cqse.check.framework.shallowparser.TokenStreamTextUtils; 045import eu.cqse.check.framework.shallowparser.TokenStreamUtils; 046 047/** 048 * Tries to parse ABAP Method/Function calls 049 */ 050public class AbapMethodCallRecognizer { 051 052 /** "CONV" */ 053 private static final String CONV_LITERAL = "CONV"; 054 055 /** "CAST" */ 056 private static final String CAST_LITERAL = "CAST"; 057 058 /** Token types that start a section in a parameter list */ 059 private static final EnumSet<ETokenType> PARAMETER_TOKEN_TYPES = EnumSet.of(EXPORTING, IMPORTING, RECEIVING, 060 CHANGING, EXCEPTIONS, TABLES); 061 062 /** Tokens that terminate a section of a parameter list. */ 063 private static final EnumSet<ETokenType> PARAMETER_TERMINATING_TOKENS = EnumSet.of(EXPORTING, IMPORTING, RECEIVING, 064 CHANGING, EXCEPTIONS, TABLES, DOT, RPAREN); 065 066 /** The tokens that get parsed */ 067 private List<IToken> tokens; 068 /** Current position of the parser in the token list */ 069 private int currentPosition = 0; 070 /** Details on the method call. Gets filled during the parse process. */ 071 private final MethodCallInfo details = new MethodCallInfo(); 072 073 /** Constructor. */ 074 private AbapMethodCallRecognizer(List<IToken> tokens) { 075 this.tokens = tokens; 076 } 077 078 /** 079 * Start parsing 080 */ 081 public static Optional<MethodCallInfo> parse(List<IToken> tokens) { 082 AbapMethodCallRecognizer recognizer = new AbapMethodCallRecognizer(tokens); 083 return recognizer.parseMethodCall(); 084 } 085 086 /** 087 * Parses a single method call (in particular its parameters). This assumes that 088 * the given tokens have the form <code>( PARAMETERS )...</code>. Inserts the 089 * given method name as {@link MethodCallInfo#methodName} of the returned 090 * object. 091 */ 092 public static Optional<MethodCallInfo> parseParameterList(List<IToken> parameterListTokens, String methodName) { 093 int parameterStart = TokenStreamUtils.firstTokenOfType(parameterListTokens, LPAREN); 094 if (parameterStart == TokenStreamUtils.NOT_FOUND) { 095 return Optional.empty(); 096 } 097 MethodCallInfo result = new MethodCallInfo(); 098 result.methodName = methodName; 099 AbapMethodCallRecognizer recognizer = new AbapMethodCallRecognizer( 100 CollectionUtils.subListFrom(parameterListTokens, parameterStart)); 101 recognizer.currentPosition = 1; // ignore the LPAREN 102 103 boolean parsedSuccessful = recognizer.parseParameterDeclarations(result); 104 if (parsedSuccessful) { 105 return Optional.of(result); 106 } 107 return Optional.empty(); 108 } 109 110 /** Parses the method call. */ 111 private Optional<MethodCallInfo> parseMethodCall() { 112 if (tokens.isEmpty() || tokens.size() < 2) { 113 return Optional.empty(); 114 } 115 if (isTokenType(currentPosition, PERFORM)) { 116 // PERFORM is the 'CALL' keyword for FORMs 117 return parsePerformCall(tokens); 118 // we should distinguish between FORM calls and METHOD calls in the 119 // calldetails at some point. 120 } 121 return parseMethodOrFunctionCall(); 122 } 123 124 /** 125 * Parses a method or function call 126 */ 127 private Optional<MethodCallInfo> parseMethodOrFunctionCall() { 128 if (EnumSet.of(ARROW, EQ).contains(tokens.get(currentPosition + 1).getType())) { 129 if (!parseAssignee()) { 130 return Optional.empty(); 131 } 132 } 133 if (TokenStreamUtils.startsWith(tokens, CALL, FUNCTION)) { 134 currentPosition += 2; 135 details.setFunctionCall(true); 136 } else { 137 // CALL and METHOD are optional 138 skipOptionalTokens(CALL, METHOD); 139 } 140 if (tokens.size() <= currentPosition) { 141 return Optional.empty(); 142 } 143 if (isTokenType(currentPosition, IDENTIFIER)) { 144 if (!parseMethodName()) { 145 return Optional.empty(); 146 } 147 } else if (isTokenType(currentPosition, ETokenType.CHARACTER_LITERAL)) { 148 details.methodName = StringUtils 149 .stripSuffix(StringUtils.stripPrefix(tokens.get(currentPosition).getText(), "'"), "'"); 150 currentPosition++; 151 } else { 152 return Optional.empty(); 153 } 154 if (details.methodName.equalsIgnoreCase(CAST_LITERAL) || details.methodName.equalsIgnoreCase(CONV_LITERAL)) { 155 // this is an ABAP cast or type conversion (looks like a function 156 // call, but is none) 157 return Optional.empty(); 158 } 159 160 int positionBeforeParameters = currentPosition; 161 // without checking whether parseParameters actually advances, "x=y" 162 // would be recognized as method call 163 if (!parseParameters() || currentPosition == positionBeforeParameters) { 164 return Optional.empty(); 165 } 166 if (!isTokenType(currentPosition, DOT)) { 167 return Optional.empty(); 168 } 169 return Optional.of(details); 170 } 171 172 /** 173 * Advance the current position by the given distance. Returns whether the new 174 * position is still in the range of the token list. 175 */ 176 private boolean advance(int distance) { 177 currentPosition += distance; 178 return currentPosition < tokens.size(); 179 } 180 181 /** 182 * Skips tokens at the currentPosition while the current token is of the given 183 * types. 184 */ 185 private void skipOptionalTokens(ETokenType optionalType, ETokenType... optionalTypesRest) { 186 EnumSet<ETokenType> types = EnumSet.of(optionalType, optionalTypesRest); 187 while (types.contains(tokens.get(currentPosition).getType())) { 188 if (!advance(1)) { 189 return; 190 } 191 } 192 } 193 194 /** 195 * Parses the name of the called method 196 */ 197 private boolean parseMethodName() { 198 if (isTokenType(currentPosition + 1, EQGT)) { 199 details.methodContainerName = tokens.get(currentPosition).getText(); 200 details.isStaticCall = true; 201 details.methodName = tokens.get(currentPosition + 2).getText(); 202 if (!advance(3)) { 203 return false; 204 } 205 } else if (isTokenType(currentPosition + 1, ARROW)) { 206 details.methodContainerName = ""; 207 details.isStaticCall = false; 208 while (isTokenType(currentPosition + 1, ARROW)) { 209 details.methodContainerName += tokens.get(currentPosition).getText(); 210 if (!advance(2)) { 211 return false; 212 } 213 if (isTokenType(currentPosition + 1, ARROW)) { 214 details.methodContainerName += "->"; 215 } 216 } 217 details.methodName = tokens.get(currentPosition).getText(); 218 if (!advance(1)) { 219 return false; 220 } 221 } else { 222 details.methodName = tokens.get(currentPosition).getText(); 223 if (!advance(1)) { 224 return false; 225 } 226 } 227 return true; 228 } 229 230 /** 231 * Returns whether the token at the given position is of the given type. 232 */ 233 private boolean isTokenType(int position, ETokenType type) { 234 if (position > tokens.size()) { 235 return false; 236 } 237 return tokens.get(position).getType() == type; 238 } 239 240 /** 241 * Tries to parse a parameter list of a method call. Returns false if the 242 * parsing failed. 243 */ 244 private boolean parseParameters() { 245 if (!isTokenType(currentPosition, LPAREN)) { 246 return parseParameterDeclarations(details); 247 } 248 // functional call 249 // can be: meth(), meth(f), meth (p=f), or meth (p1=f1 ... pn=fn) 250 if (!advance(1)) {// skip the LPAREN "(" 251 return false; 252 } 253 if (skipMandatoryToken(RPAREN)) { 254 // meth(), we are done 255 return true; 256 } else if (isTokenType(currentPosition, IDENTIFIER) && isTokenType(currentPosition + 1, RPAREN)) { 257 // just one unnamed parameter meth(f) 258 details.unnamedParameters 259 .add(AbapLanguageFeatureParser.normalizeVariable(tokens.get(currentPosition).getText())); 260 if (!advance(2)) { 261 return false; 262 } 263 } else if (isTokenType(currentPosition + 1, EQ)) { 264 return parseNamedParameters(); 265 } else { 266 if (!parseParameterDeclarations(details)) { 267 return false; 268 } 269 if (!skipMandatoryToken(RPAREN)) { 270 return false; 271 } 272 } 273 return true; 274 } 275 276 /** 277 * Tries to parse a list of named parameters of a method call. Returns false if 278 * the parsing failed. 279 */ 280 private boolean parseNamedParameters() { 281 // meth (p=f), or meth (p1=f1 ... pn=fn) 282 try { 283 parseParameterListInto(details.namedParameters); 284 } catch (MethodCallParserException e) { 285 // syntax error while parsing a parameter list. 286 return false; 287 } 288 if (!skipMandatoryToken(RPAREN)) { 289 return false; 290 } 291 return true; 292 } 293 294 /** 295 * Skips the next token if it is of the given type. Returns whether the type was 296 * as expected. 297 */ 298 private boolean skipMandatoryToken(ETokenType expectedType) { 299 // check token type and skip if it is correct 300 if (!isTokenType(currentPosition, expectedType) || !advance(1)) { 301 return false; 302 } 303 return true; 304 } 305 306 /** 307 * Parses the name of an assignee. 308 */ 309 private boolean parseAssignee() { 310 // stores the beginning of identifiers that consist of multiple 311 // tokens. e.g., a->b->c = d->e->f(g, h). 312 String identifierStart = ""; 313 while (isTokenType(currentPosition + 1, ARROW)) { 314 identifierStart += AbapLanguageFeatureParser.normalizeVariable(tokens.get(currentPosition).getText()) 315 + "->"; 316 if (!advance(2)) { 317 return false; 318 } 319 } 320 if (isTokenType(currentPosition + 1, EQ)) { 321 details.assignee = identifierStart 322 + AbapLanguageFeatureParser.normalizeVariable(tokens.get(currentPosition).getText()); 323 if (!advance(2)) { 324 return false; 325 } 326 } 327 return true; 328 } 329 330 /** 331 * Parses a PERFORM call 332 */ 333 private Optional<MethodCallInfo> parsePerformCall(List<IToken> tokens) { 334 int currentPosition = 0; 335 MethodCallInfo details = new MethodCallInfo(); 336 details.isFormCall = true; 337 if (tokens.isEmpty() || tokens.size() < 2) { 338 return Optional.empty(); 339 } 340 if (isTokenType(currentPosition, PERFORM)) { 341 currentPosition++; // PERFORM is mandatory 342 } else { 343 return Optional.empty(); 344 } 345 if (isTokenType(currentPosition, IDENTIFIER)) { 346 details.methodName = tokens.get(currentPosition).getText(); 347 currentPosition++; 348 } else { 349 return Optional.empty(); 350 } 351 while (tokens.size() > currentPosition && !isTokenType(currentPosition, DOT)) { 352 if (EnumSet.of(USING, CHANGING).contains(tokens.get(currentPosition).getType())) { 353 // USING and CHANGING can be ignored, everything is parsed into 354 // one parameter list in ABAP 355 currentPosition++; 356 } else { 357 // must be an identifier 358 details.unnamedParameters 359 .add(AbapLanguageFeatureParser.normalizeVariable(tokens.get(currentPosition).getText())); 360 currentPosition++; 361 } 362 } 363 if (isTokenType(currentPosition, DOT)) { 364 return Optional.of(details); 365 } 366 return Optional.empty(); // DOT is missing 367 } 368 369 /** 370 * Parses a parameter declarations map such as "EXPORTING x=y IMPORTING a=b c=d 371 * ..." Returns the position of the next token that is not part of the parameter 372 * list. Returns false if the parameter list could not be parsed. 373 * 374 * @param details 375 * the MethodCallInfo details where parsed data is inserted 376 * @return whether this parsing step was successful 377 */ 378 private boolean parseParameterDeclarations(MethodCallInfo details) { 379 try { 380 while (PARAMETER_TOKEN_TYPES.contains(tokens.get(currentPosition).getType())) { 381 if (!parseParameterDeclarationSection(details)) { 382 return false; 383 } 384 } 385 } catch (MethodCallParserException e) { 386 // syntax error while parsing a parameter list. 387 return false; 388 } 389 return true; 390 } 391 392 /** Parses a parameter declaration section */ 393 private boolean parseParameterDeclarationSection(MethodCallInfo details) throws MethodCallParserException { 394 ETokenType sectionType = tokens.get(currentPosition).getType(); 395 if (!advance(1)) { 396 return false; 397 } 398 switch (sectionType) { 399 case EXPORTING: 400 parseParameterListInto(details.namedParameters); 401 break; 402 case IMPORTING: 403 parseParameterListInto(details.importingParameters); 404 break; 405 case CHANGING: 406 parseParameterListInto(details.changingParameters); 407 break; 408 case RECEIVING: 409 if (!parseReceivingParameter(details)) { 410 return false; 411 } 412 break; 413 case EXCEPTIONS: 414 parseParameterListInto(details.exceptions); 415 break; 416 case TABLES: 417 parseParameterListInto(details.tablesParameters); 418 break; 419 default: 420 CCSMAssert.fail("List of parameterTokenTypes is not in sync with switch cases."); 421 } 422 return true; 423 } 424 425 /** 426 * Parses a RECEIVING parameter. Returns whether parsing was successful. 427 */ 428 private boolean parseReceivingParameter(MethodCallInfo details) throws MethodCallParserException { 429 if (!TokenStreamUtils.hasTokenTypeSequence(tokens, currentPosition, IDENTIFIER, EQ, IDENTIFIER)) { 430 throw new MethodCallParserException( 431 "Did not find pattern \"x = y\" in receiving parameter at position " + currentPosition + " ."); 432 } 433 String formalParameterName = AbapLanguageFeatureParser.normalizeVariable(tokens.get(currentPosition).getText()); 434 String actualParameterReferece = AbapLanguageFeatureParser 435 .normalizeVariable(tokens.get(currentPosition + 2).getText()); 436 details.namedReturnParameter = new Pair<>(formalParameterName, actualParameterReferece); 437 if (!advance(3)) { 438 return false; 439 } 440 return true; 441 } 442 443 /** 444 * Parses multiple instances of the pattern "x = y" and inserts them into the 445 * given parameter list. Returns the position after the last pattern instance. 446 * The currentPosition after this method's execution does not contain an 447 * IDENTIFIER. 448 */ 449 private void parseParameterListInto(Map<String, String> parameterList) throws MethodCallParserException { 450 while (!PARAMETER_TOKEN_TYPES.contains(tokens.get(currentPosition).getType()) 451 && AbapLanguageFeatureParser.isPossiblyIdentifier(tokens.get(currentPosition))) { 452 String formalParameterName = AbapLanguageFeatureParser 453 .normalizeVariable(tokens.get(currentPosition).getText()); 454 String actualParameterReference; 455 if (!isTokenType(currentPosition + 1, EQ)) { 456 throw new MethodCallParserException( 457 "Did not find pattern \"x = y\" in parameter list element at position " + currentPosition 458 + " of tokenStream " + tokens.toString() + "."); 459 } 460 if (isTokenType(currentPosition + 2, LT) && tokens.get(currentPosition + 2).getText().equals("<")) { 461 if (!isTokenType(currentPosition + 4, GT)) { 462 throw new MethodCallParserException("Could not parse element as actual parameter at " 463 + currentPosition + " of tokenStream " + tokens.toString() + "."); 464 } 465 actualParameterReference = "<" 466 + AbapLanguageFeatureParser.normalizeVariable(tokens.get(currentPosition + 3).getText()) + ">"; 467 currentPosition += 5; 468 } else { 469 currentPosition += 2; 470 actualParameterReference = AbapLanguageFeatureParser.normalizeVariable(scanUntilParameterEnd()); 471 } 472 parameterList.put(formalParameterName, actualParameterReference); 473 } 474 } 475 476 /** 477 * Assumes that the currentPosition is a the start of an actual value assigned 478 * to a parameter. Scans tokens until the actual value ends (either ended by a 479 * {@link #PARAMETER_TERMINATING_TOKENS}, or because the next parameter starts). 480 */ 481 private String scanUntilParameterEnd() throws MethodCallParserException { 482 String actualParameterReference = ""; 483 int nextParameterTerminatingTokenIndex = TokenStreamUtils.findFirstTopLevel(tokens, currentPosition, 484 PARAMETER_TERMINATING_TOKENS, Collections.singletonList(LPAREN), Collections.singletonList(RPAREN)); 485 int nextEqTokenIndex = TokenStreamUtils.findFirstTopLevel(tokens, currentPosition, 486 // can't rely on ETokenType.EQ, "eq" is valid identifier 487 token -> token.getType() == EQ && token.getText().equals("="), Collections.singletonList(LPAREN), 488 Collections.singletonList(RPAREN)); 489 if (nextEqTokenIndex == TokenStreamUtils.NOT_FOUND || nextParameterTerminatingTokenIndex < nextEqTokenIndex) { 490 actualParameterReference = TokenStreamTextUtils 491 .concatTokenTexts(tokens.subList(currentPosition, nextParameterTerminatingTokenIndex), " "); 492 currentPosition = nextParameterTerminatingTokenIndex; 493 } else if (nextEqTokenIndex < nextParameterTerminatingTokenIndex) { 494 actualParameterReference = TokenStreamTextUtils 495 .concatTokenTexts(tokens.subList(currentPosition, nextEqTokenIndex - 1), " "); 496 currentPosition = nextEqTokenIndex - 1; 497 } else if (nextParameterTerminatingTokenIndex == TokenStreamUtils.NOT_FOUND) { 498 throw new MethodCallParserException("Did not find closing token of parameter at position " + currentPosition 499 + " of tokenStream " + tokens.toString() + "."); 500 } 501 return actualParameterReference; 502 } 503 504 /** 505 * Data class wrapping information on one method call (method name, parameter, 506 * ...) 507 */ 508 public static class MethodCallInfo { 509 510 /** Name of the called method */ 511 private String methodName; 512 513 /** 514 * Name of the class or identifier of the instance that should contain the 515 * method @see {@link #isStaticCall} 516 */ 517 private String methodContainerName; 518 519 /** 520 * Whether this is a static method call: (call with => in ABAP) or an instance 521 * method call (call with -> in ABAP) 522 */ 523 private boolean isStaticCall; 524 525 /** 526 * Whether this is a function call. (CALL FUNCTION ... in ABAP) 527 */ 528 private boolean isFunctionCall = false; 529 530 /** 531 * Ordered list of unnamed parameters. The list contains the names of the actual 532 * parameters in the caller method. 533 */ 534 private List<String> unnamedParameters = new ArrayList<>(); 535 /** 536 * named parameters (EXPORTING in ABAP). Formal parameter name is mapped to 537 * actual parameter name 538 */ 539 private Map<String, String> namedParameters = new HashMap<>(); 540 /** 541 * The caller variable that is assigned with the return value of the function 542 * call. E.g., "x" in "x = foo(...).". 543 */ 544 private String assignee; 545 /** 546 * IMPORTING parameters. Formal parameter name is mapped to actual parameter 547 * name 548 */ 549 private Map<String, String> importingParameters = new HashMap<>(); 550 /** 551 * CHANGING parameters. Formal parameter name is mapped to actual parameter name 552 */ 553 private Map<String, String> changingParameters = new HashMap<>(); 554 /** 555 * The RECEIVING parameter (if present in this call). Pair(formalParameterName, 556 * actualParameterName) 557 */ 558 private Pair<String, String> namedReturnParameter; 559 560 /** 561 * The TABLES parameters. Formal parameter name is mapped to actual parameter 562 * name 563 */ 564 public Map<String, String> tablesParameters = new HashMap<>(); 565 /** 566 * The EXCEPTIONS parameter of the ABAP Call. Formal parameter name is mapped to 567 * actual parameter name 568 */ 569 private Map<String, String> exceptions = new HashMap<>(); 570 571 /** Whether this is a PERFORM call */ 572 private boolean isFormCall = false; 573 574 /** @see #methodName */ 575 public String getMethodName() { 576 return methodName; 577 } 578 579 /** @see #methodContainerName */ 580 public String getMethodContainerName() { 581 return methodContainerName; 582 } 583 584 /** @see #isStaticCall */ 585 public boolean isStaticCall() { 586 return isStaticCall; 587 } 588 589 /** @see #isFunctionCall */ 590 public boolean isFunctionCall() { 591 return isFunctionCall; 592 } 593 594 /** @see #isFunctionCall */ 595 public void setFunctionCall(boolean isFunctionCall) { 596 this.isFunctionCall = isFunctionCall; 597 } 598 599 /** @see #unnamedParameters */ 600 public List<String> getUnnamedParameters() { 601 return unnamedParameters; 602 } 603 604 /** @see #namedParameters */ 605 public Map<String, String> getNamedParameters() { 606 return namedParameters; 607 } 608 609 /** @see #assignee */ 610 public String getAssignee() { 611 return assignee; 612 } 613 614 /** @see #importingParameters */ 615 public Map<String, String> getImportingParameters() { 616 return importingParameters; 617 } 618 619 /** @see #changingParameters */ 620 public Map<String, String> getChangingParameters() { 621 return changingParameters; 622 } 623 624 /** @see #namedReturnParameter */ 625 public Pair<String, String> getNamedReturnParameter() { 626 return namedReturnParameter; 627 } 628 629 /** @see #exceptions */ 630 public Map<String, String> getExceptions() { 631 return exceptions; 632 } 633 634 /** 635 * @see #isFormCall 636 */ 637 public boolean isFormCall() { 638 return isFormCall; 639 } 640 } 641 642 /** 643 * Exception for errors in the parse process. 644 */ 645 private class MethodCallParserException extends Exception { 646 /** Serial version UID. */ 647 private static final long serialVersionUID = 1L; 648 649 /** Constructor. */ 650 public MethodCallParserException(String message) { 651 super(message); 652 } 653 } 654}