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..a9f6af74161 --- /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 + * + *

+ */ +@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(factoryMethodName.orElseThrow(), state)) + .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 statements = factoryMethod.getBody().getStatements(); + + Stream 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 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 af4ce4d6d23..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,8 @@ 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; @@ -28,13 +28,9 @@ import com.google.errorprone.matchers.Matcher; import com.google.errorprone.matchers.Matchers; import com.google.errorprone.util.ASTHelpers; -import com.sun.source.tree.ImportTree; import com.sun.source.tree.MethodTree; -import com.sun.source.tree.Tree; import java.util.Optional; import javax.lang.model.element.Modifier; -import tech.picnic.errorprone.bugpatterns.util.MoreASTHelpers; -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: @@ -108,52 +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 (MoreASTHelpers.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()); - } - private static Optional tryCanonicalizeMethodName(MethodTree tree) { return Optional.of(ASTHelpers.getSymbol(tree).getQualifiedName().toString()) .filter(name -> name.startsWith(TEST_PREFIX)) 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 16138dcc640..02807ee2264 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