Skip to content

Commit

Permalink
fix: fix snippet compilation bugs (#3897)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewbwogi authored May 20, 2021
1 parent dcc14d9 commit 89bb302
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 10 deletions.
68 changes: 64 additions & 4 deletions src/main/java/spoon/support/compiler/SnippetCompilationHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@
import spoon.reflect.reference.CtTypeReference;
import spoon.reflect.visitor.filter.TypeFilter;
import spoon.support.compiler.jdt.JDTSnippetCompiler;
import spoon.support.compiler.jdt.PositionBuilder;
import spoon.support.reflect.declaration.CtElementImpl;

import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Arrays;

/** Helper class for working with snippets */
public class SnippetCompilationHelper {
Expand All @@ -54,11 +55,14 @@ private SnippetCompilationHelper() { }
*
*/
public static void compileAndReplaceSnippetsIn(CtType<?> initialClass) {

Map<CtPath, CtElement> elements2before = new HashMap<>();
Map<CtPath, CtElement> elements2after = new HashMap<>();
for (Object o : initialClass.filterChildren(new TypeFilter<>(CtCodeSnippet.class)).list()) {
CtElement el = (CtElement) o;
if (el instanceof CtCodeSnippetStatement && containsOnlyWhiteSpace(el)) {
replaceComments((CtStatement) el);
continue;
}
elements2before.put(el.getPath(), el);
}
Factory f = initialClass.getFactory();
Expand All @@ -75,9 +79,14 @@ public static void compileAndReplaceSnippetsIn(CtType<?> initialClass) {
// add dummy statements for each comment so paths are same for initial and new class
CtType<?> clonedInitialClass = initialClass.clone();
addDummyStatements(clonedInitialClass);
removeIllegalDummyStatements(clonedInitialClass);

String pkg = initialClass.getPackage().getQualifiedName();
if (!pkg.equals("")) {
pkg = "package " + pkg + ";";
}
try {
build(f, "package " + initialClass.getPackage().getQualifiedName() + ";" + clonedInitialClass.toString());
build(f, pkg + clonedInitialClass.toString());
} finally {
// restore modifiers
initialClass.setModifiers(backup);
Expand All @@ -101,16 +110,67 @@ public static void compileAndReplaceSnippetsIn(CtType<?> initialClass) {
}
}

private static boolean containsOnlyWhiteSpace(CtElement element) {
char[] snippet = (element.toString() + '\n').toCharArray();
int next = PositionBuilder.findNextNonWhitespace(snippet, snippet.length - 1, 0);
if (next == -1) {
return true;
} else {
return false;
}
}

private static void replaceComments(CtStatement element) {
replaceComments(element, (element.toString() + '\n').toCharArray());
element.delete();
}

private static void replaceComments(CtStatement element, char[] snippet) {
Factory factory = element.getFactory();
CtComment comment;
for (int i = 0; i < snippet.length; i++) {
if (Character.isWhitespace(snippet[i])) {
continue;
}
int end = PositionBuilder.getEndOfComment(snippet, snippet.length - 1, i);
if (snippet[i + 1] == '*') {
comment = factory.createComment(new String(Arrays.copyOfRange(snippet, i + 2, end - 1)), CtComment.CommentType.BLOCK);
} else {
comment = factory.createComment(new String(Arrays.copyOfRange(snippet, i + 2, end)), CtComment.CommentType.INLINE);
}
element.insertBefore(comment);
if (end + 1 < snippet.length) {
replaceComments(element, Arrays.copyOfRange(snippet, end + 1, snippet.length));
}
break;
}
}

private static void addDummyStatements(CtType<?> clonedInitialClass) {
Factory factory = clonedInitialClass.getFactory();
CtConstructorCall call = factory.createConstructorCall(factory.createCtTypeReference(Object.class));
List<CtComment> list = clonedInitialClass.filterChildren(new TypeFilter<>(CtComment.class)).list();
for (CtComment comment : list) {
CtConstructorCall call = factory.createConstructorCall(factory.createCtTypeReference(Object.class));
comment.insertBefore(call);
comment.delete();
}
}

private static void removeIllegalDummyStatements(CtType<?> clonedInitialClass) {
for (Object o : clonedInitialClass.filterChildren(new TypeFilter<>(CtReturn.class)).list()) {
CtStatement returnStmt = (CtStatement) o;
CtBlock block = (CtBlock) returnStmt.getParent();
for (int i = block.getStatements().size() - 1; i > 0; i--) {
CtStatement currentStatement = block.getStatement(i);
if (currentStatement == returnStmt) {
break;
} else {
currentStatement.delete();
}
}
}
}

public static CtStatement compileStatement(CtCodeSnippetStatement st)
throws SnippetCompilationError {
return internalCompileStatement(st, st.getFactory().Type().VOID_PRIMITIVE);
Expand Down
12 changes: 8 additions & 4 deletions src/main/java/spoon/support/compiler/jdt/PositionBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -662,13 +662,15 @@ static int findNextChar(char[] contents, int maxOff, int off, char expectedChar)
}

/**
* @param maxOff maximum acceptable return value
* @param content the character array on which the search will be performed.
* @param maxOff maximum acceptable return value.
* @param off the offset of {@code content} where the search begins.
* @return index of first non whitespace char, searching forward.
* Can return 'off' if it is non whitespace.
* Note: all kinds of java comments are understood as whitespace too.
* The search must start out of comment or on the first character of the comment
*/
static int findNextNonWhitespace(char[] content, int maxOff, int off) {
public static int findNextNonWhitespace(char[] content, int maxOff, int off) {
return findNextNonWhitespace(true, content, maxOff, off);
}

Expand Down Expand Up @@ -775,11 +777,13 @@ static int findPrevWhitespace(char[] content, int minOff, int off) {
return -1;
}
/**
* @param maxOff maximum acceptable return value
* @param content the character array on which the search will be performed.
* @param maxOff maximum acceptable return value.
* @param off the offset of {@code content} where the search begins.
* @return if the off points at start of comment then it returns offset which points on last character of the comment
* if the off does not point at start of comment then it returns -1
*/
static int getEndOfComment(char[] content, int maxOff, int off) {
public static int getEndOfComment(char[] content, int maxOff, int off) {
maxOff = Math.min(maxOff, content.length - 1);
if (off + 1 <= maxOff) {
if (content[off] == '/' && content[off + 1] == '*') {
Expand Down
46 changes: 44 additions & 2 deletions src/test/java/spoon/test/snippets/SnippetTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,7 @@ public void testCompileSnippetsWithCtComment() {
CtBlock innerBlock = body.getStatement(0);
innerBlock.addStatement(0,factory.createCodeSnippetStatement("invokedMethod()"));
body.addStatement(0,factory.createComment("block comment", CtComment.CommentType.BLOCK));

testClass.compileAndReplaceSnippets();

assertTrue(body.getStatements().get(0) instanceof CtComment);
assertTrue(body.getStatements().get(1) instanceof CtBlock);
assertTrue(body.getStatements().get(2) instanceof CtComment);
Expand All @@ -197,4 +195,48 @@ public void testCompileSnippetsWithCtComment() {
assertEquals(0,body.getStatements().get(2).getComments().size());
assertEquals(0,body.getStatements().get(3).getComments().size());
}

@Test
public void testCommentSnippetCompilation() {
// contract: a snippet with only comments should be replaced with corresponding CtComments
Launcher launcher = new Launcher();
Factory factory = launcher.getFactory();
launcher.addInputResource("src/test/resources/snippet/SnippetCommentResource.java");
launcher.buildModel();
CtClass<?> snippetClass = factory.Class().get("snippet.test.resources.SnippetCommentResource");
CtMethod method = snippetClass.getMethodsByName("methodForCommentOnlySnippet").get(0);
CtBlock body = method.getBody();
body.addStatement(1,factory.createCodeSnippetStatement("/* a \n block \n comment */\n// inline"));
body.addStatement(0,factory.createCodeSnippetStatement("/* a \n block \n comment */"));
body.addStatement(0,factory.createCodeSnippetStatement("int x"));
body.addStatement(0,factory.createCodeSnippetStatement(" // inline"));
snippetClass.compileAndReplaceSnippets();
assertTrue(body.getStatements().get(0) instanceof CtComment);
assertTrue(body.getStatements().get(1) instanceof CtLocalVariable);
assertTrue(body.getStatements().get(2) instanceof CtComment);
assertTrue(body.getStatements().get(3) instanceof CtReturn);
assertTrue(body.getStatements().get(4) instanceof CtComment);
assertTrue(body.getStatements().get(5) instanceof CtComment);
assertEquals(CtComment.CommentType.INLINE,((CtComment) body.getStatements().get(0)).getCommentType());
assertEquals(CtComment.CommentType.BLOCK,((CtComment) body.getStatements().get(2)).getCommentType());
assertEquals(CtComment.CommentType.BLOCK,((CtComment) body.getStatements().get(4)).getCommentType());
assertEquals(CtComment.CommentType.INLINE,((CtComment) body.getStatements().get(5)).getCommentType());
assertEquals(6,body.getStatements().size());
}

@Test
public void testSnippetCompilationInUnnamedPackage() {
// contract: a snippet can be successfully compiled in a class with an unnamed package
Launcher launcher = new Launcher();
Factory factory = launcher.getFactory();
launcher.addInputResource("src/test/resources/snippet/UnnamedPackageSnippetResource.java");
launcher.buildModel();
CtClass<?> snippetClass = factory.Class().get("UnnamedPackageSnippetResource");
CtMethod method = snippetClass.getMethodsByName("method").get(0);
CtBlock body = method.getBody();
body.addStatement(0,factory.createCodeSnippetStatement("int x"));
snippetClass.compileAndReplaceSnippets();
assertTrue(body.getStatements().get(0) instanceof CtLocalVariable);
assertEquals(1,body.getStatements().size());
}
}
4 changes: 4 additions & 0 deletions src/test/resources/snippet/SnippetCommentResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ public void modifiedMethod() {

void invokedMethod() {
}

public void methodForCommentOnlySnippet(){
return;
}
}
6 changes: 6 additions & 0 deletions src/test/resources/snippet/UnnamedPackageSnippetResource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
public class UnnamedPackageSnippetResource {

public void method() {

}
}

0 comments on commit 89bb302

Please sign in to comment.