001package eu.cqse.check.framework.util.cs;
002
003import static eu.cqse.check.framework.scanner.ETokenType.LPAREN;
004import static eu.cqse.check.framework.scanner.ETokenType.MULTILINE_COMMENT;
005import static eu.cqse.check.framework.scanner.ETokenType.STRING_LITERAL;
006
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.EnumSet;
010import java.util.HashSet;
011import java.util.List;
012import java.util.Set;
013import java.util.regex.Matcher;
014import java.util.regex.Pattern;
015import java.util.stream.Collectors;
016
017import org.conqat.lib.commons.collections.SetMap;
018
019import eu.cqse.check.framework.scanner.ELanguage;
020import eu.cqse.check.framework.scanner.ETokenType;
021import eu.cqse.check.framework.scanner.IToken;
022import eu.cqse.check.framework.scanner.ScannerUtils;
023import eu.cqse.check.framework.shallowparser.framework.ShallowEntity;
024import eu.cqse.check.framework.shallowparser.languages.cs.CsShallowParser;
025import eu.cqse.check.framework.util.clike.CLikeCheckUtils;
026import eu.cqse.check.framework.util.tokens.TokenPattern;
027import eu.cqse.check.framework.util.tokens.TokenPatternMatch;
028
029/** Utility methods for analyzing C# code */
030public abstract class CsCheckUtils {
031
032        /**
033         * Matches an interpolation in a C# 6 interpolation string.
034         */
035        public static final Pattern INTERPOLATION_PATTERN = Pattern.compile("\\{([^}]*)[^}]*\\}");
036
037        /**
038         * Value for a unknown method arity. This value is used if a method name is
039         * found in a method reference.
040         */
041        public static final int UNKNOWN_ARITY = -1;
042        private static final String MAIN_METHOD_NAME = "Main";
043        private static final String TASK_TYPE_NAME = "Task";
044
045        /**
046         * The index for the group of the pattern
047         * {@link #POSSIBLE_METHOD_REFERENCE_PATTERN} which contains the actual
048         * identifier.
049         */
050        private static final int REFERENCE_GROUP_INDEX = 0;
051
052        /**
053         * Pattern that matches a possible reference to a method.
054         *
055         * A method reference here means any instance where a method is basically
056         * handled like an object or a reference, so any actual method invocation is not
057         * included.
058         *
059         * This means that we match any identifier which is NOT followed by a left
060         * paren.
061         */
062        private static final TokenPattern POSSIBLE_METHOD_REFERENCE_PATTERN = new TokenPattern()
063                        .sequence(CsShallowParser.VALID_IDENTIFIERS).group(REFERENCE_GROUP_INDEX)
064                        .sequence(EnumSet.complementOf(EnumSet.of(LPAREN)));
065
066        /**
067         * Determines, if the given method is a valid Main method according to the
068         * <a href=
069         * "https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/main-and-command-args/">Microsoft
070         * documentation</a>
071         */
072        public static boolean isMainMethod(ShallowEntity method) {
073                if (!MAIN_METHOD_NAME.equals(method.getName())) {
074                        return false;
075                }
076
077                boolean hasCorrectMainMethodArguments = hasCorrectMainMethodArguments(method);
078                if (!hasCorrectMainMethodArguments) {
079                        return false;
080                }
081
082                return hasCorrectMainMethodReturnTypeAndModifiers(method);
083        }
084
085        private static boolean hasCorrectMainMethodArguments(ShallowEntity method) {
086                List<IToken> argumentList = CLikeCheckUtils.getMethodArguments(method);
087                List<ETokenType> argTypes = argumentList.stream().map(IToken::getType).collect(Collectors.toList());
088
089                // Arguments can either be
090                // empty,
091                if (argumentList.size() == 0) {
092                        return true;
093                }
094
095                // string[] args,
096                if (argumentList.size() == 4) {
097                        List validTokens = Arrays.asList(ETokenType.STRING, ETokenType.LBRACK, ETokenType.RBRACK,
098                                        ETokenType.IDENTIFIER);
099                        return argTypes.equals(validTokens);
100                }
101
102                // or params string[] args
103                if (argumentList.size() == 5) {
104                        List validTokens = Arrays.asList(ETokenType.PARAMS, ETokenType.STRING, ETokenType.LBRACK, ETokenType.RBRACK,
105                                        ETokenType.IDENTIFIER);
106                        return argTypes.equals(validTokens);
107                }
108
109                return false;
110        }
111
112        private static boolean hasCorrectMainMethodReturnTypeAndModifiers(ShallowEntity method) {
113                List<IToken> modifierReturnTokens = CLikeCheckUtils.getMethodModifiersAndReturnType(method);
114
115                // Filter brackets and other irrelevant tokens
116                modifierReturnTokens = modifierReturnTokens.stream()
117                                .filter(token -> ETokenType.ETokenClass.KEYWORD == token.getType().getTokenClass()
118                                                || (ETokenType.ETokenClass.IDENTIFIER == token.getType().getTokenClass()))
119                                .collect(Collectors.toList());
120
121                boolean isStatic = modifierReturnTokens.stream().anyMatch(token -> ETokenType.STATIC == token.getType());
122                if (!isStatic) {
123                        return false;
124                }
125
126                Set<ETokenType> validModifierReturnTokens = new HashSet<>(
127                                Arrays.asList(ETokenType.PRIVATE, ETokenType.PROTECTED, ETokenType.PUBLIC, ETokenType.INTERNAL,
128                                                ETokenType.STATIC, ETokenType.VOID, ETokenType.INT));
129
130                // The async keyword is only allowed if the main method has return type Task
131                boolean hasReturnTypeTask = modifierReturnTokens.stream()
132                                .filter(token -> token.getType().isIdentifier() && TASK_TYPE_NAME.equals(token.getText()))
133                                .collect(Collectors.toList()).size() > 0;
134                if (hasReturnTypeTask) {
135                        validModifierReturnTokens.add(ETokenType.ASYNC);
136                }
137
138                return modifierReturnTokens.stream().allMatch(token -> validModifierReturnTokens.contains(token.getType())
139                                || (ETokenType.IDENTIFIER == token.getType() && TASK_TYPE_NAME.equals(token.getText())));
140        }
141
142        /**
143         * Retrieves all possible method references from the given tokens and adds them
144         * to the given SetMap.
145         *
146         * A method reference here means any instance where a method is basically
147         * handled like an object or a reference, so any actual method invocation is not
148         * included.
149         */
150        public static SetMap<String, Integer> retrieveMethodReferences(List<IToken> tokens) {
151                SetMap<String, Integer> result = new SetMap<>();
152                for (TokenPatternMatch match : POSSIBLE_METHOD_REFERENCE_PATTERN.findAll(tokens)) {
153                        result.add(match.groupString(REFERENCE_GROUP_INDEX), UNKNOWN_ARITY);
154                }
155                return result;
156        }
157
158        /**
159         * Filters a list of tokens for interpolated literal calls.
160         * <ul>
161         * <li>Example of an interpolated literal:
162         * <code>String string = $"{foo(a, b)} comment";</code></li>
163         * </ul>
164         *
165         * @param tokens
166         *            to check for interpolated literal tokens.
167         * @return interpolated literal tokens.
168         */
169        public static List<IToken> extractInterpolatedCalls(List<IToken> tokens) {
170                return tokens.stream()
171                                .filter(token -> token.getType() == STRING_LITERAL || token.getType() == MULTILINE_COMMENT)
172                                .filter(literal -> literal.getText().startsWith("$")).collect(Collectors.toList());
173        }
174
175        /**
176         * Tokenize the interpolated calls in string token.
177         *
178         * @param interpolatedStringToken
179         *            to be tokenized.
180         * @return interpolated variables and methods separated in a list of tokens.
181         */
182        public static List<List<IToken>> tokenizeInterpolatedCalls(List<IToken> interpolatedStringTokens) {
183                List<List<IToken>> readVariables = new ArrayList<>();
184                for (IToken interpolatedStringToken : interpolatedStringTokens) {
185                        Matcher matcher = INTERPOLATION_PATTERN.matcher(interpolatedStringToken.getText());
186                        while (matcher.find()) {
187                                String interpolation = matcher.group(0);
188                                readVariables.add(ScannerUtils.getTokens(interpolation, ELanguage.CS));
189                        }
190                }
191                return readVariables;
192        }
193
194        /**
195         * Checks whether the given variable name was used in a string interpolation in
196         * the given tokens. Returns a list with the indices of its occurrences.
197         */
198        public static List<Integer> checkForVariableUseInStringInterpolation(List<IToken> tokens, String variableName) {
199                List<Integer> uses = new ArrayList<>();
200                // Matches simple usages like {variableName}, {variableName++} or
201                // {-variableName}.
202                // Matches usages when the variable is used as method parameter
203                // {methodCall(..,variableName,..)} or
204                // (someObject.methodCall(..,variableName,..)}.
205                // Matches usages when a method is called on the variable
206                // {variableName.methodCall(...)}.
207                Pattern pattern = Pattern.compile("\\{(.*\\W)?" + Pattern.quote(variableName) + "(\\W.*)?\\}");
208                for (int index = 0; index < tokens.size(); index++) {
209                        IToken token = tokens.get(index);
210                        if (!token.getText().startsWith("$\"")) {
211                                continue;
212                        }
213
214                        if (pattern.matcher(token.getText()).find()) {
215                                uses.add(index);
216                        }
217                }
218                return uses;
219        }
220}