eu.cqse.check.framework.util.abap.FunctionCallInfo Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of teamscale-check-api Show documentation
Show all versions of teamscale-check-api Show documentation
The Teamscale Custom Check API allows users to extend Teamscale by writing custom analyses that create findings.
/*
* Copyright (c) CQSE GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.cqse.check.framework.util.abap;
import static eu.cqse.check.framework.scanner.ETokenType.CHANGING;
import static eu.cqse.check.framework.scanner.ETokenType.DOT;
import static eu.cqse.check.framework.scanner.ETokenType.EQ;
import static eu.cqse.check.framework.scanner.ETokenType.EXCEPTIONS;
import static eu.cqse.check.framework.scanner.ETokenType.EXCEPTION_TABLE;
import static eu.cqse.check.framework.scanner.ETokenType.EXPORTING;
import static eu.cqse.check.framework.scanner.ETokenType.IMPORTING;
import static eu.cqse.check.framework.scanner.ETokenType.LPAREN;
import static eu.cqse.check.framework.scanner.ETokenType.PARAMETER_TABLE;
import static eu.cqse.check.framework.scanner.ETokenType.RPAREN;
import static eu.cqse.check.framework.scanner.ETokenType.TABLES;
import static eu.cqse.check.framework.shallowparser.TokenStreamUtils.NOT_FOUND;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import org.conqat.lib.commons.string.StringUtils;
import com.google.common.annotations.VisibleForTesting;
import eu.cqse.check.framework.core.CheckException;
import eu.cqse.check.framework.core.util.CheckUtils;
import eu.cqse.check.framework.scanner.ETokenType;
import eu.cqse.check.framework.scanner.IToken;
import eu.cqse.check.framework.shallowparser.TokenStreamTextUtils;
import eu.cqse.check.framework.shallowparser.TokenStreamUtils;
/**
* Parses ABAP function calls and wraps information on the call, currently only called function name
* and exporting parameters.
*/
public class FunctionCallInfo {
/**
* Index of token where the function name starts (third token after 'CALL' and 'FUNCTION')
*/
private static final int NAME_START_TOKEN = 2;
/** Key word OTHERS for specifying default exceptions */
private static final String OTHERS = "OTHERS";
/**
* Set of {@link ETokenType}s for delimiters of parameter sections, the occurrence of such a token
* indicates that a parameter section ends before this token.
*/
private static final EnumSet PARAMETER_SECTION_DELIMITERS = EnumSet.of(EXPORTING, IMPORTING, TABLES,
CHANGING, EXCEPTIONS, PARAMETER_TABLE, EXCEPTION_TABLE, DOT);
/** Closing {@link ETokenType}s of nested method calls */
private static final List CLOSING_TOKENS = Collections.singletonList(RPAREN);
/** Opening {@link ETokenType}s of nested method calls */
private static final List OPENING_TOKENS = Collections.singletonList(LPAREN);
/**
* Constant for error codes which are not specified in EXCEPTIONS section
*/
private static final int UNSPECIFIED_ERROR_CODE = -1;
/**
* Name of the function, in the case of dynamic call of the function this holds the identifier name
*/
private final String functionName;
/**
* EXPORTING parameters. Formal parameter name is mapped to actual parameter name
*/
private final Map> exportingParameters = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
/**
* EXEPTIONS specification - exception name is mapped to assigned token.
*/
private final Map exceptionsSpecifiction = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
/**
* Tokens to parse
*/
private final List tokens;
/**
* Constructor
*
* @param tokens
* tokens of the function call must start with CALL FUNCTION
* @throws CheckException
* if parsing fails
*/
/* package */ FunctionCallInfo(List tokens) throws CheckException {
this.tokens = tokens;
functionName = parseFunctionName();
parseExportingParameters();
parseExceptions();
}
/**
* Parses the function name.
*/
private String parseFunctionName() {
int endOfName = TokenStreamUtils.firstTokenOfType(tokens,
PARAMETER_SECTION_DELIMITERS.toArray(new ETokenType[0]));
List calledFunctionTokens = tokens.subList(NAME_START_TOKEN, endOfName);
if (isStaticFunctionCall(calledFunctionTokens)) {
return CheckUtils.getUnquotedTextForCharacterLiteral(calledFunctionTokens.get(0));
}
return TokenStreamTextUtils.concatTokenTexts(calledFunctionTokens);
}
/**
* Checks if the given tokens refer to a static function call
*
* @param functionNameTokens
* tokens of the called function name
* @return true
if the function is called statically, e.g. only a character literal is
* stated
*/
private static boolean isStaticFunctionCall(List functionNameTokens) {
return functionNameTokens.size() == 1 && functionNameTokens.get(0).getType() == ETokenType.CHARACTER_LITERAL;
}
/**
* Parses the EXPORTING section and fills {@link #exportingParameters}.
*
* @throws CheckException
* in case {@link #tokens} are not well-formed
*/
private void parseExportingParameters() throws CheckException {
List exportingSectionTokens = getParameterSectionTokens(EXPORTING);
if (exportingSectionTokens.isEmpty()) {
return;
}
if (exportingSectionTokens.size() < 2) {
throw new CheckException("Unable to parse CALL FUNCTION: EXPORTING section of " + functionName
+ " does not start with 'param ='. (line " + tokens.get(0).getLineNumber() + ")");
}
int parameterStart = 0;
while (parameterStart != NOT_FOUND) {
int nextEq = TokenStreamUtils.findFirstTopLevel(exportingSectionTokens, parameterStart, EnumSet.of(EQ),
OPENING_TOKENS, CLOSING_TOKENS);
if (nextEq == NOT_FOUND) {
return;
}
String formalParameterText = TokenStreamTextUtils
.concatTokenTexts(exportingSectionTokens.subList(parameterStart, nextEq));
int actualStart = nextEq + 1;
int nextParameterStart = indexOfNextTokenAfterWhitespace(exportingSectionTokens, actualStart,
OPENING_TOKENS, CLOSING_TOKENS);
int actualEnd;
if (nextParameterStart == NOT_FOUND) {
actualEnd = exportingSectionTokens.size();
} else {
actualEnd = nextParameterStart;
}
exportingParameters.put(formalParameterText, exportingSectionTokens.subList(actualStart, actualEnd));
parameterStart = nextParameterStart;
}
}
/**
* Returns the index of the next token in the given list that is preceded by a whitespace char (to
* be precise: a char that is not part of any token; for example ' ', '\n', ...).
*
* tokens=ScannerUtils.getTokens("0 1( 4) 6\t\t7", ELanguage.ABAP);
* indexOfNextTokenAfterWhitespace(tokens, 1)
returns 6
(the parentheses are
* also individual tokens).
*
* Don't use this for C/C++ tokens that can contain macro expansions! This is based on the character
* offsets of tokens and "character offset" is not useful after preprocessor expansions (tokens that
* result from one expansion will all have the same character offset).
*/
@VisibleForTesting
/* package */static int indexOfNextTokenAfterWhitespace(List tokens, int startIndex,
List openTypes, List closingTypes) {
// we increment startIndex since we want the _next_ token after a whitespace
return TokenStreamUtils.findFirstTopLevelWithIndexPredicate(tokens, startIndex + 1, index -> {
if (index < 1) {
return false;
}
// this is the offset of the last char of the previous token
int previousEndOffset = tokens.get(index - 1).getEndOffset();
return previousEndOffset + 1 < tokens.get(index).getOffset();
}, openTypes, closingTypes);
}
/**
* Parses the EXCEPTIONS section, the normal format is
* exc1 = n1 exc2 = n2 ... [OTHERS =n_others]
where exec1 exec2 ... refer to name of
* non-class-based exceptions and n1, n2, ..., n_others refers to the error code which may be an
* integer value within [0..65535] or a string literal of an integer value. Read
* more infos.
*
* It is also possible to use a constant identifier, if this is the case
* {@link #UNSPECIFIED_ERROR_CODE} will be set as error code. (As it would be quite complex to
* resolve the value of the constant, the case that the constant could be mapped to 0 is ignored).
*
* Furthermore, there is also the obsolete short form of exc1 exc2 ..
which equivalent
* to exc1 = 1 exc2 = 1 ...
. In case of the old form {@link #UNSPECIFIED_ERROR_CODE} is
* set as error value for the exception name to be able to distinguish between actually specified
* error codes or the obsolete short form (which should be avoided).
* Read more
* infos.
*
* @throws CheckException
* in case {@link #tokens} are not well-formed
*/
private void parseExceptions() throws CheckException {
List sectionTokens = getParameterSectionTokens(EXCEPTIONS);
int currentIndex = 0;
while (currentIndex < sectionTokens.size()) {
String exceptionName = sectionTokens.get(currentIndex).getText();
if (currentIndex + 1 == sectionTokens.size() || sectionTokens.get(currentIndex + 1).getType() != EQ) {
// obsolete short form is used
exceptionsSpecifiction.put(exceptionName, UNSPECIFIED_ERROR_CODE);
currentIndex += 1;
continue;
}
currentIndex += 2;
IToken errorCodeToken = sectionTokens.get(currentIndex);
Integer returnCode = extractExceptionReturnCode(errorCodeToken);
if (returnCode != null) {
exceptionsSpecifiction.put(exceptionName, returnCode);
} else {
throw new CheckException(errorCodeToken.getType()
+ " detected but integer literal or identifier expected as exception code for "
+ exceptionName);
}
currentIndex += 1;
}
}
/**
* Gets the tokens of the parameter section which is introduced by an token of the given
* sectionType.
*
* @param sectionType
* {@link ETokenType} of the introducing token of the section
* @return tokens of the parameter section without the introducing token or an empty list if the
* section does not occur.
* @throws CheckException
* in case {@link #tokens} are not well-formed
*/
private List getParameterSectionTokens(ETokenType sectionType) throws CheckException {
int sectionStart = TokenStreamUtils.findFirstTopLevel(tokens, sectionType, OPENING_TOKENS, CLOSING_TOKENS);
while (sectionStart > 1 && (tokens.get(sectionStart - 1).getType() == EXCEPTION_TABLE
|| tokens.get(sectionStart - 1).getType() == PARAMETER_TABLE)) {
// the found section title is actually the name of a table used as argument of a
// EXCEPTION_TABLE or PARAMETER_TABLE section. Search for the next one.
sectionStart = TokenStreamUtils.findFirstTopLevel(tokens, sectionStart + 1,
Collections.singleton(sectionType), OPENING_TOKENS, CLOSING_TOKENS);
}
if (sectionStart == NOT_FOUND) {
return Collections.emptyList();
}
sectionStart++;
int sectionEnd = TokenStreamUtils.findFirstTopLevel(tokens, sectionStart + 1, PARAMETER_SECTION_DELIMITERS,
OPENING_TOKENS, CLOSING_TOKENS);
if (sectionEnd == NOT_FOUND) {
throw new CheckException("Unable to parse CALL FUNCTION: end token for EXPORTING section not found.");
}
return tokens.subList(sectionStart, sectionEnd);
}
/**
* Returns the exception return code of the given token or, {@code null} if the token does not
* represent a valid return code.
*/
private static Integer extractExceptionReturnCode(IToken errorCodeToken) {
if (errorCodeToken.getType() == ETokenType.INTEGER_LITERAL) {
return Integer.valueOf(errorCodeToken.getText());
} else if (AbapLanguageFeatureParser.isPossiblyIdentifier(errorCodeToken)) {
return UNSPECIFIED_ERROR_CODE;
} else if (errorCodeToken.getType() == ETokenType.CHARACTER_LITERAL
|| errorCodeToken.getType() == ETokenType.STRING_LITERAL) {
String errorCode = StringUtils.removeAll(errorCodeToken.getText(), "|", "`", "'");
if (StringUtils.isInteger(errorCode)) {
return Integer.valueOf(errorCode);
}
}
return null;
}
/** see {@link #functionName} */
public String getFunctionName() {
return functionName;
}
/**
* Gets the token which is passed to the given exporting parameter
*
* @return an {@link Optional} holding the single token which is passed to the given formal
* parameter. If the given exporting parameter is not set or a list of tokens is passed, the
* result is empty.
*/
public Optional getPassedExportingToken(String formalParamterName) {
List passed = exportingParameters.get(formalParamterName);
if (passed == null || passed.size() != 1) {
return Optional.empty();
}
return Optional.of(passed.get(0));
}
/**
* Checks if an error code is set for the given exception name, this is the case if a value not
* equal to {@value AbapCheckUtils#SY_SUBRC_NO_ERROR} is specified in the EXECPTIONS section or, if
* the value is not specified explicitly, the value for OTHERS is specified and not equal to
* {@value AbapCheckUtils#SY_SUBRC_NO_ERROR}.
*
* @return true
if an exception with this name specified and a value other than
* {@value AbapCheckUtils#SY_SUBRC_NO_ERROR} is assigned or, if the exception is not
* specified explicitly but OTHERS is specified to be not
* {@value AbapCheckUtils#SY_SUBRC_NO_ERROR}
*/
public boolean isSettingErrorCodeForException(String exceptionName) {
Integer errorCode = exceptionsSpecifiction.get(exceptionName);
if (errorCode == null) {
if (OTHERS.equals(exceptionName)) {
return false;
}
return isSettingErrorCodeForException(OTHERS);
}
return errorCode != AbapCheckUtils.SY_SUBRC_NO_ERROR;
}
}