diff --git a/src/main/java/spoon/support/compiler/jdt/ContextBuilder.java b/src/main/java/spoon/support/compiler/jdt/ContextBuilder.java index b03d6ff23ab..f24fac4d94a 100644 --- a/src/main/java/spoon/support/compiler/jdt/ContextBuilder.java +++ b/src/main/java/spoon/support/compiler/jdt/ContextBuilder.java @@ -12,8 +12,6 @@ import org.eclipse.jdt.internal.compiler.ast.TypeReference; import org.eclipse.jdt.internal.compiler.lookup.FieldBinding; import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding; - -import spoon.Launcher; import spoon.compiler.Environment; import spoon.reflect.code.CtCatchVariable; import spoon.reflect.code.CtConstructorCall; @@ -47,8 +45,8 @@ import java.util.EnumSet; import java.util.List; -import static spoon.reflect.ModelElementContainerDefaultCapacities.CASTS_CONTAINER_DEFAULT_CAPACITY; import static java.lang.String.format; +import static spoon.reflect.ModelElementContainerDefaultCapacities.CASTS_CONTAINER_DEFAULT_CAPACITY; @Internal public class ContextBuilder { @@ -92,7 +90,6 @@ void enter(CtElement e, ASTNode node) { e.setPosition(this.jdtTreeBuilder.getPositionBuilder().buildPositionCtElement(e, node)); } catch (Exception ex) { e.setPosition(SourcePosition.NOPOSITION); - Launcher.LOGGER.warn("PositionBuilder failed for element " + e.toString(), ex); } } } diff --git a/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java b/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java index cd17f5b8c91..780cd47404a 100644 --- a/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java +++ b/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java @@ -13,6 +13,7 @@ import org.eclipse.jdt.internal.compiler.ast.AnnotationMethodDeclaration; import org.eclipse.jdt.internal.compiler.ast.Argument; import org.eclipse.jdt.internal.compiler.ast.ArrayTypeReference; +import org.eclipse.jdt.internal.compiler.ast.AssertStatement; import org.eclipse.jdt.internal.compiler.ast.CaseStatement; import org.eclipse.jdt.internal.compiler.ast.Expression; import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration; @@ -68,9 +69,10 @@ SourcePosition buildPosition(int sourceStart, int sourceEnd) { return this.jdtTreeBuilder.getFactory().Core().createSourcePosition(cu, sourceStart, sourceEnd, lineSeparatorPositions); } + /** creates a position for a given element with the information of ASTNode */ SourcePosition buildPositionCtElement(CtElement e, ASTNode node) { if (e instanceof CtCatch) { - //we cannot compute position of CtCatch, because we do not know position of it's body yet + //we cannot compute position of CtCatch, because we do not know position of its body yet //it is computed later by #buildPosition(CtCatch) return SourcePosition.NOPOSITION; } @@ -405,6 +407,9 @@ SourcePosition buildPositionCtElement(CtElement e, ASTNode node) { if (contents[sourceEnd] != ':') { return handlePositionProblem("Unexpected character " + contents[sourceEnd] + " instead of \':\' in CtCase on: " + sourceEnd); } + } else if ((node instanceof AssertStatement)) { + AssertStatement assert_ = (AssertStatement) node; + sourceEnd = findNextChar(contents, contents.length, sourceEnd, ';'); } if (e instanceof CtModifiable) { diff --git a/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java b/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java index 2f9aecf1b8a..c247f116ad6 100644 --- a/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java +++ b/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java @@ -7,7 +7,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import spoon.Launcher; import spoon.reflect.ModelElementContainerDefaultCapacities; import spoon.reflect.annotations.MetamodelPropertyField; import spoon.reflect.code.CtComment; @@ -165,9 +164,6 @@ public CtAnnotation getAnnotation(CtTypeReference a public List> getAnnotations() { if (this instanceof CtShadowable) { CtShadowable shadowable = (CtShadowable) this; - if (shadowable.isShadow()) { - Launcher.LOGGER.debug("Some annotations might be unreachable from the shadow element: " + this.getShortRepresentation()); - } } return unmodifiableList(annotations); } diff --git a/src/main/java/spoon/support/sniper/SniperJavaPrettyPrinter.java b/src/main/java/spoon/support/sniper/SniperJavaPrettyPrinter.java index 9dffa3e57cb..22654c3679f 100644 --- a/src/main/java/spoon/support/sniper/SniperJavaPrettyPrinter.java +++ b/src/main/java/spoon/support/sniper/SniperJavaPrettyPrinter.java @@ -149,37 +149,47 @@ private String detectLineSeparator(String text) { * @param printAction the executor of the action, we are listening for. Call it send token to output */ void onTokenWriterWrite(TokenType tokenType, String token, CtComment comment, Runnable printAction) { - onPrintEvent(new TokenPrinterEvent(tokenType, token, comment) { + executePrintEvent(new TokenPrinterEvent(tokenType, token, comment) { @Override - public void print(Boolean muted) { - runInMutedState(muted, printAction); + public void print(boolean muted) { + boolean originMuted = mutableTokenWriter.isMuted(); + try { + if (originMuted != muted) { + mutableTokenWriter.setMuted(muted); + } + printAction.run(); + } finally { + if (originMuted != muted) { + mutableTokenWriter.setMuted(originMuted); + } + } } @Override public void printSourceFragment(SourceFragment fragment, Boolean isModified) { boolean isCollectionStarted = false; if (fragment instanceof CollectionSourceFragment) { - CollectionSourceFragment csf = (CollectionSourceFragment) fragment; //we started scanning of collection of elements - SourceFragmentContext listContext = csf.isOrdered() - ? new SourceFragmentContextList(mutableTokenWriter, null, csf.getItems(), getChangeResolver()) - : new SourceFragmentContextSet(mutableTokenWriter, null, csf.getItems(), getChangeResolver()); + SourceFragmentContext listContext = getCollectionContext(null, (CollectionSourceFragment) fragment, isModified); //push the context of this collection - sourceFragmentContextStack.push(listContext); + pushContext(listContext); isCollectionStarted = true; } + if (isModified == null || isModified) { //print origin token printAction.run(); return; - } - mutableTokenWriter.getPrinterHelper().directPrint(fragment.getSourceCode()); - if (isCollectionStarted) { - mutableTokenWriter.setMuted(true); + } else { + mutableTokenWriter.getPrinterHelper().directPrint(fragment.getSourceCode()); } } }); } + private void pushContext(SourceFragmentContext listContext) { + listContext.onPush(); + sourceFragmentContextStack.push(listContext); + } private static boolean hasImplicitAncestor(CtElement el) { @@ -217,9 +227,9 @@ public String printElement(CtElement element) { element, Collections.singletonList(esf), new ChangeResolver(getChangeCollector(), element)), - () -> onPrintEvent(new ElementPrinterEvent(role, element) { + () -> executePrintEvent(new ElementPrinterEvent(role, element) { @Override - public void print(Boolean muted) { + public void print(boolean muted) { superScanInContext(element, SourceFragmentContextPrettyPrint.INSTANCE, muted); } @@ -244,9 +254,9 @@ public void printSourceFragment(SourceFragment fragment, Boolean isModified) { public SniperJavaPrettyPrinter scan(CtElement element) { if (element != null) { CtRole role = getRoleInCompilationUnit(element); - onPrintEvent(new ElementPrinterEvent(role, element) { + executePrintEvent(new ElementPrinterEvent(role, element) { @Override - public void print(Boolean muted) { + public void print(boolean muted) { superScanInContext(element, SourceFragmentContextPrettyPrint.INSTANCE, muted); } @Override @@ -269,7 +279,7 @@ private CtRole getRoleInCompilationUnit(CtElement element) { /** * Called whenever {@link DefaultJavaPrettyPrinter} scans/prints an element or writes a token */ - private void onPrintEvent(PrinterEvent event) { + private void executePrintEvent(PrinterEvent event) { SourceFragmentContext sfc = detectCurrentContext(event); if (sfc == null) { throw new SpoonException("Missing SourceFragmentContext"); @@ -277,7 +287,7 @@ private void onPrintEvent(PrinterEvent event) { //there is an context let it handle scanning if (mutableTokenWriter.isMuted()) { //it is already muted by an parent. Simply scan and ignore all tokens, - event.print(null); + event.print(true); return; } //let context handle the event @@ -295,8 +305,7 @@ private SourceFragmentContext detectCurrentContext(PrinterEvent event) { while ((sfc = sourceFragmentContextStack.peek()) != null && sfc.matchesPrinterEvent(event) == false) { //this context handles only subset of roles, which just finished //leave it and return back to parent context - sourceFragmentContextStack.pop(); - sfc.onFinished(); + sfc = popSourceFragmentContext(); } return sfc; } @@ -312,28 +321,26 @@ private void scanInternal(CtRole role, CtElement element, SourceFragment fragmen if (mutableTokenWriter.isMuted()) { throw new SpoonException("Unexpected state of sniper pretty printer. TokenWriter is muted."); } + + //it is not muted yet, so this element or any sibling is modified if (fragment == null) { throw new SpoonException("Missing source fragment. Call PrintEvent#print instead."); } //we have sources of fragment if (fragment instanceof CollectionSourceFragment) { - CollectionSourceFragment csf = (CollectionSourceFragment) fragment; //we started scanning of collection of elements - SourceFragmentContext listContext = csf.isOrdered() - ? new SourceFragmentContextList(mutableTokenWriter, element, csf.getItems(), getChangeResolver()) - : new SourceFragmentContextSet(mutableTokenWriter, element, csf.getItems(), getChangeResolver()); + SourceFragmentContext listContext = getCollectionContext(element, (CollectionSourceFragment) fragment, isFragmentModified); //push the context of this collection - sourceFragmentContextStack.push(listContext); + pushContext(listContext); + + //and scan first element of that collection again in new context of that collection if (Boolean.FALSE.equals(isFragmentModified)) { + // we print the original source code mutableTokenWriter.getPrinterHelper().directPrint(fragment.getSourceCode()); - //and mute the token writer and let DJPP scan it and ignore everything - mutableTokenWriter.setMuted(true); - //TODO check if DJPP needs this call somewhere (because of some state)... may be we can skip this scan completely?? - scan(element); - //and keep it muted until SourceFragmentContextList is finished } else { + // we print it normally scan(element); } } else if (fragment instanceof ElementSourceFragment) { @@ -364,6 +371,45 @@ private void scanInternal(CtRole role, CtElement element, SourceFragment fragmen } } + private SourceFragmentContext getCollectionContext(CtElement element, CollectionSourceFragment csf, boolean isModified) { + return csf.isOrdered() + ? new SourceFragmentContextList(mutableTokenWriter, element, csf.getItems(), getChangeResolver()) { + @Override + public void onPush() { + super.onPush(); + if (!isModified) { + mutableTokenWriter.setMuted(true); + } + } + + @Override + public void onFinished() { + super.onFinished(); + if (!isModified) { + mutableTokenWriter.setMuted(false); + } + } + + } + : new SourceFragmentContextSet(mutableTokenWriter, element, csf.getItems(), getChangeResolver()) { + @Override + public void onPush() { + super.onPush(); + if (!isModified) { + mutableTokenWriter.setMuted(true); + } + } + + @Override + public void onFinished() { + super.onFinished(); + if (!isModified) { + mutableTokenWriter.setMuted(false); + } + } + }; + } + /** * Call normal java printing in defined `context` * @param element to be printed element @@ -373,10 +419,19 @@ private void scanInternal(CtRole role, CtElement element, SourceFragment fragmen * false - not muted * null - same like before */ - private void superScanInContext(CtElement element, SourceFragmentContext context, Boolean muted) { - runInContext(context, - () -> runInMutedState(muted, - () -> super.scan(element))); + private void superScanInContext(CtElement element, SourceFragmentContext context, boolean muted) { + boolean originMuted = mutableTokenWriter.isMuted(); + try { + if (originMuted != muted) { + mutableTokenWriter.setMuted(muted); + } + runInContext(context, + () -> super.scan(element)); + } finally { + if (originMuted != muted) { + mutableTokenWriter.setMuted(originMuted); + } + } } /** @@ -385,47 +440,29 @@ private void superScanInContext(CtElement element, SourceFragmentContext context * @param code a to be processed {@link Runnable} */ private void runInContext(SourceFragmentContext context, Runnable code) { - sourceFragmentContextStack.push(context); + pushContext(context); try { code.run(); } finally { - //remove `context` and all it's child contexts + // we make sure to remve all contexts that have been pushed so far + // and we also remove parameter `context` + // so that we can leave the sourceFragmentContextStack clean while (true) { if (sourceFragmentContextStack.isEmpty()) { throw new SpoonException("Inconsistent sourceFragmentContextStack"); //NOSONAR } - SourceFragmentContext c = sourceFragmentContextStack.pop(); - c.onFinished(); + SourceFragmentContext c = popSourceFragmentContext(); if (c == context) { break; } } } } - /** - * Run code using {@link MutableTokenWriter} in defined state. - * After this function leaves, the muted status is restored. - * @param muted required muted status - * @param code to be processed {@link Runnable} - */ - private void runInMutedState(Boolean muted, Runnable code) { - boolean originMuted = mutableTokenWriter.isMuted(); - if (muted == null) { - muted = originMuted; - } - try { - mutableTokenWriter.setMuted(muted); - code.run(); - } finally { - //assure that muted status did not changed in between - if (mutableTokenWriter.isMuted() != muted) { - if (mutableTokenWriter.isMuted()) { - throw new SpoonException("Unexpected state: Token writer is muted after scanning"); //NOSONAR - } else { - throw new SpoonException("Unexpected state: Token writer is not muted after scanning"); //NOSONAR - } - } - mutableTokenWriter.setMuted(originMuted); - } + + /** makes the two atomic operations together pop+finish to maintain core contracts */ + private SourceFragmentContext popSourceFragmentContext() { + SourceFragmentContext c = sourceFragmentContextStack.pop(); + c.onFinished(); + return c; } } diff --git a/src/main/java/spoon/support/sniper/internal/AbstractSourceFragmentContext.java b/src/main/java/spoon/support/sniper/internal/AbstractSourceFragmentContext.java index 51f8bd3623d..579baccb6c6 100644 --- a/src/main/java/spoon/support/sniper/internal/AbstractSourceFragmentContext.java +++ b/src/main/java/spoon/support/sniper/internal/AbstractSourceFragmentContext.java @@ -50,7 +50,7 @@ public void onPrintEvent(PrinterEvent event) { //but may be it is not good idea //send all inc/dec tab to printer helper to have configured expected indentation - event.print(null); + event.print(false); return; } if (tpe.getType().isWhiteSpace()) { @@ -285,4 +285,8 @@ protected void printStandardSpaces() { } separatorActions.clear(); } + + @Override + public void onPush() { + } } diff --git a/src/main/java/spoon/support/sniper/internal/AbstractSourceFragmentContextCollection.java b/src/main/java/spoon/support/sniper/internal/AbstractSourceFragmentContextCollection.java index a0b3b314b68..eb4e79281b9 100644 --- a/src/main/java/spoon/support/sniper/internal/AbstractSourceFragmentContextCollection.java +++ b/src/main/java/spoon/support/sniper/internal/AbstractSourceFragmentContextCollection.java @@ -77,16 +77,14 @@ protected String getSuffixSpace() { @Override public void onFinished() { - //we are at the end of the list of elements. Printer just tries to print something out of this context. + // we are at the end of the list of elements. Printer just tries to print something out of this context. if (mutableTokenWriter.isMuted() == false) { //print list suffix String suffix = getSuffixSpace(); if (suffix != null) { //we have origin source code for that list suffix mutableTokenWriter.getPrinterHelper().directPrint(suffix); - separatorActions.clear(); } } - mutableTokenWriter.setMuted(false); } } diff --git a/src/main/java/spoon/support/sniper/internal/PrinterEvent.java b/src/main/java/spoon/support/sniper/internal/PrinterEvent.java index d9517e3944f..1574fc21c34 100644 --- a/src/main/java/spoon/support/sniper/internal/PrinterEvent.java +++ b/src/main/java/spoon/support/sniper/internal/PrinterEvent.java @@ -27,7 +27,7 @@ public interface PrinterEvent { * false if {@link DefaultJavaPrettyPrinter} will really print into output. * null if `muted` status should be kept as it is */ - void print(Boolean muted); + void print(boolean muted); /** * We have a source fragment of to be printed element. diff --git a/src/main/java/spoon/support/sniper/internal/SourceFragmentContext.java b/src/main/java/spoon/support/sniper/internal/SourceFragmentContext.java index d13524072f3..cafa8062bea 100644 --- a/src/main/java/spoon/support/sniper/internal/SourceFragmentContext.java +++ b/src/main/java/spoon/support/sniper/internal/SourceFragmentContext.java @@ -27,4 +27,9 @@ public interface SourceFragmentContext { * @return true if this context can handle `role` */ boolean matchesPrinterEvent(PrinterEvent event); + + /** + * called when pushed on the stack + */ + void onPush(); } diff --git a/src/main/java/spoon/support/sniper/internal/SourceFragmentContextPrettyPrint.java b/src/main/java/spoon/support/sniper/internal/SourceFragmentContextPrettyPrint.java index e0b426b7819..865c08f7b70 100644 --- a/src/main/java/spoon/support/sniper/internal/SourceFragmentContextPrettyPrint.java +++ b/src/main/java/spoon/support/sniper/internal/SourceFragmentContextPrettyPrint.java @@ -17,9 +17,13 @@ public class SourceFragmentContextPrettyPrint implements SourceFragmentContext { private SourceFragmentContextPrettyPrint() { } + @Override + public void onPush() { + } + @Override public void onPrintEvent(PrinterEvent event) { - event.print(null); + event.print(false); } @Override @@ -30,4 +34,5 @@ public void onFinished() { public boolean matchesPrinterEvent(PrinterEvent event) { return true; } + } diff --git a/src/test/java/spoon/test/prettyprinter/PrinterTest.java b/src/test/java/spoon/test/prettyprinter/PrinterTest.java index 84b0a4f30bd..8b142f8835e 100644 --- a/src/test/java/spoon/test/prettyprinter/PrinterTest.java +++ b/src/test/java/spoon/test/prettyprinter/PrinterTest.java @@ -405,6 +405,9 @@ public TokenWriter writeIdentifier(String identifier) { checkTokenWhitespace(identifier, false); for (int i = 0; i < identifier.length(); i++) { char c = identifier.charAt(i); + if ('*' == c) { + continue; + } if(i==0) { assertTrue(Character.isJavaIdentifierStart(c)); } else { diff --git a/src/test/java/spoon/test/prettyprinter/SniperAssertTest.java b/src/test/java/spoon/test/prettyprinter/SniperAssertTest.java new file mode 100644 index 00000000000..7ec43589ebc --- /dev/null +++ b/src/test/java/spoon/test/prettyprinter/SniperAssertTest.java @@ -0,0 +1,65 @@ +package spoon.test.prettyprinter; + +import org.apache.commons.io.*; +import org.hamcrest.*; +import org.junit.*; +import static org.junit.Assert.*; + +import spoon.Launcher; +import spoon.compiler.*; +import spoon.reflect.*; +import spoon.reflect.code.CtComment.*; +import spoon.reflect.declaration.*; +import spoon.reflect.visitor.filter.*; +import spoon.support.sniper.*; + +import java.io.*; +import java.nio.charset.*; +import java.nio.file.*; + +public class SniperAssertTest { + private static final Path INPUT_PATH = Paths.get("src/test/java/"); + private static final Path OUTPUT_PATH = Paths.get("target/test-output"); + + @BeforeClass + public static void setup() throws IOException { + FileUtils.deleteDirectory(OUTPUT_PATH.toFile()); + } + + @Test + public void assertNOK() throws IOException { + runSniperJavaPrettyPrinter("spoon/test/prettyprinter/testclasses/asserttest/AssertNOK.java"); + } + + @Test + public void assertOk() throws IOException { + runSniperJavaPrettyPrinter("spoon/test/prettyprinter/testclasses/asserttest/AssertOk.java"); + } + + private void runSniperJavaPrettyPrinter(String path) throws IOException { + final Launcher launcher = new Launcher(); + final Environment e = launcher.getEnvironment(); + e.setLevel("INFO"); + e.setPrettyPrinterCreator(() -> new SniperJavaPrettyPrinter(e)); + + launcher.addInputResource(INPUT_PATH.resolve(path).toString()); + launcher.setSourceOutputDirectory(OUTPUT_PATH.toString()); + + CtModel model = launcher.buildModel(); + CtMethod method = model.getElements(new TypeFilter<>(CtMethod.class)).get(0); + + method.getBody().insertBegin(launcher.getFactory().Code().createComment("test", CommentType.BLOCK)); + + launcher.process(); + launcher.prettyprint(); + // Verify result file exist and is not empty + assertThat("Output file for " + path + " should exist", OUTPUT_PATH.resolve(path).toFile().exists(), + CoreMatchers.equalTo(true)); + + String content = new String(Files.readAllBytes(OUTPUT_PATH.resolve(path)), StandardCharsets.UTF_8); + + assertThat(content, CoreMatchers.notNullValue()); + assertThat("Result class should not be empty", content.trim(), CoreMatchers.not(CoreMatchers.equalTo(""))); + assertThat("Should contain assert with semicolon", content.trim(), CoreMatchers.containsString("assert true;")); + } +} diff --git a/src/test/java/spoon/test/prettyprinter/TestSniperPrinter.java b/src/test/java/spoon/test/prettyprinter/TestSniperPrinter.java index 432f044eb62..ac4774f47e8 100644 --- a/src/test/java/spoon/test/prettyprinter/TestSniperPrinter.java +++ b/src/test/java/spoon/test/prettyprinter/TestSniperPrinter.java @@ -122,6 +122,17 @@ public void testPrintAfterRemoveOfFirstParameter() { }); } + @Test + public void testSimple() { + //contract: sniper print after remove of last statement + testSniper(spoon.test.prettyprinter.testclasses.Simple.class.getName(), type -> { + //delete first parameter of method `andSomeOtherMethod` + type.getMethodsByName("andSomeOtherMethod").get(0).getBody().getStatements().get(1).delete(); + }, (type, printed) -> { + assertIsPrintedWithExpectedChanges(type, printed, "\\s*System.out.println\\(\"bbb\"\\);", ""); + }); + } + @Test public void testPrintAfterRemoveOfMiddleParameter() { //contract: sniper print after remove of middle (not first and not last) parameter diff --git a/src/test/java/spoon/test/prettyprinter/testclasses/Simple.java b/src/test/java/spoon/test/prettyprinter/testclasses/Simple.java new file mode 100644 index 00000000000..4f0428f973c --- /dev/null +++ b/src/test/java/spoon/test/prettyprinter/testclasses/Simple.java @@ -0,0 +1,10 @@ +package spoon.test.prettyprinter.testclasses; + +public class Simple +{ + public void andSomeOtherMethod() + { + System.out.println("aaa"); + System.out.println("bbb"); + } +} diff --git a/src/test/java/spoon/test/prettyprinter/testclasses/asserttest/AssertNOK.java b/src/test/java/spoon/test/prettyprinter/testclasses/asserttest/AssertNOK.java new file mode 100644 index 00000000000..102567e5de2 --- /dev/null +++ b/src/test/java/spoon/test/prettyprinter/testclasses/asserttest/AssertNOK.java @@ -0,0 +1,7 @@ +package spoon.test.prettyprinter.testclasses.asserttest; + +public class AssertNOK { + private void test() { + assert true; + } +} diff --git a/src/test/java/spoon/test/prettyprinter/testclasses/asserttest/AssertOk.java b/src/test/java/spoon/test/prettyprinter/testclasses/asserttest/AssertOk.java new file mode 100644 index 00000000000..905bd309e9c --- /dev/null +++ b/src/test/java/spoon/test/prettyprinter/testclasses/asserttest/AssertOk.java @@ -0,0 +1,10 @@ +package spoon.test.prettyprinter.testclasses.asserttest; + +public class AssertOk { + private void test1() { + }; + + private void test2() { + assert true; + } +}