All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eu.cqse.check.framework.util.abap.FunctionCallInfo Maven / Gradle / Ivy

Go to download

The Teamscale Custom Check API allows users to extend Teamscale by writing custom analyses that create findings.

There is a newer version: 2024.7.2
Show newest version
/*
 * 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;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy