001package eu.cqse.check.framework.util.abap; 002 003import static eu.cqse.check.framework.scanner.ETokenType.CHANGING; 004import static eu.cqse.check.framework.scanner.ETokenType.DOT; 005import static eu.cqse.check.framework.scanner.ETokenType.EQ; 006import static eu.cqse.check.framework.scanner.ETokenType.EXCEPTIONS; 007import static eu.cqse.check.framework.scanner.ETokenType.EXCEPTION_TABLE; 008import static eu.cqse.check.framework.scanner.ETokenType.EXPORTING; 009import static eu.cqse.check.framework.scanner.ETokenType.IMPORTING; 010import static eu.cqse.check.framework.scanner.ETokenType.LPAREN; 011import static eu.cqse.check.framework.scanner.ETokenType.PARAMETER_TABLE; 012import static eu.cqse.check.framework.scanner.ETokenType.RPAREN; 013import static eu.cqse.check.framework.scanner.ETokenType.TABLES; 014import static eu.cqse.check.framework.shallowparser.TokenStreamUtils.NOT_FOUND; 015 016import java.util.Arrays; 017import java.util.Collections; 018import java.util.EnumSet; 019import java.util.List; 020import java.util.Map; 021import java.util.Optional; 022import java.util.TreeMap; 023 024import eu.cqse.check.framework.core.CheckException; 025import eu.cqse.check.framework.core.util.CheckUtils; 026import eu.cqse.check.framework.scanner.ETokenType; 027import eu.cqse.check.framework.scanner.IToken; 028import eu.cqse.check.framework.shallowparser.TokenStreamTextUtils; 029import eu.cqse.check.framework.shallowparser.TokenStreamUtils; 030 031/** 032 * Parses ABAP function calls and wraps information on the call, currently only 033 * called function name and exporting parameters. 034 */ 035public class FunctionCallInfo { 036 037 /** 038 * Index of token where the function name starts (third token after 'CALL' and 039 * 'FUNCTION') 040 */ 041 private static final int NAME_START_TOKEN = 2; 042 043 /** Key word OTHERS for specifying default exceptions */ 044 private static final String OTHERS = "OTHERS"; 045 046 /** 047 * Set of {@link ETokenType}s for delimiters of parameter sections, the 048 * occurrence of such a token indicates that a parameter section ends before 049 * this token. 050 */ 051 private static final EnumSet<ETokenType> PARAMETER_SECTION_DELIMITERS = EnumSet.of(EXPORTING, IMPORTING, TABLES, 052 CHANGING, EXCEPTIONS, PARAMETER_TABLE, EXCEPTION_TABLE, DOT); 053 054 /** Closing {@link ETokenType}s of nested method calls */ 055 private static final List<ETokenType> CLOSING_TOKENS = Arrays.asList(RPAREN); 056 057 /** Opening {@link ETokenType}s of nested method calls */ 058 private static final List<ETokenType> OPENING_TOKENS = Arrays.asList(LPAREN); 059 060 /** 061 * Constant for error codes which are not specified in EXCEPTIONS section 062 */ 063 private static final int UNSPECIFIED_ERROR_CODE = -1; 064 065 /** 066 * Name of the function, in the case of dynamic call of the function this holds 067 * the identifier name 068 */ 069 private final String functionName; 070 071 /** 072 * EXPORTING parameters. Formal parameter name is mapped to actual parameter 073 * name 074 */ 075 private final Map<String, List<IToken>> exportingParameters = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 076 077 /** 078 * EXEPTIONS specification - exception name is mapped to assigned token. 079 */ 080 private final Map<String, Integer> exceptionsSpecifiction = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 081 082 /** 083 * Tokens to parse 084 */ 085 private final List<IToken> tokens; 086 087 /** 088 * Constructor 089 * 090 * @param tokens 091 * tokens of the function call must start with CALL FUNCTION 092 * @throws CheckException 093 * if parsing fails 094 */ 095 /* package */ FunctionCallInfo(List<IToken> tokens) throws CheckException { 096 this.tokens = tokens; 097 functionName = parseFunctionName(); 098 parseExportingParameters(); 099 parseExceptions(); 100 } 101 102 /** 103 * Parses the function name. 104 */ 105 private String parseFunctionName() { 106 int endOfName = TokenStreamUtils.firstTokenOfType(tokens, 107 PARAMETER_SECTION_DELIMITERS.toArray(new ETokenType[PARAMETER_SECTION_DELIMITERS.size()])); 108 109 List<IToken> calledFunctionTokens = tokens.subList(NAME_START_TOKEN, endOfName); 110 111 if (isStaticFunctionCall(calledFunctionTokens)) { 112 return CheckUtils.getUnquotedTextForCharacterLiteral(calledFunctionTokens.get(0)); 113 } 114 115 return TokenStreamTextUtils.concatTokenTexts(calledFunctionTokens); 116 } 117 118 /** 119 * Checks if the given tokens refer to a static function call 120 * 121 * @param functionNameTokens 122 * tokens of the called function name 123 * @return <code>true</code> if the function is called statically, e.g. only a 124 * character literal is stated 125 */ 126 private static boolean isStaticFunctionCall(List<IToken> functionNameTokens) { 127 return functionNameTokens.size() == 1 && functionNameTokens.get(0).getType() == ETokenType.CHARACTER_LITERAL; 128 } 129 130 /** 131 * Parses the EXPORTING section and fills {@link #exportingParameters}. 132 * 133 * @throws CheckException 134 * in case {@link #tokens} are not well-formed 135 */ 136 private void parseExportingParameters() throws CheckException { 137 List<IToken> exportingSectionTokens = getParameterSectionTokens(EXPORTING); 138 if (exportingSectionTokens.isEmpty()) { 139 return; 140 } 141 if (exportingSectionTokens.size() < 2 || exportingSectionTokens.get(1).getType() != EQ) { 142 throw new CheckException("Unable to parse CALL FUNCTION: EXPORTING section of " + functionName 143 + " does not start with 'param ='. (line " + tokens.get(0).getLineNumber() + ")"); 144 } 145 int nextEq = 1; 146 while (nextEq != NOT_FOUND) { 147 IToken formalParameter = exportingSectionTokens.get(nextEq - 1); 148 int actualStart = nextEq + 1; 149 nextEq = TokenStreamUtils.findFirstTopLevel(exportingSectionTokens, nextEq + 1, EnumSet.of(EQ), 150 OPENING_TOKENS, CLOSING_TOKENS); 151 int actuelEnd; 152 if (nextEq == NOT_FOUND) { 153 actuelEnd = exportingSectionTokens.size(); 154 } else { 155 actuelEnd = nextEq - 1; 156 } 157 exportingParameters.put(formalParameter.getText(), exportingSectionTokens.subList(actualStart, actuelEnd)); 158 } 159 } 160 161 /** 162 * Parses the EXCEPTIONS section, the normal format is 163 * <code>exc1 = n1 exc2 = n2 ... [OTHERS =n_others]</code> where exec1 exec2 ... 164 * refer to name of non-class-based exceptions and n1, n2, ..., n_others refers 165 * to the error code which must be an integer value within [0..65535]. See 166 * http://help.sap.com/abapdocu_751/en/abapcall_function_parameter.htm#! 167 * ABAP_ADDITION_6@6@ 168 * 169 * It is also possible to use a constant identifier, if this is the case 170 * {@link #UNSPECIFIED_ERROR_CODE} will be set as error code. (As it would be 171 * quite complex to resolve the value if the constant, the case the the constant 172 * could be mapped to 0 is ignored). 173 * 174 * Furthermore, there is also the obsolete short form of 175 * <code>exc1 exc2 ..</code> which equivalent to 176 * <code>exc1 = 1 exc2 = 1 ...</code>. In case of the old form 177 * {@link #UNSPECIFIED_ERROR_CODE} is set as error value for the exception name 178 * to be able to distinguish between actually specified error codes or the 179 * obsolete short form (which should be avoided). See also 180 * http://help.sap.com/abapdocu_751/en/abapcall_function_exc_short_form.htm 181 * 182 * @throws CheckException 183 * in case {@link #tokens} are not well-formed 184 */ 185 private void parseExceptions() throws CheckException { 186 List<IToken> sectionTokens = getParameterSectionTokens(EXCEPTIONS); 187 int currentIndex = 0; 188 while (currentIndex < sectionTokens.size()) { 189 String exceptionName = sectionTokens.get(currentIndex).getText(); 190 if (currentIndex + 1 == sectionTokens.size() || sectionTokens.get(currentIndex + 1).getType() != EQ) { 191 // obsolete short form is used 192 exceptionsSpecifiction.put(exceptionName, UNSPECIFIED_ERROR_CODE); 193 currentIndex += 1; 194 continue; 195 } 196 currentIndex += 2; 197 IToken errorCodeToken = sectionTokens.get(currentIndex); 198 if (errorCodeToken.getType() == ETokenType.INTEGER_LITERAL) { 199 exceptionsSpecifiction.put(exceptionName, Integer.valueOf(errorCodeToken.getText())); 200 } else if (AbapLanguageFeatureParser.isPossiblyIdentifier(errorCodeToken)) { 201 exceptionsSpecifiction.put(exceptionName, UNSPECIFIED_ERROR_CODE); 202 } else { 203 throw new CheckException(errorCodeToken.getType() 204 + " detected but integer literal or identifier expected as exception code for " 205 + exceptionName); 206 } 207 currentIndex += 1; 208 } 209 } 210 211 /** 212 * Gets the tokens of the parameter section which is introduced by an token of 213 * the given sectionType. 214 * 215 * @param sectionType 216 * {@link ETokenType} of the introducing token of the section 217 * @return tokens of the parameter section without the introducing token or an 218 * empty list if the section does not occur. 219 * @throws CheckException 220 * in case {@link #tokens} are not well-formed 221 */ 222 private List<IToken> getParameterSectionTokens(ETokenType sectionType) throws CheckException { 223 int sectionStart = TokenStreamUtils.findFirstTopLevel(tokens, sectionType, OPENING_TOKENS, CLOSING_TOKENS); 224 if (sectionStart == NOT_FOUND) { 225 return Collections.emptyList(); 226 } 227 sectionStart++; 228 int sectionEnd = TokenStreamUtils.findFirstTopLevel(tokens, sectionStart + 1, PARAMETER_SECTION_DELIMITERS, 229 OPENING_TOKENS, CLOSING_TOKENS); 230 if (sectionEnd == NOT_FOUND) { 231 throw new CheckException("Unable to parse CALL FUNCTION: end token for EXPORTING section not found."); 232 } 233 return tokens.subList(sectionStart, sectionEnd); 234 } 235 236 /** see {@link #functionName} */ 237 public String getFunctionName() { 238 return functionName; 239 } 240 241 /** 242 * Gets the token which is passed to the given exporting parameter 243 * 244 * @return an {@link Optional} holding the single token which is passed to the 245 * given formal parameter. If the given exporting parameter is not set 246 * or a list of tokens is passed, the result is empty. 247 */ 248 public Optional<IToken> getPassedExportingToken(String formalParamterName) { 249 List<IToken> passed = exportingParameters.get(formalParamterName); 250 if (passed == null || passed.size() != 1) { 251 return Optional.empty(); 252 } 253 return Optional.of(passed.get(0)); 254 } 255 256 /** 257 * Checks if an error code is set for the given exception name, this is the case 258 * if a value not equal to {@value AbapCheckUtils#SY_SUBRC_NO_ERROR} is 259 * specified in the EXECPTIONS section or, if the value is not specified 260 * explicitly, the value for OTHERS is specified and not equal to 261 * {@value AbapCheckUtils#SY_SUBRC_NO_ERROR}. 262 * 263 * @return <code>true</code> if an exception with this name specified and a 264 * value other than {@value AbapCheckUtils#SY_SUBRC_NO_ERROR} is 265 * assigned or, if the exception is not specified explicitly but OTHERS 266 * is specified to be not {@value AbapCheckUtils#SY_SUBRC_NO_ERROR} 267 */ 268 public boolean isSettingErrorCodeForException(String exceptionName) { 269 Integer errorCode = exceptionsSpecifiction.get(exceptionName); 270 if (errorCode == null) { 271 if (OTHERS.equals(exceptionName)) { 272 return false; 273 } 274 return isSettingErrorCodeForException(OTHERS); 275 } 276 if (errorCode == AbapCheckUtils.SY_SUBRC_NO_ERROR) { 277 return false; 278 } 279 return true; 280 } 281}