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 dcdf755d3b6..1fd8a17f705 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 76089514b7c..131da0ba38d 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));
+ }
+ }
}