From 6e6f8d9f7b65b34aa94bfbdcebbc314ec2d1dd24 Mon Sep 17 00:00:00 2001 From: Stephan Schroevers Date: Fri, 7 Apr 2023 15:05:11 +0200 Subject: [PATCH] Introduce `SourceCode#unwrapMethodInvocation` utility method (#561) --- .../bugpatterns/util/SourceCode.java | 63 ++++++++++ .../bugpatterns/util/SourceCodeTest.java | 111 +++++++++++++++++- 2 files changed, 168 insertions(+), 6 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/util/SourceCode.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/util/SourceCode.java index dcdf755d3b..1fd8a17f70 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/util/SourceCode.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/util/SourceCode.java @@ -1,12 +1,22 @@ package tech.picnic.errorprone.bugpatterns.util; +import static com.sun.tools.javac.parser.Tokens.TokenKind.RPAREN; import static com.sun.tools.javac.util.Position.NOPOS; +import static java.util.stream.Collectors.joining; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CharMatcher; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Streams; import com.google.errorprone.VisitorState; import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.util.ErrorProneToken; +import com.google.errorprone.util.ErrorProneTokens; +import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition; +import com.sun.tools.javac.util.Position; +import java.util.Optional; /** * A collection of Error Prone utility methods for dealing with the source code representation of @@ -59,4 +69,57 @@ public static SuggestedFix deleteWithTrailingWhitespace(Tree tree, VisitorState whitespaceEndPos == -1 ? sourceCode.length() : whitespaceEndPos, ""); } + + /** + * Creates a {@link SuggestedFix} for the replacement of the given {@link MethodInvocationTree} + * with just the arguments to the method invocation, effectively "unwrapping" the method + * invocation. + * + *

For example, given the method invocation {@code foo.bar(1, 2, 3)}, this method will return a + * {@link SuggestedFix} that replaces the method invocation with {@code 1, 2, 3}. + * + *

This method aims to preserve the original formatting of the method invocation, including + * whitespace and comments. + * + * @param tree The AST node to be unwrapped. + * @param state A {@link VisitorState} describing the context in which the given {@link + * MethodInvocationTree} is found. + * @return A non-{@code null} {@link SuggestedFix}. + */ + public static SuggestedFix unwrapMethodInvocation(MethodInvocationTree tree, VisitorState state) { + CharSequence sourceCode = state.getSourceCode(); + int startPosition = state.getEndPosition(tree.getMethodSelect()); + int endPosition = state.getEndPosition(tree); + + if (sourceCode == null || startPosition == Position.NOPOS || endPosition == Position.NOPOS) { + return unwrapMethodInvocationDroppingWhitespaceAndComments(tree, state); + } + + ImmutableList tokens = + ErrorProneTokens.getTokens( + sourceCode.subSequence(startPosition, endPosition).toString(), state.context); + + Optional leftParenPosition = + tokens.stream().findFirst().map(t -> startPosition + t.endPos()); + Optional rightParenPosition = + Streams.findLast(tokens.stream().filter(t -> t.kind() == RPAREN)) + .map(t -> startPosition + t.pos()); + if (leftParenPosition.isEmpty() || rightParenPosition.isEmpty()) { + return unwrapMethodInvocationDroppingWhitespaceAndComments(tree, state); + } + + return SuggestedFix.replace( + tree, + sourceCode + .subSequence(leftParenPosition.orElseThrow(), rightParenPosition.orElseThrow()) + .toString()); + } + + @VisibleForTesting + static SuggestedFix unwrapMethodInvocationDroppingWhitespaceAndComments( + MethodInvocationTree tree, VisitorState state) { + return SuggestedFix.replace( + tree, + tree.getArguments().stream().map(arg -> treeToString(arg, state)).collect(joining(", "))); + } } diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/util/SourceCodeTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/util/SourceCodeTest.java index 76089514b7..131da0ba38 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/util/SourceCodeTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/util/SourceCodeTest.java @@ -8,22 +8,22 @@ import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.AnnotationTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; import javax.lang.model.element.Name; import org.junit.jupiter.api.Test; final class SourceCodeTest { - private final BugCheckerRefactoringTestHelper refactoringTestHelper = - BugCheckerRefactoringTestHelper.newInstance(TestChecker.class, getClass()); - @Test void deleteWithTrailingWhitespaceAnnotations() { - refactoringTestHelper + BugCheckerRefactoringTestHelper.newInstance( + DeleteWithTrailingWhitespaceTestChecker.class, getClass()) .addInputLines("AnnotationToBeDeleted.java", "@interface AnnotationToBeDeleted {}") .expectUnchanged() .addInputLines( @@ -96,7 +96,8 @@ void deleteWithTrailingWhitespaceAnnotations() { @Test void deleteWithTrailingWhitespaceMethods() { - refactoringTestHelper + BugCheckerRefactoringTestHelper.newInstance( + DeleteWithTrailingWhitespaceTestChecker.class, getClass()) .addInputLines( "MethodDeletions.java", "interface MethodDeletions {", @@ -162,13 +163,78 @@ void deleteWithTrailingWhitespaceMethods() { .doTest(TestMode.TEXT_MATCH); } + @Test + void unwrapMethodInvocation() { + BugCheckerRefactoringTestHelper.newInstance(UnwrapMethodInvocationTestChecker.class, getClass()) + .addInputLines( + "A.java", + "import com.google.common.collect.ImmutableList;", + "", + "class A {", + " Object[] m() {", + " return new Object[][] {", + " {ImmutableList.of()},", + " {ImmutableList.of(1)},", + " {com.google.common.collect.ImmutableList.of(1, 2)},", + " {", + " 0, /*a*/", + " ImmutableList /*b*/./*c*/ /*d*/of /*e*/(/*f*/ 1 /*g*/, /*h*/ 2 /*i*/) /*j*/", + " }", + " };", + " }", + "}") + .addOutputLines( + "A.java", + "import com.google.common.collect.ImmutableList;", + "", + "class A {", + " Object[] m() {", + " return new Object[][] {{}, {1}, {1, 2}, {0, /*a*/ /*f*/ 1 /*g*/, /*h*/ 2 /*i*/ /*j*/}};", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + void unwrapMethodInvocationDroppingWhitespaceAndComments() { + BugCheckerRefactoringTestHelper.newInstance( + UnwrapMethodInvocationDroppingWhitespaceAndCommentsTestChecker.class, getClass()) + .addInputLines( + "A.java", + "import com.google.common.collect.ImmutableList;", + "", + "class A {", + " Object[] m() {", + " return new Object[][] {", + " {ImmutableList.of()},", + " {ImmutableList.of(1)},", + " {com.google.common.collect.ImmutableList.of(1, 2)},", + " {", + " 0, /*a*/", + " ImmutableList /*b*/./*c*/ /*d*/of /*e*/(/*f*/ 1 /*g*/, /*h*/ 2 /*i*/) /*j*/", + " }", + " };", + " }", + "}") + .addOutputLines( + "A.java", + "import com.google.common.collect.ImmutableList;", + "", + "class A {", + " Object[] m() {", + " return new Object[][] {{}, {1}, {1, 2}, {0, /*a*/ 1, 2 /*j*/}};", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + /** * A {@link BugChecker} that uses {@link SourceCode#deleteWithTrailingWhitespace(Tree, * VisitorState)} to suggest the deletion of annotations and methods with a name containing * {@value DELETION_MARKER}. */ @BugPattern(severity = ERROR, summary = "Interacts with `SourceCode` for testing purposes") - public static final class TestChecker extends BugChecker + public static final class DeleteWithTrailingWhitespaceTestChecker extends BugChecker implements AnnotationTreeMatcher, MethodTreeMatcher { private static final long serialVersionUID = 1L; private static final String DELETION_MARKER = "ToBeDeleted"; @@ -192,4 +258,37 @@ private Description match(Tree tree, Name name, VisitorState state) { : Description.NO_MATCH; } } + + /** + * A {@link BugChecker} that applies {@link + * SourceCode#unwrapMethodInvocation(MethodInvocationTree, VisitorState)} to all method + * invocations. + */ + @BugPattern(severity = ERROR, summary = "Interacts with `SourceCode` for testing purposes") + public static final class UnwrapMethodInvocationTestChecker extends BugChecker + implements MethodInvocationTreeMatcher { + private static final long serialVersionUID = 1L; + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + return describeMatch(tree, SourceCode.unwrapMethodInvocation(tree, state)); + } + } + + /** + * A {@link BugChecker} that applies {@link + * SourceCode#unwrapMethodInvocationDroppingWhitespaceAndComments(MethodInvocationTree, + * VisitorState)} to all method invocations. + */ + @BugPattern(severity = ERROR, summary = "Interacts with `SourceCode` for testing purposes") + public static final class UnwrapMethodInvocationDroppingWhitespaceAndCommentsTestChecker + extends BugChecker implements MethodInvocationTreeMatcher { + private static final long serialVersionUID = 1L; + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + return describeMatch( + tree, SourceCode.unwrapMethodInvocationDroppingWhitespaceAndComments(tree, state)); + } + } }