001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 the ConQAT Project                                   |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| You may obtain a copy of the License at                                  |
008|                                                                          |
009|    http://www.apache.org/licenses/LICENSE-2.0                            |
010|                                                                          |
011| Unless required by applicable law or agreed to in writing, software      |
012| distributed under the License is distributed on an "AS IS" BASIS,        |
013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
014| See the License for the specific language governing permissions and      |
015| limitations under the License.                                           |
016+-------------------------------------------------------------------------*/
017package eu.cqse.check.framework.util;
018
019import static eu.cqse.check.framework.scanner.ETokenType.BOOLEAN;
020import static eu.cqse.check.framework.scanner.ETokenType.BYTE;
021import static eu.cqse.check.framework.scanner.ETokenType.CHAR;
022import static eu.cqse.check.framework.scanner.ETokenType.COLON;
023import static eu.cqse.check.framework.scanner.ETokenType.DOT;
024import static eu.cqse.check.framework.scanner.ETokenType.DOUBLE;
025import static eu.cqse.check.framework.scanner.ETokenType.DOUBLE_COLON;
026import static eu.cqse.check.framework.scanner.ETokenType.FLOAT;
027import static eu.cqse.check.framework.scanner.ETokenType.IDENTIFIER;
028import static eu.cqse.check.framework.scanner.ETokenType.INT;
029import static eu.cqse.check.framework.scanner.ETokenType.LBRACE;
030import static eu.cqse.check.framework.scanner.ETokenType.LONG;
031import static eu.cqse.check.framework.scanner.ETokenType.RBRACE;
032import static eu.cqse.check.framework.scanner.ETokenType.SHORT;
033import static eu.cqse.check.framework.scanner.ETokenType.STATIC;
034import static eu.cqse.check.framework.scanner.ETokenType.VOID;
035
036import java.util.ArrayList;
037import java.util.Arrays;
038import java.util.Collections;
039import java.util.EnumSet;
040import java.util.HashSet;
041import java.util.List;
042import java.util.Set;
043
044import org.conqat.lib.commons.assertion.CCSMAssert;
045import org.conqat.lib.commons.collections.CollectionUtils;
046import org.conqat.lib.commons.collections.UnmodifiableList;
047import org.conqat.lib.commons.string.StringUtils;
048
049import eu.cqse.check.framework.scanner.ELanguage;
050import eu.cqse.check.framework.scanner.ETokenType;
051import eu.cqse.check.framework.scanner.IToken;
052import eu.cqse.check.framework.shallowparser.SubTypeNames;
053import eu.cqse.check.framework.shallowparser.TokenStreamTextUtils;
054import eu.cqse.check.framework.shallowparser.TokenStreamUtils;
055import eu.cqse.check.framework.shallowparser.framework.EShallowEntityType;
056import eu.cqse.check.framework.shallowparser.framework.ShallowEntity;
057import eu.cqse.check.framework.shallowparser.util.ShallowParsingUtils;
058import eu.cqse.check.framework.util.variable.CLikeVariableUseExtractor;
059
060/**
061 * Language feature parser for Java.
062 */
063public class JavaLanguageFeatureParser extends CLikeLanguageFeatureParserBase {
064
065        /** All token types that can be used to specify a non primitive type. */
066        public static final EnumSet<ETokenType> ADDITIONAL_TYPE_TOKENS = EnumSet.of(IDENTIFIER);
067
068        /** All token types that can be used to specify a primitive type. */
069        public static final EnumSet<ETokenType> PRIMITIVE_TYPE_TOKENS = EnumSet.of(BOOLEAN, CHAR, BYTE, SHORT, INT, LONG,
070                        FLOAT, DOUBLE, VOID);
071
072        /** All token types that can be used as identifier. */
073        public static final EnumSet<ETokenType> VALID_IDENTIFIERS = EnumSet.of(IDENTIFIER);
074
075        /** Names of classes that count as generic exceptions. */
076        private static final Set<String> GENERIC_EXCEPTION_NAMES = new HashSet<>();
077
078        static {
079                // Exception classes that count as generic exceptions
080                Set<Class<? extends Throwable>> genericExceptions = CollectionUtils.asHashSet(RuntimeException.class,
081                                Error.class, Throwable.class, Exception.class);
082                for (Class<? extends Throwable> clazz : genericExceptions) {
083                        GENERIC_EXCEPTION_NAMES.add(clazz.getSimpleName());
084                        GENERIC_EXCEPTION_NAMES.add(clazz.getName());
085                }
086        }
087
088        /** Constructor. */
089        public JavaLanguageFeatureParser() {
090                super(ELanguage.JAVA, VALID_IDENTIFIERS, PRIMITIVE_TYPE_TOKENS, ADDITIONAL_TYPE_TOKENS, DOT, ".",
091                                new CLikeVariableUseExtractor(EnumSet.of(DOT, DOUBLE_COLON), VALID_IDENTIFIERS));
092        }
093
094        /**
095         * Returns the imported package name from a given "import" entity. Static
096         * imports are ignored.
097         */
098        @Override
099        public String getImportName(ShallowEntity entity) {
100                CCSMAssert.isTrue(isImport(entity), "entity.getType() must be equal to EShallowEntityType.META and "
101                                + "entity.getSubtype() must be equal to SubTypeNames.IMPORT or SubTypeNames.STATIC_IMPORT");
102
103                List<IToken> tokens = entity.ownStartTokens();
104                if (TokenStreamUtils.firstTokenOfType(tokens, STATIC) != TokenStreamUtils.NOT_FOUND) {
105                        return null;
106                }
107
108                String importName = entity.getName();
109                return StringUtils.removeLastPart(importName, '.');
110        }
111
112        /**
113         * Returns whether the given entity is an import (including static imports).
114         */
115        @Override
116        public boolean isImport(ShallowEntity entity) {
117                return entity.getType().equals(EShallowEntityType.META) && (entity.getSubtype().equals(SubTypeNames.IMPORT)
118                                || entity.getSubtype().equals(SubTypeNames.STATIC_IMPORT));
119        }
120
121        /** Returns whether the given entity is a constant. */
122        public boolean isConstant(ShallowEntity entity) {
123                if (entity.getType() != EShallowEntityType.ATTRIBUTE) {
124                        return false;
125                }
126
127                // We require "static final", as "final" alone only indicates
128                // immutable field but not "classic" constants
129                if (TokenStreamUtils.containsAll(entity.ownStartTokens(), ETokenType.FINAL, ETokenType.STATIC)) {
130                        return true;
131                }
132
133                // If the attribute is part of an interface, it is always constant
134                ShallowEntity parent = entity.getParent();
135                return parent != null && parent.getType().equals(EShallowEntityType.TYPE)
136                                && (parent.getSubtype().equals(SubTypeNames.INTERFACE)
137                                                || parent.getSubtype().equals(SubTypeNames.ANNOTATION_INTERFACE));
138        }
139
140        /** Returns whether the given entity is a for each loop. */
141        public boolean isForEachLoop(ShallowEntity entity) {
142                if (entity.getType().equals(EShallowEntityType.STATEMENT) && entity.getSubtype().equals(SubTypeNames.FOR)) {
143                        return TokenStreamUtils.findFirstTopLevel(entity.ownStartTokens(), COLON, Arrays.asList(LBRACE),
144                                        Arrays.asList(RBRACE)) != TokenStreamUtils.NOT_FOUND;
145                }
146                return false;
147        }
148
149        /** Returns whether the given entity is an annotation. */
150        public boolean isAnnotation(ShallowEntity entity) {
151                return entity.getType() == EShallowEntityType.META && SubTypeNames.ANNOTATION.equals(entity.getSubtype());
152        }
153
154        /** Returns all annotation entities that annotate the given entity. */
155        public List<ShallowEntity> getAnnotations(ShallowEntity entity) {
156                return getAnnotations(entity, false);
157        }
158
159        /** Returns all annotation entities that annotate the given entity. */
160        public List<ShallowEntity> getAnnotations(ShallowEntity entity, boolean includeParentEntities) {
161                ShallowEntity parent = entity.getParent();
162                if (parent == null) {
163                        return CollectionUtils.emptyList();
164                }
165
166                Set<ShallowEntity> annotations = new HashSet<>();
167
168                UnmodifiableList<ShallowEntity> entities = parent.getChildren();
169                for (int i = entities.indexOf(entity) - 1; i >= 0; i--) {
170                        ShallowEntity previousEntity = entities.get(i);
171                        if (!isAnnotation(previousEntity)) {
172                                break;
173                        }
174                        annotations.add(previousEntity);
175                }
176
177                if (includeParentEntities) {
178                        annotations.addAll(getAnnotations(entity.getParent(), true));
179                }
180
181                return new ArrayList<>(annotations);
182        }
183
184        /**
185         * Returns all token lists representing thrown types of a method in the order of
186         * declaration.
187         */
188        public List<List<IToken>> getThrownTypesTokens(ShallowEntity methodEntity) {
189                CCSMAssert.isTrue(methodEntity.getType().equals(EShallowEntityType.METHOD),
190                                "Only methods can throw exceptions");
191
192                List<IToken> tokens = methodEntity.ownStartTokens();
193                List<IToken> thrownClauseTokens = TokenStreamUtils.tokensBetween(tokens, EnumSet.of(ETokenType.THROWS),
194                                EnumSet.of(ETokenType.LBRACE, ETokenType.SEMICOLON));
195
196                if (thrownClauseTokens.isEmpty()) {
197                        return CollectionUtils.emptyList();
198                }
199
200                return TokenStreamUtils.split(thrownClauseTokens, ETokenType.COMMA);
201        }
202
203        /**
204         * Returns the token of the throws keyword of a given method. If no throws
205         * keyword is found null is returned.
206         */
207        public IToken getThrowsToken(ShallowEntity methodEntity) {
208                CCSMAssert.isTrue(methodEntity.getType().equals(EShallowEntityType.METHOD),
209                                "Only methods can throw exceptions");
210
211                List<IToken> tokens = methodEntity.ownStartTokens();
212                int throwsTokenIndex = TokenStreamUtils.firstTokenOfType(tokens, ETokenType.THROWS);
213
214                if (throwsTokenIndex == TokenStreamUtils.NOT_FOUND) {
215                        return null;
216                }
217
218                return tokens.get(throwsTokenIndex);
219        }
220
221        /** Returns if the entity has at least one annotation. */
222        public boolean isAnnotated(ShallowEntity entity) {
223                return !getAnnotations(entity).isEmpty();
224        }
225
226        /**
227         * Get the detailed return type of a method. The detailed return type includes
228         * the type name and array and generic declarations.
229         */
230        public String getDetailedReturnType(ShallowEntity entity) {
231                CCSMAssert.isTrue(entity.getType() == EShallowEntityType.METHOD, "May only be applied to methods!");
232                CCSMAssert.isFalse(ShallowParsingUtils.isLambdaMethod(entity), "May not be applied to lambda methods!");
233
234                List<IToken> tokens = entity.ownStartTokens();
235                int nameIndex = getMethodNameIndex(entity, tokens);
236                List<IToken> typeTokens = getDetailedReturnTypeTokens(tokens, nameIndex);
237                return TokenStreamTextUtils.concatTokenTexts(typeTokens);
238        }
239
240        /**
241         * Returns the index to the name of the given entity within the given tokens.
242         */
243        private static int getMethodNameIndex(ShallowEntity entity, List<IToken> tokens) {
244                int nameIndex = 0;
245                for (IToken token : tokens) {
246                        if (token.getText().equals(entity.getName())) {
247                                nameIndex = tokens.indexOf(token);
248                        }
249                }
250                return nameIndex;
251        }
252
253        /**
254         * Returns the detailled return type tokens including arrays and generics from
255         * the given method tokens. The nameIndex must point to the name token of the
256         * method.
257         */
258        private static List<IToken> getDetailedReturnTypeTokens(List<IToken> tokens, int nameIndex) {
259                int startIndex = nameIndex - 1;
260                while (startIndex >= 2) {
261                        ETokenType startType = tokens.get(startIndex).getType();
262                        if (startType == ETokenType.RBRACK) {
263                                startIndex -= 2;
264                        } else if (startType == ETokenType.GT) {
265                                startIndex = TokenStreamUtils.findMatchingOpeningToken(tokens, startIndex - 1, ETokenType.LT,
266                                                ETokenType.GT) - 1;
267                        } else if (startType == ETokenType.IDENTIFIER && startIndex > 0
268                                        && tokens.get(startIndex - 1).getType() == ETokenType.DOT) {
269                                startIndex -= 2;
270                        } else {
271                                break;
272                        }
273                }
274                return tokens.subList(startIndex, nameIndex);
275        }
276
277        /**
278         * Determines whether the given class name represents a generic exception.
279         */
280        public boolean isGenericExceptionClass(String className) {
281                return GENERIC_EXCEPTION_NAMES.contains(className);
282        }
283
284        @Override
285        protected List<IToken> filterGenericTokens(List<IToken> genericTokens) {
286                if (genericTokens.isEmpty()) {
287                        return genericTokens;
288                }
289                // only return first token (i.e. skip "extends" part)
290                return Collections.singletonList(genericTokens.get(0));
291        }
292}