diff --git a/src/main/java/spoon/support/visitor/SubInheritanceHierarchyResolver.java b/src/main/java/spoon/support/visitor/SubInheritanceHierarchyResolver.java new file mode 100644 index 00000000000..6abd297df2f --- /dev/null +++ b/src/main/java/spoon/support/visitor/SubInheritanceHierarchyResolver.java @@ -0,0 +1,232 @@ +/** + * Copyright (C) 2006-2017 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.support.visitor; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashSet; +import java.util.Set; + +import spoon.SpoonException; +import spoon.reflect.declaration.CtClass; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtEnum; +import spoon.reflect.declaration.CtPackage; +import spoon.reflect.declaration.CtType; +import spoon.reflect.declaration.CtTypeInformation; +import spoon.reflect.declaration.CtTypeParameter; +import spoon.reflect.reference.CtTypeReference; +import spoon.reflect.visitor.Filter; +import spoon.reflect.visitor.chain.CtConsumer; +import spoon.reflect.visitor.chain.CtQuery; +import spoon.reflect.visitor.chain.CtScannerListener; +import spoon.reflect.visitor.chain.ScanningMode; +import spoon.reflect.visitor.filter.CtScannerFunction; +import spoon.reflect.visitor.filter.SuperInheritanceHierarchyFunction; +import spoon.reflect.visitor.filter.TypeFilter; + +import static spoon.reflect.visitor.chain.ScanningMode.NORMAL; +import static spoon.reflect.visitor.chain.ScanningMode.SKIP_ALL; + +/** + * Expects a {@link CtPackage} as input + * and upon calls to forEachSubTypeInPackage produces all sub classes and sub interfaces, which extends or implements super type(s) provided in constructor and stored as `targetSuperTypes`.
+ * + * The repeated processing of this mapping function on the same input returns only newly found sub types. + * The instance of {@link SubInheritanceHierarchyResolver} returns found sub types only once. + * So repeated call with same input package returns nothing. + * Create and use new instance of {@link SubInheritanceHierarchyResolver} if you need to scan the subtype hierarchy again. + */ +public class SubInheritanceHierarchyResolver { + + /** where the subtypes will be looked for */ + private CtPackage inputPackage; + + /** whether interfaces are included in the result */ + private boolean includingInterfaces = true; + /** + * Set of qualified names of all super types whose sub types we are searching for. + * Each found sub type is added to this set too + */ + private Set targetSuperTypes = new HashSet<>(); + /** + * if true then we have to check if type is a subtype of superClass or superInterfaces too + * if false then it is enough to search in superClass hierarchy only (faster) + */ + private boolean hasSuperInterface = false; + + private boolean failOnClassNotFound = false; + + public SubInheritanceHierarchyResolver(CtPackage input) { + inputPackage = input; + } + + /** + * Add another super type to this mapping function. + * Using this function you can search parallel in more sub type hierarchies. + * + * @param superType - the type whose sub types will be returned by this mapping function too. + */ + public SubInheritanceHierarchyResolver addSuperType(CtTypeInformation superType) { + targetSuperTypes.add(superType.getQualifiedName()); + if (hasSuperInterface == false) { + hasSuperInterface = superType.isInterface(); + } + return this; + } + + /** + * @param includingInterfaces if false then interfaces are not visited - only super classes. By default it is true. + */ + public SubInheritanceHierarchyResolver includingInterfaces(boolean includingInterfaces) { + this.includingInterfaces = includingInterfaces; + return this; + } + + /** + * @param failOnClassNotFound sets whether processing should throw an exception if class is missing in noClassPath mode + */ + public SubInheritanceHierarchyResolver failOnClassNotFound(boolean failOnClassNotFound) { + this.failOnClassNotFound = failOnClassNotFound; + return this; + } + + /** + * Calls `outputConsumer.apply(subType)` for each sub type of the targetSuperTypes that are found in `inputPackage`. + * Each sub type is returned only once. + * It makes sense to call this method again for example after new super types are added + * by {@link #addSuperType(CtTypeInformation)}. + * + * If this method is called again with same input and configuration, nothing in sent to outputConsumer + * @param outputConsumer the consumer for found sub types + */ + public void forEachSubTypeInPackage(final CtConsumer outputConsumer) { + /* + * Set of qualified names of all visited types, independent on whether they are sub types or not. + */ + final Set allVisitedTypeNames = new HashSet<>(); + /* + * the queue of types whose super inheritance hierarchy we are just visiting. + * They are potential sub types of an `targetSuperTypes` + */ + final Deque> currentSubTypes = new ArrayDeque<>(); + //algorithm + //1) query step: scan input package for sub classes and sub interfaces + final CtQuery q = inputPackage.map(new CtScannerFunction()); + //2) query step: visit only required CtTypes + if (includingInterfaces) { + //the client is interested in sub inheritance hierarchy of interfaces too. Check interfaces, classes, enums, Annotations, but not CtTypeParameters. + q.select(typeFilter); + } else { + //the client is not interested in sub inheritance hierarchy of interfaces. Check only classes and enums. + q.select(classFilter); + } + /* + * 3) query step: for each found CtType, visit it's super inheritance hierarchy and search there for a type which is equal to one of targetSuperTypes. + * If found then all sub types in hierarchy (variable `currentSubTypes`) are sub types of targetSuperTypes. So return them + */ + q.map(new SuperInheritanceHierarchyFunction() + //if there is any interface between `targetSuperTypes`, then we have to check superInterfaces too + .includingInterfaces(hasSuperInterface) + .failOnClassNotFound(failOnClassNotFound) + /* + * listen for types in super inheritance hierarchy + * 1) to collect `currentSubTypes` + * 2) to check if we have already found a targetSuperType + * 3) if found then send `currentSubTypes` to `outputConsumer` and skip visiting of further super types + */ + .setListener(new CtScannerListener() { + @Override + public ScanningMode enter(CtElement element) { + final CtTypeReference typeRef = (CtTypeReference) element; + String qName = typeRef.getQualifiedName(); + if (targetSuperTypes.contains(qName)) { + /* + * FOUND! we are in super inheritance hierarchy, which extends from an searched super type(s). + * All `currentSubTypes` are sub types of searched super type + */ + while (currentSubTypes.size() > 0) { + final CtTypeReference currentTypeRef = currentSubTypes.pop(); + String currentQName = currentTypeRef.getQualifiedName(); + /* + * Send them to outputConsumer and add then as targetSuperTypes too, to perform faster with detection of next sub types. + */ + if (!targetSuperTypes.contains(currentQName)) { + targetSuperTypes.add(currentQName); + outputConsumer.accept((T) currentTypeRef.getTypeDeclaration()); + } + } + //we do not have to go deeper into super inheritance hierarchy. Skip visiting of further super types + //but continue visiting of siblings (do not terminate query) + return SKIP_ALL; + } + if (allVisitedTypeNames.add(qName) == false) { + /* + * this type was already visited, by another way. So it is not sub type of `targetSuperTypes`. + * Stop visiting it's inheritance hierarchy. + */ + return SKIP_ALL; + } + /* + * This type was not visited yet. + * We still do not know whether this type is a sub type of any target super type(s) + * continue searching in super inheritance hierarchy + */ + currentSubTypes.push(typeRef); + return NORMAL; + } + @Override + public void exit(CtElement element) { + CtTypeInformation type = (CtTypeInformation) element; + if (currentSubTypes.isEmpty() == false) { + //remove current type, which is not a sub type of targetSuperTypes from the currentSubTypes + CtTypeInformation stackType = currentSubTypes.pop(); + if (stackType != type) { + //the enter/exit was not called consistently. There is a bug in SuperInheritanceHierarchyFunction + throw new SpoonException("CtScannerListener#exit was not called after enter."); + } + } + } + }) + ).forEach(new CtConsumer>() { + @Override + public void accept(CtType type) { + //we do not care about types visited by query `q`. + //the result of whole mapping function was already produced by `sendResult` call + //but we have to consume all these results to let query running + } + }); + } + + /** + * accept all {@link CtType} excluding {@link CtTypeParameter} + */ + private static final Filter> typeFilter = new Filter>() { + @Override + public boolean matches(CtType type) { + if (type instanceof CtTypeParameter) { + return false; + } + return true; + } + }; + + /** + * Accept all {@link CtClass}, {@link CtEnum} + */ + private static final Filter> classFilter = new TypeFilter>(CtClass.class); +} diff --git a/src/test/java/spoon/test/filters/FilterTest.java b/src/test/java/spoon/test/filters/FilterTest.java index 544e984d12a..aa20ccd26ed 100644 --- a/src/test/java/spoon/test/filters/FilterTest.java +++ b/src/test/java/spoon/test/filters/FilterTest.java @@ -39,6 +39,7 @@ import spoon.reflect.declaration.CtNamedElement; import spoon.reflect.declaration.CtPackage; import spoon.reflect.declaration.CtType; +import spoon.reflect.declaration.CtTypeInformation; import spoon.reflect.declaration.CtVariable; import spoon.reflect.declaration.ModifierKind; import spoon.reflect.factory.Factory; @@ -72,6 +73,7 @@ import spoon.reflect.visitor.filter.TypeFilter; import spoon.support.comparator.DeepRepresentationComparator; import spoon.support.reflect.declaration.CtMethodImpl; +import spoon.support.visitor.SubInheritanceHierarchyResolver; import spoon.test.filters.testclasses.AbstractTostada; import spoon.test.filters.testclasses.Antojito; import spoon.test.filters.testclasses.FieldAccessFilterTacos; @@ -1045,4 +1047,61 @@ public void exit(CtElement element) { //contract: if enter is called and does not returns SKIP_ALL, then exit must be called too. Exceptions are ignored for now assertEquals(context2.nrOfEnterRetTrue, context2.nrOfExit); } + + @Test + public void testSubInheritanceHierarchyResolver() throws Exception { + // contract; SubInheritanceHierarchyResolver supports finding subtypes in an incremental manner + final Launcher launcher = new Launcher(); + launcher.setArgs(new String[] {"--output-type", "nooutput","--level","info" }); + launcher.addInputResource("./src/test/java/spoon/test/filters/testclasses"); + launcher.buildModel(); + + SubInheritanceHierarchyResolver resolver = new SubInheritanceHierarchyResolver(launcher.getModel().getRootPackage()); + + // contract: by default, nothing is sent to the consumer + resolver.forEachSubTypeInPackage(new CtConsumer() { + @Override + public void accept(CtTypeInformation ctTypeInformation) { + fail(); + } + }); + + // we add a type + resolver.addSuperType(launcher.getFactory().Type().createReference(AbstractTostada.class)); + class Counter { int counter =0;} + Counter c = new Counter(); + resolver.forEachSubTypeInPackage(new CtConsumer() { + @Override + public void accept(CtTypeInformation ctTypeInformation) { + c.counter++; + } + }); + + // there are 5 subtypes of AbstractTostada + assertEquals(5, c.counter); + + // we add a type already visited + resolver.addSuperType(launcher.getFactory().Type().createReference(Tostada.class)); + // nothing is sent to the consumer + resolver.forEachSubTypeInPackage(new CtConsumer() { + @Override + public void accept(CtTypeInformation ctTypeInformation) { + fail(); + } + }); + + // we add a new type + resolver.addSuperType(launcher.getFactory().Type().createReference(ITostada.class)); + Counter c2 = new Counter(); + resolver.forEachSubTypeInPackage(new CtConsumer() { + @Override + public void accept(CtTypeInformation ctTypeInformation) { + c2.counter++; + assertEquals("spoon.test.filters.testclasses.Tacos", ctTypeInformation.getQualifiedName()); + } + }); + + // only one subtype remains unvisited + assertEquals(1, c2.counter); + } }