diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/JUnitFactoryMethodDeclaration.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/JUnitFactoryMethodDeclaration.java
new file mode 100644
index 00000000000..d22f048e8d9
--- /dev/null
+++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/JUnitFactoryMethodDeclaration.java
@@ -0,0 +1,283 @@
+package tech.picnic.errorprone.bugpatterns;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
+import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
+import static com.google.errorprone.BugPattern.StandardTags.STYLE;
+import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE;
+import static com.google.errorprone.matchers.Matchers.allOf;
+import static com.google.errorprone.matchers.Matchers.annotations;
+import static com.google.errorprone.matchers.Matchers.anyOf;
+import static com.google.errorprone.matchers.Matchers.enclosingClass;
+import static com.google.errorprone.matchers.Matchers.hasModifier;
+import static com.google.errorprone.matchers.Matchers.isType;
+import static com.google.errorprone.matchers.Matchers.not;
+import static java.util.stream.Collectors.joining;
+import static javax.lang.model.element.Modifier.ABSTRACT;
+import static javax.lang.model.element.Modifier.FINAL;
+import static javax.lang.model.element.Modifier.PRIVATE;
+import static tech.picnic.errorprone.bugpatterns.util.ConflictDetection.findMethodRenameBlocker;
+import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
+import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.HAS_METHOD_SOURCE;
+import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.TEST_METHOD;
+
+import com.google.auto.service.AutoService;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
+import com.google.errorprone.fixes.SuggestedFix;
+import com.google.errorprone.fixes.SuggestedFixes;
+import com.google.errorprone.matchers.Description;
+import com.google.errorprone.matchers.Matcher;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.source.tree.AnnotationTree;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.MethodTree;
+import com.sun.source.tree.StatementTree;
+import com.sun.source.tree.Tree.Kind;
+import com.sun.source.tree.VariableTree;
+import com.sun.tools.javac.parser.Tokens.Comment;
+import com.sun.tools.javac.parser.Tokens.TokenKind;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+import tech.picnic.errorprone.bugpatterns.util.MoreASTHelpers;
+import tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers;
+
+/**
+ * A {@link BugChecker} that flags non-canonical JUnit factory method declarations.
+ *
+ *
At Picnic, we consider a JUnit factory method canonical if it
+ *
+ *
+ * - has the same name as the test method it provides test cases for, but with a `TestCases`
+ * suffix, and
+ *
- has a comment which connects the return statement to the names of the parameters in the
+ * corresponding test method.
+ *
+ */
+@AutoService(BugChecker.class)
+@BugPattern(
+ summary = "JUnit factory method declaration can likely be improved",
+ link = BUG_PATTERNS_BASE_URL + "JUnitFactoryMethodDeclaration",
+ linkType = CUSTOM,
+ severity = SUGGESTION,
+ tags = STYLE)
+public final class JUnitFactoryMethodDeclaration extends BugChecker implements MethodTreeMatcher {
+ private static final long serialVersionUID = 1L;
+
+ private static final Matcher HAS_UNMODIFIABLE_SIGNATURE =
+ anyOf(
+ annotations(AT_LEAST_ONE, isType("java.lang.Override")),
+ allOf(
+ not(hasModifier(FINAL)),
+ not(hasModifier(PRIVATE)),
+ enclosingClass(hasModifier(ABSTRACT))));
+
+ /** Instantiates a new {@link JUnitFactoryMethodDeclaration} instance. */
+ public JUnitFactoryMethodDeclaration() {}
+
+ @Override
+ public Description matchMethod(MethodTree tree, VisitorState state) {
+ if (!TEST_METHOD.matches(tree, state) || !HAS_METHOD_SOURCE.matches(tree, state)) {
+ return Description.NO_MATCH;
+ }
+
+ AnnotationTree methodSourceAnnotation =
+ ASTHelpers.getAnnotationWithSimpleName(
+ tree.getModifiers().getAnnotations(), "MethodSource");
+
+ if (methodSourceAnnotation == null) {
+ return Description.NO_MATCH;
+ }
+
+ Optional factoryMethodName =
+ MoreJUnitMatchers.extractSingleFactoryMethodName(methodSourceAnnotation);
+
+ if (factoryMethodName.isEmpty()) {
+ /* If a test has multiple factory methods, not all of them can be given the desired name. */
+ return Description.NO_MATCH;
+ }
+
+ ImmutableList factoryMethods =
+ Optional.ofNullable(state.findEnclosing(ClassTree.class))
+ .map(
+ enclosingClass ->
+ MoreASTHelpers.findMethods(enclosingClass, factoryMethodName.orElseThrow()))
+ .stream()
+ .flatMap(Collection::stream)
+ .filter(methodTree -> methodTree.getParameters().isEmpty())
+ .collect(toImmutableList());
+
+ if (factoryMethods.size() != 1) {
+ /* If we cannot reliably find the factory method, err on the side of not proposing any fixes. */
+ return Description.NO_MATCH;
+ }
+
+ ImmutableList fixes =
+ getSuggestedFixes(
+ tree, methodSourceAnnotation, Iterables.getOnlyElement(factoryMethods), state);
+
+ /* Even though we match on the test method, none of the fixes apply to it directly, so we report
+ the fixes separately using `VisitorState::reportMatch`. */
+ fixes.forEach(state::reportMatch);
+ return Description.NO_MATCH;
+ }
+
+ private ImmutableList getSuggestedFixes(
+ MethodTree tree,
+ AnnotationTree methodSourceAnnotation,
+ MethodTree factoryMethod,
+ VisitorState state) {
+ ImmutableList factoryMethodNameFixes =
+ getFactoryMethodNameFixes(tree, methodSourceAnnotation, factoryMethod, state);
+
+ ImmutableList commentFixes =
+ getReturnStatementCommentFixes(tree, factoryMethod, state);
+
+ return ImmutableList.builder()
+ .addAll(factoryMethodNameFixes)
+ .addAll(commentFixes)
+ .build();
+ }
+
+ private ImmutableList getFactoryMethodNameFixes(
+ MethodTree tree,
+ AnnotationTree methodSourceAnnotation,
+ MethodTree factoryMethod,
+ VisitorState state) {
+ String expectedFactoryMethodName = tree.getName() + "TestCases";
+
+ if (HAS_UNMODIFIABLE_SIGNATURE.matches(factoryMethod, state)
+ || factoryMethod.getName().toString().equals(expectedFactoryMethodName)) {
+ return ImmutableList.of();
+ }
+
+ Optional blocker = findMethodRenameBlocker(expectedFactoryMethodName, state);
+ if (blocker.isPresent()) {
+ reportMethodRenameBlocker(
+ factoryMethod, blocker.orElseThrow(), expectedFactoryMethodName, state);
+ return ImmutableList.of();
+ } else {
+ return ImmutableList.of(
+ buildDescription(methodSourceAnnotation)
+ .setMessage(
+ String.format(
+ "The test cases should be supplied by a method named `%s`",
+ expectedFactoryMethodName))
+ .addFix(
+ SuggestedFixes.updateAnnotationArgumentValues(
+ methodSourceAnnotation,
+ state,
+ "value",
+ ImmutableList.of("\"" + expectedFactoryMethodName + "\""))
+ .build())
+ .build(),
+ buildDescription(factoryMethod)
+ .setMessage(
+ String.format(
+ "The test cases should be supplied by a method named `%s`",
+ expectedFactoryMethodName))
+ .addFix(SuggestedFixes.renameMethod(factoryMethod, expectedFactoryMethodName, state))
+ .build());
+ }
+ }
+
+ private void reportMethodRenameBlocker(
+ MethodTree tree, String reason, String suggestedName, VisitorState state) {
+ state.reportMatch(
+ buildDescription(tree)
+ .setMessage(
+ String.format(
+ "The test cases should be supplied by a method named `%s` (but note that %s)",
+ suggestedName, reason))
+ .build());
+ }
+
+ private ImmutableList getReturnStatementCommentFixes(
+ MethodTree testMethod, MethodTree factoryMethod, VisitorState state) {
+ ImmutableList parameterNames =
+ testMethod.getParameters().stream()
+ .map(VariableTree::getName)
+ .map(Object::toString)
+ .collect(toImmutableList());
+
+ String expectedComment = parameterNames.stream().collect(joining(", ", "/* { ", " } */"));
+
+ List extends StatementTree> statements = factoryMethod.getBody().getStatements();
+
+ Stream extends StatementTree> returnStatementsNeedingComment =
+ Streams.mapWithIndex(statements.stream(), IndexedStatement::new)
+ .filter(indexedStatement -> indexedStatement.getStatement().getKind() == Kind.RETURN)
+ .filter(
+ indexedStatement ->
+ !hasExpectedComment(
+ factoryMethod,
+ expectedComment,
+ statements,
+ indexedStatement.getIndex(),
+ state))
+ .map(IndexedStatement::getStatement);
+
+ return returnStatementsNeedingComment
+ .map(
+ s ->
+ buildDescription(s)
+ .setMessage(
+ "The return statement should be prefixed by a comment giving the names of the test case parameters")
+ .addFix(SuggestedFix.prefixWith(s, expectedComment + "\n"))
+ .build())
+ .collect(toImmutableList());
+ }
+
+ private static boolean hasExpectedComment(
+ MethodTree factoryMethod,
+ String expectedComment,
+ List extends StatementTree> statements,
+ long statementIndex,
+ VisitorState state) {
+ int startPosition =
+ statementIndex > 0
+ ? state.getEndPosition(statements.get((int) statementIndex - 1))
+ : ASTHelpers.getStartPosition(factoryMethod);
+ int endPosition = state.getEndPosition(statements.get((int) statementIndex));
+
+ ImmutableList comments =
+ extractReturnStatementComments(startPosition, endPosition, state);
+
+ return comments.stream()
+ .map(Comment::getText)
+ .anyMatch(comment -> comment.equals(expectedComment));
+ }
+
+ private static ImmutableList extractReturnStatementComments(
+ int startPosition, int endPosition, VisitorState state) {
+ return state.getOffsetTokens(startPosition, endPosition).stream()
+ .filter(t -> t.kind() == TokenKind.RETURN)
+ .flatMap(errorProneToken -> errorProneToken.comments().stream())
+ .collect(toImmutableList());
+ }
+
+ private static final class IndexedStatement {
+ private final StatementTree statement;
+ private final long index;
+
+ private IndexedStatement(StatementTree statement, long index) {
+ this.statement = statement;
+ this.index = index;
+ }
+
+ public StatementTree getStatement() {
+ return statement;
+ }
+
+ public long getIndex() {
+ return index;
+ }
+ }
+}
diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/JUnitMethodDeclaration.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/JUnitMethodDeclaration.java
index 26373ba53ea..d2ff987564c 100644
--- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/JUnitMethodDeclaration.java
+++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/JUnitMethodDeclaration.java
@@ -11,8 +11,10 @@
import static com.google.errorprone.matchers.Matchers.hasModifier;
import static com.google.errorprone.matchers.Matchers.isType;
import static java.util.function.Predicate.not;
+import static tech.picnic.errorprone.bugpatterns.util.ConflictDetection.findMethodRenameBlocker;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
-import static tech.picnic.errorprone.bugpatterns.util.JavaKeywords.isReservedKeyword;
+import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.SETUP_OR_TEARDOWN_METHOD;
+import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.TEST_METHOD;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableSet;
@@ -25,19 +27,10 @@
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.Matchers;
-import com.google.errorprone.matchers.MultiMatcher;
-import com.google.errorprone.predicates.TypePredicate;
import com.google.errorprone.util.ASTHelpers;
-import com.sun.source.tree.AnnotationTree;
-import com.sun.source.tree.ClassTree;
-import com.sun.source.tree.ImportTree;
import com.sun.source.tree.MethodTree;
-import com.sun.source.tree.Tree;
-import com.sun.tools.javac.code.Symbol;
import java.util.Optional;
import javax.lang.model.element.Modifier;
-import javax.lang.model.element.Name;
-import tech.picnic.errorprone.bugpatterns.util.SourceCode;
/** A {@link BugChecker} that flags non-canonical JUnit method declarations. */
// XXX: Consider introducing a class-level check that enforces that test classes:
@@ -64,20 +57,6 @@ public final class JUnitMethodDeclaration extends BugChecker implements MethodTr
Matchers.not(hasModifier(Modifier.FINAL)),
Matchers.not(hasModifier(Modifier.PRIVATE)),
enclosingClass(hasModifier(Modifier.ABSTRACT))));
- private static final MultiMatcher TEST_METHOD =
- annotations(
- AT_LEAST_ONE,
- anyOf(
- isType("org.junit.jupiter.api.Test"),
- hasMetaAnnotation("org.junit.jupiter.api.TestTemplate")));
- private static final MultiMatcher SETUP_OR_TEARDOWN_METHOD =
- annotations(
- AT_LEAST_ONE,
- anyOf(
- isType("org.junit.jupiter.api.AfterAll"),
- isType("org.junit.jupiter.api.AfterEach"),
- isType("org.junit.jupiter.api.BeforeAll"),
- isType("org.junit.jupiter.api.BeforeEach")));
/** Instantiates a new {@link JUnitMethodDeclaration} instance. */
public JUnitMethodDeclaration() {}
@@ -125,61 +104,6 @@ private void reportMethodRenameBlocker(MethodTree tree, String reason, VisitorSt
.build());
}
- /**
- * If applicable, returns a human-readable argument against assigning the given name to an
- * existing method.
- *
- * This method implements imperfect heuristics. Things it currently does not consider include
- * the following:
- *
- *
- * - Whether the rename would merely introduce a method overload, rather than clashing with an
- * existing method declaration.
- *
- Whether the rename would cause a method in a superclass to be overridden.
- *
- Whether the rename would in fact clash with a static import. (It could be that a static
- * import of the same name is only referenced from lexical scopes in which the method under
- * consideration cannot be referenced directly.)
- *
- */
- private static Optional findMethodRenameBlocker(String methodName, VisitorState state) {
- if (isMethodInEnclosingClass(methodName, state)) {
- return Optional.of(
- String.format("a method named `%s` already exists in this class", methodName));
- }
-
- if (isSimpleNameStaticallyImported(methodName, state)) {
- return Optional.of(String.format("`%s` is already statically imported", methodName));
- }
-
- if (isReservedKeyword(methodName)) {
- return Optional.of(String.format("`%s` is a reserved keyword", methodName));
- }
-
- return Optional.empty();
- }
-
- private static boolean isMethodInEnclosingClass(String methodName, VisitorState state) {
- return state.findEnclosing(ClassTree.class).getMembers().stream()
- .filter(MethodTree.class::isInstance)
- .map(MethodTree.class::cast)
- .map(MethodTree::getName)
- .map(Name::toString)
- .anyMatch(methodName::equals);
- }
-
- private static boolean isSimpleNameStaticallyImported(String simpleName, VisitorState state) {
- return state.getPath().getCompilationUnit().getImports().stream()
- .filter(ImportTree::isStatic)
- .map(ImportTree::getQualifiedIdentifier)
- .map(tree -> getStaticImportSimpleName(tree, state))
- .anyMatch(simpleName::contentEquals);
- }
-
- private static CharSequence getStaticImportSimpleName(Tree tree, VisitorState state) {
- String source = SourceCode.treeToString(tree, state);
- return source.subSequence(source.lastIndexOf('.') + 1, source.length());
- }
-
private static Optional tryCanonicalizeMethodName(MethodTree tree) {
return Optional.of(ASTHelpers.getSymbol(tree).getQualifiedName().toString())
.filter(name -> name.startsWith(TEST_PREFIX))
@@ -188,18 +112,4 @@ private static Optional tryCanonicalizeMethodName(MethodTree tree) {
.map(name -> Character.toLowerCase(name.charAt(0)) + name.substring(1))
.filter(name -> !Character.isDigit(name.charAt(0)));
}
-
- // XXX: Move to a `MoreMatchers` utility class.
- private static Matcher hasMetaAnnotation(String annotationClassName) {
- TypePredicate typePredicate = hasAnnotation(annotationClassName);
- return (tree, state) -> {
- Symbol sym = ASTHelpers.getSymbol(tree);
- return sym != null && typePredicate.apply(sym.type, state);
- };
- }
-
- // XXX: Move to a `MoreTypePredicates` utility class.
- private static TypePredicate hasAnnotation(String annotationClassName) {
- return (type, state) -> ASTHelpers.hasAnnotation(type.tsym, annotationClassName, state);
- }
}
diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/util/ConflictDetection.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/util/ConflictDetection.java
new file mode 100644
index 00000000000..4947d444f8a
--- /dev/null
+++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/util/ConflictDetection.java
@@ -0,0 +1,67 @@
+package tech.picnic.errorprone.bugpatterns.util;
+
+import static tech.picnic.errorprone.bugpatterns.util.JavaKeywords.isReservedKeyword;
+import static tech.picnic.errorprone.bugpatterns.util.MoreASTHelpers.isMethodInEnclosingClass;
+
+import com.google.errorprone.VisitorState;
+import com.sun.source.tree.ImportTree;
+import com.sun.source.tree.Tree;
+import java.util.Optional;
+
+/**
+ * A set of helper methods for finding conflicts which would be caused by applying certain fixes.
+ */
+public final class ConflictDetection {
+ private ConflictDetection() {}
+
+ /**
+ * If applicable, returns a human-readable argument against assigning the given name to an
+ * existing method.
+ *
+ * This method implements imperfect heuristics. Things it currently does not consider include
+ * the following:
+ *
+ *
+ * - Whether the rename would merely introduce a method overload, rather than clashing with an
+ * existing method declaration.
+ *
- Whether the rename would cause a method in a superclass to be overridden.
+ *
- Whether the rename would in fact clash with a static import. (It could be that a static
+ * import of the same name is only referenced from lexical scopes in which the method under
+ * consideration cannot be referenced directly.)
+ *
+ *
+ * @param methodName The proposed name to assign.
+ * @param state The {@link VisitorState} to use for searching for blockers.
+ * @return A human-readable argument against assigning the proposed name to an existing method, or
+ * {@link Optional#empty()} if no blocker was found.
+ */
+ public static Optional findMethodRenameBlocker(String methodName, VisitorState state) {
+ if (isMethodInEnclosingClass(methodName, state)) {
+ return Optional.of(
+ String.format("a method named `%s` already exists in this class", methodName));
+ }
+
+ if (isSimpleNameStaticallyImported(methodName, state)) {
+ return Optional.of(String.format("`%s` is already statically imported", methodName));
+ }
+
+ if (isReservedKeyword(methodName)) {
+ return Optional.of(String.format("`%s` is a reserved keyword", methodName));
+ }
+
+ return Optional.empty();
+ }
+
+ private static boolean isSimpleNameStaticallyImported(String simpleName, VisitorState state) {
+ return state.getPath().getCompilationUnit().getImports().stream()
+ .filter(ImportTree::isStatic)
+ .map(ImportTree::getQualifiedIdentifier)
+ .map(tree -> getStaticImportSimpleName(tree, state))
+ .anyMatch(simpleName::contentEquals);
+ }
+
+ private static CharSequence getStaticImportSimpleName(Tree tree, VisitorState state) {
+ String source = SourceCode.treeToString(tree, state);
+ return source.subSequence(source.lastIndexOf('.') + 1, source.length());
+ }
+}
diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/JUnitFactoryMethodDeclarationTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/JUnitFactoryMethodDeclarationTest.java
new file mode 100644
index 00000000000..11733765212
--- /dev/null
+++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/JUnitFactoryMethodDeclarationTest.java
@@ -0,0 +1,280 @@
+package tech.picnic.errorprone.bugpatterns;
+
+import com.google.errorprone.BugCheckerRefactoringTestHelper;
+import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
+import com.google.errorprone.CompilationTestHelper;
+import org.junit.jupiter.api.Test;
+
+final class JUnitFactoryMethodDeclarationTest {
+ private final CompilationTestHelper compilationTestHelper =
+ CompilationTestHelper.newInstance(JUnitFactoryMethodDeclaration.class, getClass());
+ private final BugCheckerRefactoringTestHelper refactoringTestHelper =
+ BugCheckerRefactoringTestHelper.newInstance(JUnitFactoryMethodDeclaration.class, getClass());
+
+ @Test
+ void identification() {
+ compilationTestHelper
+ .addSourceLines(
+ "A.java",
+ "import static org.junit.jupiter.params.provider.Arguments.arguments;",
+ "",
+ "import java.util.List;",
+ "import java.util.stream.Stream;",
+ "import org.junit.jupiter.params.ParameterizedTest;",
+ "import org.junit.jupiter.params.provider.Arguments;",
+ "import org.junit.jupiter.params.provider.MethodSource;",
+ "",
+ "class A {",
+ " @ParameterizedTest",
+ " // BUG: Diagnostic contains: The test cases should be supplied by a method named",
+ " // `method1TestCases`",
+ " @MethodSource(\"testCasesForMethod1\")",
+ " void method1(int foo, boolean bar, String baz) {}",
+ "",
+ " // BUG: Diagnostic contains: The test cases should be supplied by a method named",
+ " // `method1TestCases`",
+ " private static Stream testCasesForMethod1() {",
+ " /* { foo, bar, baz } */",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method2TestCases\")",
+ " void method2(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method2TestCases() {",
+ " /* { foo, bar, baz } */",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " private static void method2TestCases(int i) {}",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method3TestCases\")",
+ " void method3(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method3TestCases() {",
+ " // BUG: Diagnostic contains: The return statement should be prefixed by a comment giving the",
+ " // names of the test case parameters",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method4TestCases\")",
+ " void method4(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method4TestCases() {",
+ " /* { foo, bar, baz } */",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"testCasesForMethod5\")",
+ " void method5(int foo, boolean bar, String baz) {}",
+ "",
+ " void method5TestCases() {}",
+ "",
+ " // BUG: Diagnostic contains: (but note that a method named `method5TestCases` already exists in",
+ " // this class)",
+ " private static Stream testCasesForMethod5() {",
+ " /* { foo, bar, baz } */",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method6TestCases\")",
+ " void method6(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method6TestCases() {",
+ " List arguments = List.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " /* { foo, bar, baz } */",
+ " return arguments.stream();",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method7TestCases\")",
+ " void method7(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method7TestCases() {",
+ " List arguments = List.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " // BUG: Diagnostic contains: The return statement should be prefixed by a comment giving the",
+ " // names of the test case parameters",
+ " return arguments.stream();",
+ " }",
+ "",
+ " private static Stream method8TestCases() {",
+ " /* { foo, bar, baz } */",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method8TestCases\")",
+ " void method8(int foo, boolean bar, String baz) {}",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ void replacement() {
+ refactoringTestHelper
+ .addInputLines(
+ "A.java",
+ "import static org.junit.jupiter.params.provider.Arguments.arguments;",
+ "",
+ "import java.util.List;",
+ "import java.util.stream.Stream;",
+ "import org.junit.jupiter.params.ParameterizedTest;",
+ "import org.junit.jupiter.params.provider.Arguments;",
+ "import org.junit.jupiter.params.provider.MethodSource;",
+ "",
+ "class A {",
+ " @ParameterizedTest",
+ " @MethodSource(\"testCasesForMethod1\")",
+ " void method1(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream testCasesForMethod1() {",
+ " /* { foo, bar, baz } */",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method2TestCases\")",
+ " void method2(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method2TestCases() {",
+ " /* { foo, bar, baz } */",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " private static void method2TestCases(int i) {}",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method3TestCases\")",
+ " void method3(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method3TestCases() {",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method4TestCases\")",
+ " void method4(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method4TestCases() {",
+ " /* { foo, bar, baz } */",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"testCasesForMethod5\")",
+ " void method5(int foo, boolean bar, String baz) {}",
+ "",
+ " void method5TestCases() {}",
+ "",
+ " private static Stream testCasesForMethod5() {",
+ " /* { foo, bar, baz } */",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method6TestCases\")",
+ " void method6(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method6TestCases() {",
+ " List arguments = List.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " /* { foo, bar, baz } */",
+ " return arguments.stream();",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method7TestCases\")",
+ " void method7(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method7TestCases() {",
+ " List arguments = List.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " return arguments.stream();",
+ " }",
+ "}")
+ .addOutputLines(
+ "A.java",
+ "import static org.junit.jupiter.params.provider.Arguments.arguments;",
+ "",
+ "import java.util.List;",
+ "import java.util.stream.Stream;",
+ "import org.junit.jupiter.params.ParameterizedTest;",
+ "import org.junit.jupiter.params.provider.Arguments;",
+ "import org.junit.jupiter.params.provider.MethodSource;",
+ "",
+ "class A {",
+ " @ParameterizedTest",
+ " @MethodSource(\"method1TestCases\")",
+ " void method1(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method1TestCases() {",
+ " /* { foo, bar, baz } */",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method2TestCases\")",
+ " void method2(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method2TestCases() {",
+ " /* { foo, bar, baz } */",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " private static void method2TestCases(int i) {}",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method3TestCases\")",
+ " void method3(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method3TestCases() {",
+ " /* { foo, bar, baz } */",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method4TestCases\")",
+ " void method4(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method4TestCases() {",
+ " /* { foo, bar, baz } */",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"testCasesForMethod5\")",
+ " void method5(int foo, boolean bar, String baz) {}",
+ "",
+ " void method5TestCases() {}",
+ "",
+ " private static Stream testCasesForMethod5() {",
+ " /* { foo, bar, baz } */",
+ " return Stream.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method6TestCases\")",
+ " void method6(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method6TestCases() {",
+ " List arguments = List.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " /* { foo, bar, baz } */",
+ " return arguments.stream();",
+ " }",
+ "",
+ " @ParameterizedTest",
+ " @MethodSource(\"method7TestCases\")",
+ " void method7(int foo, boolean bar, String baz) {}",
+ "",
+ " private static Stream method7TestCases() {",
+ " List arguments = List.of(arguments(1, true, \"A\"), arguments(2, false, \"B\"));",
+ " /* { foo, bar, baz } */",
+ " return arguments.stream();",
+ " }",
+ "}")
+ .doTest(TestMode.TEXT_MATCH);
+ }
+}
diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/refasterrules/RefasterRulesTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/refasterrules/RefasterRulesTest.java
index eeb1afdc5fb..e34302b3e87 100644
--- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/refasterrules/RefasterRulesTest.java
+++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/refasterrules/RefasterRulesTest.java
@@ -71,6 +71,7 @@ final class RefasterRulesTest {
private static Stream validateRuleCollectionTestCases() {
// XXX: Drop the filter once we have added tests for AssertJ! We can then also replace this
// method with `@ValueSource(classes = {...})`.
+ /* { clazz } */
return RULE_COLLECTIONS.stream()
.filter(not(AssertJRules.class::equals))
.map(Arguments::arguments);
diff --git a/pom.xml b/pom.xml
index 8d949091db8..c34e75606b9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -878,6 +878,7 @@
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
+ --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
-Xmaxerrs
@@ -1023,6 +1024,7 @@
--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
+ --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED