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}