001package eu.cqse.check.framework.util.abap;
002
003import java.util.Arrays;
004import java.util.EnumSet;
005import java.util.List;
006import java.util.Optional;
007import java.util.Set;
008
009import org.conqat.lib.commons.assertion.CCSMAssert;
010import org.conqat.lib.commons.collections.CollectionUtils;
011import org.conqat.lib.commons.string.StringUtils;
012
013import eu.cqse.check.framework.core.CheckException;
014import eu.cqse.check.framework.core.ICheckContext;
015import eu.cqse.check.framework.core.phase.ECodeViewOption;
016import eu.cqse.check.framework.scanner.ETokenType;
017import eu.cqse.check.framework.scanner.IToken;
018import eu.cqse.check.framework.shallowparser.SubTypeNames;
019import eu.cqse.check.framework.shallowparser.framework.EShallowEntityType;
020import eu.cqse.check.framework.shallowparser.framework.ShallowEntity;
021import eu.cqse.check.framework.shallowparser.framework.ShallowEntityTraversalUtils;
022import eu.cqse.check.framework.typetracker.ITypeResolution;
023import eu.cqse.check.framework.typetracker.ScopedTypeLookup;
024import eu.cqse.check.framework.util.LanguageFeatureParser;
025
026/**
027 * Utility methods for checks analyzing ABAP code.
028 */
029public abstract class AbapCheckUtils {
030
031        /** Value of return code SY-SUBRC which indicates no error */
032        public static final int SY_SUBRC_NO_ERROR = 0;
033
034        /** A list of token types which can include a variable name. **/
035        public static final Set<ETokenType> TOKEN_TYPES_POTENTIALLY_CONTAINING_VARIABLE_NAMES = CollectionUtils
036                        .unionSet(ETokenType.IDENTIFIERS, ETokenType.KEYWORDS);
037
038        /** A list of entity types representing events. */
039        public static final Set<String> EVENT_TYPES = CollectionUtils.asHashSet(SubTypeNames.AT_LINE_SELECTION,
040                        SubTypeNames.PF_EVENT, SubTypeNames.AT_SELECTION_SCREEN, SubTypeNames.AT_USER_COMMAND,
041                        SubTypeNames.END_OF_PAGE, SubTypeNames.END_OF_SELECTION, SubTypeNames.INITIALIZATION,
042                        SubTypeNames.LOAD_OF_PROGRAM, SubTypeNames.START_OF_SELECTION, SubTypeNames.TOP_OF_PAGE,
043                        SubTypeNames.GET_LATE, SubTypeNames.GET, SubTypeNames.TOP_OF_PAGE_DURING_LINE_SELECTION);
044
045        /**
046         * Tokens that identify when to filter out an INCLUDE statements. We only want
047         * to find statements of type <code>INCLUDE foobar</code> where foobar is
048         * another ABAP source object, but not <code>INCLUDE TYPE foobar</code> or
049         * <code>INCLUDE STRUCTURE foobar</code>. Since one may name his included source
050         * object like an ABAP keyword, we need to have a blacklist approach here.
051         */
052        private static final Set<ETokenType> INCLUDE_FILTER_TYPES = EnumSet.of(ETokenType.STRUCTURE, ETokenType.TYPE);
053
054        /**
055         * All entity types whose children are in class scope.
056         */
057        public static final Set<String> PARENT_TYPES_OF_CLASS_SCOPE = CollectionUtils.asHashSet(
058                        SubTypeNames.CLASS_DEFINITION, SubTypeNames.CLASS_IMPLEMENTATION, SubTypeNames.INTERFACE_DEFINITION);
059
060        /**
061         * All entity types whose children are in global scope.
062         */
063        public static final Set<String> PARENT_TYPES_OF_GLOBAL_SCOPE = CollectionUtils.unionSet(
064                        CollectionUtils.asHashSet(SubTypeNames.REPORT, SubTypeNames.PROGRAM, SubTypeNames.DOCUMENT_ROOT),
065                        EVENT_TYPES);
066
067        /**
068         * All entity types whose children are in local scope.
069         *
070         * @see #isEntityWithinLocalValidScope(ShallowEntity)
071         */
072        public static final Set<String> PARENT_TYPES_OF_LOCAL_VALID_SCOPE = CollectionUtils.asHashSet(
073                        SubTypeNames.METHOD_DECLARATION, SubTypeNames.METHOD_IMPLEMENTATION, SubTypeNames.FORM,
074                        SubTypeNames.MODULE_INPUT, SubTypeNames.MODULE_OUTPUT, SubTypeNames.FUNCTION);
075
076        /**
077         * Meta comment which is added by Teamsale Git importer as last source line to
078         * indicate that an function module is RFC enabled.
079         */
080        public static final String META_COMMENT_IS_RFC_ENABLED = "*#$#* teamscale-meta[rfc-enabled] *#$#*";
081
082        /**
083         * Checks whether an entity is within the scope of a class.
084         *
085         * @param entity
086         *            The entity to be checked.
087         * @return Yes if passed entity is within class scope, false otherwise.
088         */
089        public static boolean isEntityWithinClassScope(ShallowEntity entity) {
090                CCSMAssert.isNotNull(entity.getParent(), "Entity " + entity + " must have a parent.");
091                String currSubtype = entity.getParent().getSubtype();
092                return PARENT_TYPES_OF_CLASS_SCOPE.contains(currSubtype);
093        }
094
095        /**
096         * Checks whether an entity is in global scope, i.e. is a child of a top entity.
097         *
098         * @param entity
099         *            The entity to be checked.
100         * @return Yes if passed entity is global, false otherwise.
101         */
102
103        public static boolean isEntityWithinGlobalScope(ShallowEntity entity) {
104                CCSMAssert.isNotNull(entity.getParent(), "Entity " + entity + " must have a parent.");
105                String currSubtype = entity.getParent().getSubtype();
106                return PARENT_TYPES_OF_GLOBAL_SCOPE.contains(currSubtype);
107        }
108
109        /**
110         * Checks whether an entity is within a valid, local scope. Valid local scopes
111         * are if the parent of the entity is e.g. a method, a form or a function.
112         * Invalid local scopes are if the parent is e.g. an IF statement.
113         *
114         * @param entity
115         *            The entity to be checked.
116         * @return true if passed entity is within local, valid scope, false otherwise.
117         */
118        public static boolean isEntityWithinLocalValidScope(ShallowEntity entity) {
119                CCSMAssert.isNotNull(entity.getParent(), "Entity " + entity + " must have a parent.");
120                String currSubtype = entity.getParent().getSubtype();
121                return PARENT_TYPES_OF_LOCAL_VALID_SCOPE.contains(currSubtype);
122        }
123
124        /**
125         * Disable check for inherited methods, since we cannot see their interface and
126         * hence cannot judge whether tables are passed as parameters or refer to the
127         * database.
128         *
129         * Also disable check for cases in which we cannot be sure we see all variables.
130         * This is true if
131         * <ul>
132         * <li>we have INCLUDEs in the code</li>
133         * <li>we are inside a program include</li>
134         * <li>we are inside a function group include</li>
135         * </ul>
136         *
137         * @param entity
138         *            The entity to check. May be a database statement or a macro.
139         * @param rootEntities
140         *            The corresponding file's root entities.
141         * @param uniformPath
142         *            The uniformPath to that file.
143         */
144        public static boolean isIgnoredForDatabaseChecks(ShallowEntity entity, List<ShallowEntity> rootEntities,
145                        String uniformPath) {
146                if (uniformPath.contains("/FUGR/") || containsInclude(rootEntities)) {
147                        return true;
148                }
149                if (uniformPath.contains("/PROG/") && !isReportOrProgram(rootEntities)) {
150                        return true;
151                }
152
153                ShallowEntity method = getMethodAncestor(entity);
154                if (method == null) {
155                        return false;
156                }
157
158                switch (method.getSubtype()) {
159                case SubTypeNames.METHOD_IMPLEMENTATION:
160                        return LanguageFeatureParser.ABAP.getDeclarationTokensForMethod(rootEntities, method) == null;
161                default:
162                        return false;
163                }
164        }
165
166        /**
167         * Whether the given identifier is considered a variable (rather than e.g. a
168         * database table) with regards to the current variable scope.
169         *
170         * @param identifier
171         *            The identifier inside a statement to check for being a variable.
172         * @param entity
173         *            The entity containing the statement. This may e.g. be an SQL
174         *            statement directly or a macro containing the statement.
175         * @param typeResolution
176         *            The type resolution. This typically comes directly from the check
177         *            context.
178         */
179        public static boolean isVariable(String identifier, ShallowEntity entity, ITypeResolution typeResolution) {
180                identifier = AbapLanguageFeatureParser.normalizeVariable(identifier);
181
182                // Ignore parameters and local variables.
183                ScopedTypeLookup currentTypeLookup = typeResolution.getTypeLookup(entity);
184                if (currentTypeLookup.containsVariable(identifier)) {
185                        return true;
186                }
187
188                // in case we are in a program or report, also check the type lookup of
189                // the corresponding PROGRAM or REPORT entity, since that is actually
190                // not a method, but the global scope.
191                ScopedTypeLookup globalLookup = getGlobalLookup(entity, typeResolution);
192                if (globalLookup != null && globalLookup.containsVariable(identifier)) {
193                        return true;
194                }
195
196                // in case the identifier points to a field of a structure, only the structure
197                // is the local variable. Therefore we also check the structure name. This also
198                // works with meshes, since these are basically structures with table-type
199                // fields.
200                if (identifier.contains("-")) {
201                        return isVariable(StringUtils.getFirstParts(identifier, 1, '-'), entity, typeResolution);
202                }
203                return false;
204        }
205
206        /**
207         * Whether the given list of root entities belong to a REPORT or PROGRAM.
208         */
209        private static boolean isReportOrProgram(List<ShallowEntity> rootEntities) {
210                return ShallowEntityTraversalUtils.listMethodsNonRecursive(rootEntities).stream()
211                                .anyMatch(method -> (method.getSubtype().equals(SubTypeNames.REPORT)
212                                                || method.getSubtype().equals(SubTypeNames.PROGRAM)));
213        }
214
215        /**
216         * If the entity belongs to a report or program, returns the type lookup for the
217         * REPORT or PROGRAM method scope, since this is actually the global scope.
218         * Returns null otherwise.
219         */
220        private static ScopedTypeLookup getGlobalLookup(ShallowEntity entity, ITypeResolution typeResolution) {
221                ShallowEntity method = getMethodAncestor(entity);
222                if (method == null) {
223                        return null;
224                }
225                return getReportOrProgram(method)
226                                // get the fist child, so the type lookup's scope is the report.
227                                .flatMap(r -> r.getChildren().stream().findFirst()).map(r -> typeResolution.getTypeLookup(r))
228                                .orElse(null);
229        }
230
231        /**
232         * Get the REPORT or PROGRAM sibling corresponding to the given method entity.
233         */
234        private static Optional<ShallowEntity> getReportOrProgram(ShallowEntity method) {
235                if (method.getParent() == null) {
236                        return Optional.empty();
237                }
238                return method.getParent().getChildrenOfType(EShallowEntityType.METHOD).stream()
239                                .filter(child -> (child.getSubtype().equals(SubTypeNames.REPORT)
240                                                || child.getSubtype().equals(SubTypeNames.PROGRAM)))
241                                .findFirst();
242        }
243
244        /**
245         * Checks whether the root entities contain an include.
246         */
247        private static boolean containsInclude(List<ShallowEntity> rootEntities) {
248                List<ShallowEntity> includes = ShallowEntityTraversalUtils.selectEntities(rootEntities,
249                                AbapCheckUtils::isInclude);
250                return !includes.isEmpty();
251        }
252
253        /**
254         * Searches for INCLUDE statements that are not INCLUDE TYPE or INCLUDE
255         * STRUCTURE.
256         */
257        private static boolean isInclude(ShallowEntity entity) {
258                if (entity.getType() != EShallowEntityType.STATEMENT || !entity.getSubtype().equals(SubTypeNames.INCLUDE)) {
259                        return false;
260                }
261                List<IToken> tokens = entity.ownStartTokens();
262                if (tokens.size() < 2 || INCLUDE_FILTER_TYPES.contains(tokens.get(1).getType())) {
263                        return false;
264                }
265                return true;
266        }
267
268        /**
269         * Returns the closest ancestor that is of type
270         * {@link EShallowEntityType#METHOD}.
271         */
272        private static ShallowEntity getMethodAncestor(ShallowEntity entity) {
273                while (entity != null) {
274                        if (entity.getType() == EShallowEntityType.METHOD) {
275                                return entity;
276                        }
277                        entity = entity.getParent();
278                }
279                return null;
280        }
281
282        /**
283         * Checks if the given entity is a function call of a function with any of the
284         * given names.
285         */
286        public static boolean isFunctionCallOfAny(ShallowEntity entity, String... functionNames) throws CheckException {
287                Optional<FunctionCallInfo> functionCallInfo = LanguageFeatureParser.ABAP.getFunctionCallInfo(entity);
288                if (!functionCallInfo.isPresent()) {
289                        return false;
290                }
291                if (!Arrays.asList(functionNames).contains(functionCallInfo.get().getFunctionName())) {
292                        return false;
293                }
294                return true;
295        }
296
297        /**
298         * Gets the normalized text of the start tokens of a {@link ShallowEntity}
299         * (typically for statement). Normalization is performed as follows:
300         * <p>
301         * 1) full comment lines (introduced with *) are removed
302         * <p>
303         * 2) mid-line comments (introduced with ") will be removed (unless a ' follows
304         * the " since it is assumed that the " is within a String literal in that
305         * case).
306         * <p>
307         * 3) white spaces before the closing . are removed
308         * <p>
309         * 4) all (sequences of) white spaces are replaced by a single space
310         */
311        public static String getNormalizedStartTokensText(ShallowEntity entity, ICheckContext context)
312                        throws CheckException {
313                List<IToken> tokens = entity.ownStartTokens();
314                if (tokens.isEmpty()) {
315                        return StringUtils.EMPTY_STRING;
316                }
317                String statementText = context.getTextContent(ECodeViewOption.ETextViewOption.FILTERED_CONTENT)
318                                .substring(tokens.get(0).getOffset(), tokens.get(tokens.size() - 1).getEndOffset() + 1);
319                return statementText.replaceAll("(?m)^\\*.*", StringUtils.EMPTY_STRING)
320                                .replaceAll("\"[^']*?\\n", StringUtils.SPACE).trim().replaceAll("\\s+\\.$", ".")
321                                .replaceAll("\\s+", StringUtils.SPACE);
322        }
323
324        /**
325         * Checks if the given content, what is expected to be the unfiltered content,
326         * contains the meta data comment for RFC enablement in the last line
327         */
328        public static boolean hasMetaCommentForRfcEnablement(String unfilteredContent) {
329                List<String> lines = StringUtils.splitLinesAsList(unfilteredContent);
330                return hasMetaCommentForRfcEnablement(lines);
331        }
332
333        /**
334         * Checks if the given content lines contains the meta data comment for RFC
335         * enablement in the last line
336         */
337        public static boolean hasMetaCommentForRfcEnablement(List<String> contentLines) {
338                if (contentLines.size() < 1) {
339                        return false;
340                }
341                return contentLines.get(contentLines.size() - 1).equals(META_COMMENT_IS_RFC_ENABLED);
342        }
343}