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}