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}