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}