From 54723694197a0fceb480550e6a11fe02813dd3e7 Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Thu, 9 Mar 2017 21:44:21 +0100 Subject: [PATCH] feature: introduces CtScannerFunction as basic infrastructure for advanced functions and queries (#1180) --- .../spoon/reflect/visitor/chain/CtQuery.java | 12 + .../reflect/visitor/chain/CtQueryAware.java | 33 ++ .../reflect/visitor/chain/CtQueryImpl.java | 368 ++++++------------ .../visitor/filter/CtScannerFunction.java | 102 +++++ .../java/spoon/test/filters/FilterTest.java | 87 +++++ 5 files changed, 351 insertions(+), 251 deletions(-) create mode 100644 src/main/java/spoon/reflect/visitor/chain/CtQueryAware.java create mode 100644 src/main/java/spoon/reflect/visitor/filter/CtScannerFunction.java diff --git a/src/main/java/spoon/reflect/visitor/chain/CtQuery.java b/src/main/java/spoon/reflect/visitor/chain/CtQuery.java index 0155808a251..a06d8421829 100644 --- a/src/main/java/spoon/reflect/visitor/chain/CtQuery.java +++ b/src/main/java/spoon/reflect/visitor/chain/CtQuery.java @@ -166,4 +166,16 @@ public interface CtQuery extends CtQueryable { */ @Override CtQuery map(CtConsumableFunction queryStep); + + /** + * Terminates the evaluation of this query. + * The query still returns all results collected before termination. + * This method should not throw an exception. + */ + void terminate(); + + /** + * @return true if the evaluation has been terminated. + */ + boolean isTerminated(); } diff --git a/src/main/java/spoon/reflect/visitor/chain/CtQueryAware.java b/src/main/java/spoon/reflect/visitor/chain/CtQueryAware.java new file mode 100644 index 00000000000..191ed4d7db6 --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/chain/CtQueryAware.java @@ -0,0 +1,33 @@ +/** + * 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.reflect.visitor.chain; + +/** + * Expert-only capability interface so as to write advanced {@link CtFunction} and {@link spoon.reflect.visitor.Filter} + * that need to access the state of the top-level {@link CtQuery} instance + * containing the function to be evaluated. + * + * Not meant to be implemented directly, only in conjunction with + * {@link CtConsumableFunction}, {@link CtFunction} or {@link spoon.reflect.visitor.Filter}. + */ +public interface CtQueryAware { + /** + * This method is called when the filter/function is added as a step to a {@link CtQuery} by the query engine ({@link CtQueryImpl}). + * @param query an instance registering this function/filter. + */ + void setQuery(CtQuery query); +} diff --git a/src/main/java/spoon/reflect/visitor/chain/CtQueryImpl.java b/src/main/java/spoon/reflect/visitor/chain/CtQueryImpl.java index 82e8e223152..d91ca528251 100644 --- a/src/main/java/spoon/reflect/visitor/chain/CtQueryImpl.java +++ b/src/main/java/spoon/reflect/visitor/chain/CtQueryImpl.java @@ -24,8 +24,8 @@ import spoon.Launcher; import spoon.SpoonException; import spoon.reflect.declaration.CtElement; -import spoon.reflect.visitor.EarlyTerminatingScanner; import spoon.reflect.visitor.Filter; +import spoon.reflect.visitor.filter.CtScannerFunction; /** * The facade of {@link CtQuery} which represents a query bound to the {@link CtElement}, @@ -39,6 +39,12 @@ public class CtQueryImpl implements CtQuery { */ private List inputs; + private OutputFunctionWrapper outputStep = new OutputFunctionWrapper(); + private AbstractStep lastStep = outputStep; + private AbstractStep firstStep = lastStep; + + private boolean terminated = false; + public CtQueryImpl(Object... input) { setInput(input); } @@ -76,65 +82,11 @@ public CtQueryImpl addInput(Object... input) { return this; } - /** - * The evaluation context of the CtQuery. Can be used to bind the query the the output {@link CtConsumer} - * using {@link CtQueryContext#outputConsumer(CtConsumer)} and then - *
    - *
  • to evaluate the query on provided input using {@link CtQueryContext#accept(Object)} - *
  • to terminate the query evaluation at any phase of query execution using {@link CtQueryContext#terminate()} - *
  • to check if query is terminated at any phase of query execution using {@link CtQueryContext#isTerminated()} - * and to stop an expensive query evaluating process - *
- */ - private interface CtQueryContext extends CtConsumer { - /** - * @return the {@link CtConsumer} used to deliver results of the query evaluation - */ - CtConsumer getOutputConsumer(); - /** - * @param outputConsumer the {@link CtConsumer} used to deliver results of the query evaluation - * @return this to support fluent API - */ - CtQueryContext outputConsumer(CtConsumer outputConsumer); - - /** - * terminates current query evaluation. - * This method returns normally. It does not throw exception. - * But it causes that query evaluation engine terminates - * and returns all the till now collected results. - */ - void terminate(); - /** - * @return true if evaluation has to be/was terminated - */ - boolean isTerminated(); - } - - /** - * Creates CtQueryContext, which can be used to evaluate or terminate the query. - * Usage:
- *
-	 * {@code
-	 * CtQueryContext cc = factory.createQuery().map(...).createQueryContext();
-	 * cc.setOutputConsumer(e->{
-	 *   //... process returned elements
-	 *   //... or optionally terminate the query by
-	 *   cc.terminate();
-	 * });
-	 * //evaluate the query with `input`. The results will be delivered to `resultConsumer`
-	 * cc.accept(input);
-	 * }
-	 * 
- * @return new instance of CtQueryContext of this query - */ - private CtQueryContext createQueryContext() { - return new CurrentStep(); - } - + @Override public void forEach(CtConsumer consumer) { - CtQueryContext cc = createQueryContext().outputConsumer(consumer); + outputStep.setNext(consumer); for (Object input : inputs) { - cc.accept(input); + firstStep.accept(input); } } @@ -166,44 +118,46 @@ public R first() { @SuppressWarnings("unchecked") @Override public R first(final Class itemClass) { - final CtQueryContext cc = createQueryContext(); final Object[] result = new Object[1]; - cc.outputConsumer(new CtConsumer() { + outputStep.setNext(new CtConsumer() { @Override public void accept(R out) { if (out != null && itemClass.isAssignableFrom(out.getClass())) { result[0] = out; - cc.terminate(); + terminate(); } } }); for (Object input : inputs) { - cc.accept(input); + firstStep.accept(input); + if (isTerminated()) { + break; + } } return (R) result[0]; } - private List steps = new ArrayList<>(); - private boolean logging = false; private QueryFailurePolicy failurePolicy = QueryFailurePolicy.FAIL; @Override public CtQueryImpl map(CtConsumableFunction code) { - steps.add(new LazyFunctionWrapper(code)); + addStep(new LazyFunctionWrapper(code)); return this; } @Override public CtQueryImpl map(CtFunction function) { - steps.add(new FunctionWrapper(function)); + addStep(new FunctionWrapper(function)); return this; } @Override public CtQueryImpl filterChildren(Filter filter) { - map(new ChildrenFilteringFunction(filter)); - stepFailurePolicy(QueryFailurePolicy.IGNORE); + map(new CtScannerFunction()); + if (filter != null) { + select(filter); + } return this; } @@ -219,6 +173,15 @@ public Boolean apply(R input) { return this; } + @Override + public boolean isTerminated() { + return terminated; + } + @Override + public void terminate() { + terminated = true; + } + /** * Evaluates this query, ignoring bound input - if any * @@ -226,12 +189,13 @@ public Boolean apply(R input) { * @param outputConsumer method accept of the outputConsumer is called for each element produced by last mapping function of this query */ public void evaluate(I input, CtConsumer outputConsumer) { - createQueryContext().outputConsumer(outputConsumer).accept(input); + outputStep.setNext(outputConsumer); + firstStep.accept(input); } @Override public CtQueryImpl name(String name) { - getLastStep().setName(name); + lastStep.setName(name); return this; } @@ -242,7 +206,7 @@ public CtQueryImpl failurePolicy(QueryFailurePolicy policy) { } public CtQueryImpl stepFailurePolicy(QueryFailurePolicy policy) { - getLastStep().setLocalFailurePolicy(policy); + lastStep.setLocalFailurePolicy(policy); return this; } /** @@ -257,18 +221,33 @@ public CtQueryImpl logging(boolean logging) { return this; } - private AbstractStep getStep(int stepIdx) { - if (stepIdx >= steps.size()) { - return null; + protected void handleListenerSetQuery(Object target) { + if (target instanceof CtQueryAware) { + ((CtQueryAware) target).setQuery(this); } - return steps.get(stepIdx); } - private AbstractStep getLastStep() { - if (steps.isEmpty()) { - throw new SpoonException("There is no step in the query"); + private void addStep(AbstractStep step) { + step.nextStep = outputStep; + lastStep.nextStep = step; + lastStep = step; + if (firstStep == outputStep) { + firstStep = step; + } + step.setName(String.valueOf(getStepIndex(step) + 1)); + } + + private int getStepIndex(AbstractStep step) { + int idx = 0; + AbstractStep s = firstStep; + while (s != outputStep) { + if (s == step) { + return idx; + } + s = (AbstractStep) s.nextStep; + idx++; } - return getStep(steps.size() - 1); + return -1; } private boolean isLogging() { @@ -281,7 +260,7 @@ private boolean isLogging() { * @param e * @param parameters */ - private void onClassCastException(CurrentStep step, ClassCastException e, Object... parameters) { + private void onClassCastException(AbstractStep step, ClassCastException e, Object... parameters) { if (step.isFailOnCCE()) { throw new SpoonException(getStepDescription(step, e.getMessage(), parameters), e); } else if (Launcher.LOGGER.isTraceEnabled()) { @@ -291,13 +270,13 @@ private void onClassCastException(CurrentStep step, ClassCastException e, Object log(step, e.getMessage(), parameters); } - private void log(CurrentStep step, String message, Object... parameters) { + private void log(AbstractStep step, String message, Object... parameters) { if (isLogging() && Launcher.LOGGER.isInfoEnabled()) { Launcher.LOGGER.info(getStepDescription(step, message, parameters)); } } - private String getStepDescription(CurrentStep step, String message, Object... parameters) { + private String getStepDescription(AbstractStep step, String message, Object... parameters) { StringBuilder sb = new StringBuilder("Step "); sb.append(step.getName()).append(") "); sb.append(message); @@ -312,113 +291,13 @@ private String getStepDescription(CurrentStep step, String message, Object... pa return sb.toString(); } - /** - * The thread local implementation of CtConsumer, - * which knows index of actually processed step - * and handles response of current step and sends it to next step. - * - * This class plays a role of an orchestrator to move the step cursor forward, - * get the step, apply it and finally to call the output consumer. - */ - private class CurrentStep implements CtQueryContext { - private CtConsumer outputConsumer; - private int stepIdx = 0; - private boolean terminated = false; - - CurrentStep() { - } - - @Override - public CtConsumer getOutputConsumer() { - return outputConsumer; - } - - @Override - public CtQueryContext outputConsumer(CtConsumer outputConsumer) { - this.outputConsumer = outputConsumer; - return this; - } - - @SuppressWarnings("unchecked") - @Override - public void accept(Object input) { - if (input == null || isTerminated()) { - return; - } - stepIdx++; - try { - if (stepIdx <= steps.size()) { - //process next intermediate step - AbstractStep step = getStep(); - log(this, "received", input); - try { - step.apply(input, this); - } catch (ClassCastException e) { - onClassCastException(this, e, input); - } - } else { - //send element to outputConsumer, it means return one value of the query - log(this, "returning", input); - if (outputConsumer != null) { - try { - ((CtConsumer) outputConsumer).accept(input); - } catch (ClassCastException e) { - onClassCastException(this, e, input); - } - } - } - } finally { - stepIdx--; - } - } - - private String getName() { - AbstractStep stepFunction = getStep(); - if (stepFunction == null) { - return "outputConsumer"; - } - String name = stepFunction.getName(); - if (name == null) { - name = String.valueOf(stepIdx); - } - return name; - } - - private boolean isFailOnCCE() { - AbstractStep step = getStep(); - if (step == null) { - //it is final consumer. Never throw CCE on final forEach consumer - return false; - } - return step.isFailOnCCE(); - } - - private AbstractStep getStep() { - return CtQueryImpl.this.getStep(stepIdx - 1); - } - - @Override - public String toString() { - return "Step " + getName(); - } - - @Override - public void terminate() { - terminated = true; - } - - @Override - public boolean isTerminated() { - return terminated; - } - } - /** * Holds optional name and local QueryFailurePolicy of each step */ - private abstract class AbstractStep { + private abstract class AbstractStep implements CtConsumer { String name; QueryFailurePolicy localFailurePolicy = null; + CtConsumer nextStep; /** * @return name of this Step - for debugging purposes @@ -446,8 +325,40 @@ private boolean isFailOnCCE() { private void setLocalFailurePolicy(QueryFailurePolicy localFailurePolicy) { this.localFailurePolicy = localFailurePolicy; } + } + + /** + * Wrapper around terminal {@link CtConsumer}, which accepts output of this query + */ + private class OutputFunctionWrapper extends AbstractStep { + @Override + public void accept(Object element) { + if (element == null || isTerminated()) { + return; + } + try { + nextStep.accept(element); + } catch (ClassCastException e) { + if (Launcher.LOGGER.isTraceEnabled()) { + //log expected CCE ... there might be some unexpected too! + Launcher.LOGGER.trace(e); + } + } + } + @SuppressWarnings({ "unchecked", "rawtypes" }) + void setNext(CtConsumer out) { + //we are preparing new query execution. + reset(); + nextStep = (CtConsumer) out; + handleListenerSetQuery(nextStep); + } + } - abstract void apply(Object input, CurrentStep outputConsumer); + /** + * Called before query is evaluated again + */ + protected void reset() { + terminated = false; } private class LazyFunctionWrapper extends AbstractStep { @@ -457,14 +368,18 @@ private class LazyFunctionWrapper extends AbstractStep { LazyFunctionWrapper(CtConsumableFunction fnc) { super(); this.fnc = (CtConsumableFunction) fnc; + handleListenerSetQuery(this.fnc); } @Override - public void apply(Object input, CurrentStep outputConsumer) { + public void accept(Object input) { + if (input == null || isTerminated()) { + return; + } try { - fnc.apply(input, outputConsumer); + fnc.apply(input, nextStep); } catch (ClassCastException e) { - onClassCastException(outputConsumer, e, input); + onClassCastException(this, e, input); return; } } @@ -480,35 +395,39 @@ private class FunctionWrapper extends AbstractStep { FunctionWrapper(CtFunction code) { super(); fnc = (CtFunction) code; + handleListenerSetQuery(fnc); } @SuppressWarnings("unchecked") @Override - public void apply(Object input, CurrentStep outputConsumer) { + public void accept(Object input) { + if (input == null || isTerminated()) { + return; + } Object result; try { result = fnc.apply(input); } catch (ClassCastException e) { - onClassCastException(outputConsumer, e, input); + onClassCastException(this, e, input); return; } - if (result == null || outputConsumer.isTerminated()) { + if (result == null || isTerminated()) { return; } if (result instanceof Boolean) { //the code is a predicate. send the input to output if result is true if ((Boolean) result) { - outputConsumer.accept(input); + nextStep.accept(input); } else { - log(outputConsumer, "Skipped element, because CtFunction#accept(input) returned false", input); + log(this, "Skipped element, because CtFunction#accept(input) returned false", input); } return; } if (result instanceof Iterable) { //send each item of Iterable to the next step for (Object out : (Iterable) result) { - outputConsumer.accept(out); - if (outputConsumer.isTerminated()) { + nextStep.accept(out); + if (isTerminated()) { return; } } @@ -517,67 +436,14 @@ public void apply(Object input, CurrentStep outputConsumer) { if (result.getClass().isArray()) { //send each item of Array to the next step for (int i = 0; i < Array.getLength(result); i++) { - outputConsumer.accept(Array.get(result, i)); - if (outputConsumer.isTerminated()) { + nextStep.accept(Array.get(result, i)); + if (isTerminated()) { return; } } return; } - outputConsumer.accept(result); - } - } - - /** - * a step which scans all children of input element and only elements matching filter go to the next step - */ - private class ChildrenFilteringFunction extends EarlyTerminatingScanner implements CtConsumableFunction { - - protected CurrentStep next; - private Filter filter; - - @SuppressWarnings("unchecked") - ChildrenFilteringFunction(Filter filter) { - this.filter = (Filter) filter; - } - - @Override - public void apply(CtElement input, CtConsumer outputConsumer) { - next = (CurrentStep) (CtConsumer) outputConsumer; - scan(input); - } - @Override - public void scan(CtElement element) { - processFilter(element); - super.scan(element); - } - @Override - protected boolean isTerminated() { - return next.isTerminated(); - } - - private void processFilter(CtElement element) { - if (element == null || isTerminated()) { - return; - } - boolean matches = true; - if (filter != null) { - try { - matches = filter.matches(element); - } catch (ClassCastException e) { - onClassCastException(next, e, element); - return; - } - } - if (isTerminated()) { - return; - } - if (matches) { - //send input to output, because Fitler.matches returned true - next.accept(element); - } else { - log(next, "Skipped child element, because Filter#matches(input) returned false", element); - } + nextStep.accept(result); } } } diff --git a/src/main/java/spoon/reflect/visitor/filter/CtScannerFunction.java b/src/main/java/spoon/reflect/visitor/filter/CtScannerFunction.java new file mode 100644 index 00000000000..c07856bc778 --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/filter/CtScannerFunction.java @@ -0,0 +1,102 @@ +/** + * 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.reflect.visitor.filter; + +import spoon.reflect.declaration.CtElement; +import spoon.reflect.visitor.EarlyTerminatingScanner; +import spoon.reflect.visitor.chain.CtConsumableFunction; +import spoon.reflect.visitor.chain.CtConsumer; +import spoon.reflect.visitor.chain.CtQuery; +import spoon.reflect.visitor.chain.CtQueryAware; +import spoon.reflect.visitor.chain.CtScannerListener; +import spoon.reflect.visitor.chain.ScanningMode; + +/** + * Returns all children of an element. + * More than this, it is a parameterizable class to be subclassed which provides all the power of {@link spoon.reflect.visitor.CtScanner} in the context of queries. + *
+ * In particular, one can a register a {@link CtScannerListener}, it is called-back when entering/exiting each scanned AST node + * and it drives the scanning process (see {@link ScanningMode}). + */ +public class CtScannerFunction implements CtConsumableFunction, CtQueryAware { + + private final Scanner scanner; + private boolean includingSelf = true; + + public CtScannerFunction() { + scanner = new Scanner(); + } + + /** + * @param includingSelf if true then input element is sent to output too. By default it is false. + */ + public CtScannerFunction includingSelf(boolean includingSelf) { + this.includingSelf = includingSelf; + return this; + } + + /** + * @param listener the implementation of {@link CtScannerListener}, which will listen for enter/exit of nodes during scanning of AST + * @return this to support fluent API + */ + public CtScannerFunction setListener(CtScannerListener listener) { + scanner.setListener(listener); + return this; + } + + @Override + public void apply(CtElement input, CtConsumer outputConsumer) { + scanner.next = outputConsumer; + if (this.includingSelf) { + scanner.scan(input); + } else { + input.accept(scanner); + } + } + + /* + * it is called automatically by CtQuery implementation, + * when this mapping function is added. + */ + @Override + public void setQuery(CtQuery query) { + scanner.query = query; + } + + private static class Scanner extends EarlyTerminatingScanner { + protected CtConsumer next; + private CtQuery query; + + @Override + protected void doScan(CtElement element, ScanningMode mode) { + //send input to output + if (mode.visitElement) { + next.accept(element); + } + if (mode.visitChildren) { + element.accept(this); + } + } + /* + * override {@link EarlyTerminatingScanner#isTerminated()} and let it stop when query is terminated + */ + @Override + protected boolean isTerminated() { + return query.isTerminated(); + } + } +} diff --git a/src/test/java/spoon/test/filters/FilterTest.java b/src/test/java/spoon/test/filters/FilterTest.java index 2d131fb931f..b313f82265e 100644 --- a/src/test/java/spoon/test/filters/FilterTest.java +++ b/src/test/java/spoon/test/filters/FilterTest.java @@ -37,6 +37,7 @@ import spoon.reflect.declaration.CtInterface; import spoon.reflect.declaration.CtMethod; import spoon.reflect.declaration.CtNamedElement; +import spoon.reflect.declaration.CtPackage; import spoon.reflect.declaration.CtType; import spoon.reflect.declaration.CtVariable; import spoon.reflect.declaration.ModifierKind; @@ -48,12 +49,15 @@ import spoon.reflect.visitor.Query; import spoon.reflect.visitor.chain.CtConsumableFunction; import spoon.reflect.visitor.chain.CtQueryImpl; +import spoon.reflect.visitor.chain.CtScannerListener; import spoon.reflect.visitor.chain.QueryFailurePolicy; +import spoon.reflect.visitor.chain.ScanningMode; import spoon.reflect.visitor.chain.CtConsumer; import spoon.reflect.visitor.chain.CtQuery; import spoon.reflect.visitor.filter.AbstractFilter; import spoon.reflect.visitor.filter.AnnotationFilter; import spoon.reflect.visitor.filter.CompositeFilter; +import spoon.reflect.visitor.filter.CtScannerFunction; import spoon.reflect.visitor.filter.FieldAccessFilter; import spoon.reflect.visitor.filter.FilteringOperator; import spoon.reflect.visitor.filter.InvocationFilter; @@ -954,4 +958,87 @@ class Context { //contract: if includingSelf(true), then input element is first element assertSame(varStrings, varStrings.map(new ParentFunction().includingSelf(true)).first()); } + @Test + public void testCtScannerListener() throws Exception { + // contract: CtScannerFunction can be subclassed and configured by a CtScannerListener + + final Launcher launcher = new Launcher(); + launcher.setArgs(new String[] {"--output-type", "nooutput","--level","info" }); + launcher.addInputResource("./src/test/java/spoon/test/filters/testclasses"); + launcher.run(); + + class Context { + long nrOfEnter = 0; + long nrOfEnterRetTrue = 0; + long nrOfExit = 0; + long nrOfResults = 0; + } + + Context context1 = new Context(); + + // scan only packages until top level classes. Do not scan class internals + List result1 = launcher.getFactory().getModel().getRootPackage().map(new CtScannerFunction().setListener(new CtScannerListener() { + @Override + public ScanningMode enter(CtElement element) { + context1.nrOfEnter++; + if (element instanceof CtType) { + return ScanningMode.SKIP_CHILDREN; + } + return ScanningMode.NORMAL; + } + @Override + public void exit(CtElement element) { + context1.nrOfExit++; + } + + })).list(); + + //check that test is visiting some nodes + assertTrue(context1.nrOfEnter>0); + assertTrue(result1.size()>0); + //contract: if enter is called and returns SKIP_CHILDREN or NORMAL, then exit must be called too. Exceptions are ignored for now + assertEquals(context1.nrOfEnter, context1.nrOfExit); + + Context context2 = new Context(); + + Iterator iter = result1.iterator(); + + //scan only from packages till top level classes. Do not scan class internals + launcher.getFactory().getModel().getRootPackage().map(new CtScannerFunction().setListener(new CtScannerListener() { + int inClass = 0; + @Override + public ScanningMode enter(CtElement element) { + context2.nrOfEnter++; + if(inClass>0) { + //we are in class. skip this node and all children + return ScanningMode.SKIP_ALL; + } + if (element instanceof CtType) { + inClass++; + } + context2.nrOfEnterRetTrue++; + return ScanningMode.NORMAL; + } + @Override + public void exit(CtElement element) { + context2.nrOfExit++; + if (element instanceof CtType) { + inClass--; + } + assertTrue(inClass==0 || inClass==1); + } + + })).forEach(ele->{ + context2.nrOfResults++; + assertTrue(ele instanceof CtPackage || ele instanceof CtType); + //check that first and second query returned same results + assertSame(ele, iter.next()); + }); + //check that test is visiting some nodes + assertTrue(context2.nrOfEnter>0); + assertTrue(context2.nrOfEnter>context2.nrOfEnterRetTrue); + assertEquals(result1.size(), context2.nrOfResults); + //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); + } }