From b3ca01a6c7187982cf104ce8ababcbcbd3697c0d Mon Sep 17 00:00:00 2001 From: Stephan Schroevers Date: Sat, 14 Sep 2024 12:02:37 +0200 Subject: [PATCH] Introduce `RefasterRuleTestExtractor` for documentation generation (#1317) This new `Extractor` implementation collects Refaster example input and output code from rule collection tests. This change also introduces explicit compilation steps for the test code. As a side-effect this produces faster feedback in case of invalid input or output code. --- documentation-support/pom.xml | 9 + .../BugPatternTestExtractor.java | 50 ++--- .../RefasterRuleCollectionTestExtractor.java | 176 ++++++++++++++++++ .../BugPatternTestExtractorTest.java | 44 ++--- ...fasterRuleCollectionTestExtractorTest.java | 166 +++++++++++++++++ error-prone-contrib/pom.xml | 49 +++++ .../ImmutableMultisetRulesTestInput.java | 1 + .../ImmutableMultisetRulesTestOutput.java | 1 + ...ImmutableSortedMultisetRulesTestInput.java | 1 + ...mmutableSortedMultisetRulesTestOutput.java | 1 + .../refaster/test/RefasterRuleCollection.java | 3 + 11 files changed, 455 insertions(+), 46 deletions(-) create mode 100644 documentation-support/src/main/java/tech/picnic/errorprone/documentation/RefasterRuleCollectionTestExtractor.java create mode 100644 documentation-support/src/test/java/tech/picnic/errorprone/documentation/RefasterRuleCollectionTestExtractorTest.java diff --git a/documentation-support/pom.xml b/documentation-support/pom.xml index 1ccf022a7f..e71a8ed1ca 100644 --- a/documentation-support/pom.xml +++ b/documentation-support/pom.xml @@ -33,6 +33,15 @@ error_prone_test_helpers test + + ${project.groupId} + error-prone-utils + + + ${project.groupId} + refaster-test-support + test + com.fasterxml.jackson.core jackson-annotations diff --git a/documentation-support/src/main/java/tech/picnic/errorprone/documentation/BugPatternTestExtractor.java b/documentation-support/src/main/java/tech/picnic/errorprone/documentation/BugPatternTestExtractor.java index 7ca51f683a..467a664ff9 100644 --- a/documentation-support/src/main/java/tech/picnic/errorprone/documentation/BugPatternTestExtractor.java +++ b/documentation-support/src/main/java/tech/picnic/errorprone/documentation/BugPatternTestExtractor.java @@ -27,7 +27,7 @@ import java.util.List; import java.util.Optional; import org.jspecify.annotations.Nullable; -import tech.picnic.errorprone.documentation.BugPatternTestExtractor.TestCases; +import tech.picnic.errorprone.documentation.BugPatternTestExtractor.BugPatternTestCases; /** * An {@link Extractor} that describes how to extract data from classes that test a {@code @@ -40,7 +40,7 @@ @Immutable @AutoService(Extractor.class) @SuppressWarnings("rawtypes" /* See https://github.com/google/auto/issues/870. */) -public final class BugPatternTestExtractor implements Extractor { +public final class BugPatternTestExtractor implements Extractor { /** Instantiates a new {@link BugPatternTestExtractor} instance. */ public BugPatternTestExtractor() {} @@ -50,7 +50,7 @@ public String identifier() { } @Override - public Optional tryExtract(ClassTree tree, VisitorState state) { + public Optional tryExtract(ClassTree tree, VisitorState state) { BugPatternTestCollector collector = new BugPatternTestCollector(); collector.scan(tree, state); @@ -59,7 +59,7 @@ public Optional tryExtract(ClassTree tree, VisitorState state) { .filter(not(ImmutableList::isEmpty)) .map( tests -> - new AutoValue_BugPatternTestExtractor_TestCases( + new AutoValue_BugPatternTestExtractor_BugPatternTestCases( state.getPath().getCompilationUnit().getSourceFile().toUri(), ASTHelpers.getSymbol(tree).className(), tests)); @@ -95,10 +95,10 @@ private static final class BugPatternTestCollector .onDescendantOf("com.google.errorprone.BugCheckerRefactoringTestHelper.ExpectOutput") .namedAnyOf("addOutputLines", "expectUnchanged"); - private final List collectedTestCases = new ArrayList<>(); + private final List collectedBugPatternTestCases = new ArrayList<>(); - private ImmutableList getCollectedTests() { - return ImmutableList.copyOf(collectedTestCases); + private ImmutableList getCollectedTests() { + return ImmutableList.copyOf(collectedBugPatternTestCases); } @Override @@ -110,14 +110,14 @@ private ImmutableList getCollectedTests() { classUnderTest -> { List entries = new ArrayList<>(); if (isReplacementTest) { - extractReplacementTestCases(node, entries, state); + extractReplacementBugPatternTestCases(node, entries, state); } else { - extractIdentificationTestCases(node, entries, state); + extractIdentificationBugPatternTestCases(node, entries, state); } if (!entries.isEmpty()) { - collectedTestCases.add( - new AutoValue_BugPatternTestExtractor_TestCase( + collectedBugPatternTestCases.add( + new AutoValue_BugPatternTestExtractor_BugPatternTestCase( classUnderTest, ImmutableList.copyOf(entries).reverse())); } }); @@ -140,7 +140,7 @@ private static Optional getClassUnderTest( : Optional.empty(); } - private static void extractIdentificationTestCases( + private static void extractIdentificationBugPatternTestCases( MethodInvocationTree tree, List sink, VisitorState state) { if (IDENTIFICATION_SOURCE_LINES.matches(tree, state)) { String path = ASTHelpers.constValue(tree.getArguments().get(0), String.class); @@ -155,11 +155,11 @@ private static void extractIdentificationTestCases( ExpressionTree receiver = ASTHelpers.getReceiver(tree); if (receiver instanceof MethodInvocationTree methodInvocation) { - extractIdentificationTestCases(methodInvocation, sink, state); + extractIdentificationBugPatternTestCases(methodInvocation, sink, state); } } - private static void extractReplacementTestCases( + private static void extractReplacementBugPatternTestCases( MethodInvocationTree tree, List sink, VisitorState state) { if (REPLACEMENT_OUTPUT_SOURCE_LINES.matches(tree, state)) { /* @@ -185,7 +185,7 @@ private static void extractReplacementTestCases( ExpressionTree receiver = ASTHelpers.getReceiver(tree); if (receiver instanceof MethodInvocationTree methodInvocation) { - extractReplacementTestCases(methodInvocation, sink, state); + extractReplacementBugPatternTestCases(methodInvocation, sink, state); } } @@ -208,24 +208,26 @@ private static Optional getSourceCode(MethodInvocationTree tree) { } @AutoValue - @JsonDeserialize(as = AutoValue_BugPatternTestExtractor_TestCases.class) - abstract static class TestCases { - static TestCases create(URI source, String testClass, ImmutableList testCases) { - return new AutoValue_BugPatternTestExtractor_TestCases(source, testClass, testCases); + @JsonDeserialize(as = AutoValue_BugPatternTestExtractor_BugPatternTestCases.class) + abstract static class BugPatternTestCases { + static BugPatternTestCases create( + URI source, String testClass, ImmutableList testCases) { + return new AutoValue_BugPatternTestExtractor_BugPatternTestCases( + source, testClass, testCases); } abstract URI source(); abstract String testClass(); - abstract ImmutableList testCases(); + abstract ImmutableList testCases(); } @AutoValue - @JsonDeserialize(as = AutoValue_BugPatternTestExtractor_TestCase.class) - abstract static class TestCase { - static TestCase create(String classUnderTest, ImmutableList entries) { - return new AutoValue_BugPatternTestExtractor_TestCase(classUnderTest, entries); + @JsonDeserialize(as = AutoValue_BugPatternTestExtractor_BugPatternTestCase.class) + abstract static class BugPatternTestCase { + static BugPatternTestCase create(String classUnderTest, ImmutableList entries) { + return new AutoValue_BugPatternTestExtractor_BugPatternTestCase(classUnderTest, entries); } abstract String classUnderTest(); diff --git a/documentation-support/src/main/java/tech/picnic/errorprone/documentation/RefasterRuleCollectionTestExtractor.java b/documentation-support/src/main/java/tech/picnic/errorprone/documentation/RefasterRuleCollectionTestExtractor.java new file mode 100644 index 0000000000..8094a35419 --- /dev/null +++ b/documentation-support/src/main/java/tech/picnic/errorprone/documentation/RefasterRuleCollectionTestExtractor.java @@ -0,0 +1,176 @@ +package tech.picnic.errorprone.documentation; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.errorprone.matchers.Matchers.isSubtypeOf; +import static java.util.stream.Collectors.joining; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.auto.service.AutoService; +import com.google.auto.value.AutoValue; +import com.google.common.base.Splitter; +import com.google.common.base.Supplier; +import com.google.common.base.VerifyException; +import com.google.common.collect.ImmutableList; +import com.google.errorprone.VisitorState; +import com.google.errorprone.annotations.FormatMethod; +import com.google.errorprone.annotations.Immutable; +import com.google.errorprone.matchers.Matcher; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.MethodTree; +import java.net.URI; +import java.util.Optional; +import java.util.regex.Pattern; +import tech.picnic.errorprone.documentation.RefasterRuleCollectionTestExtractor.RefasterTestCases; +import tech.picnic.errorprone.utils.SourceCode; + +/** + * An {@link Extractor} that describes how to extract data from Refaster rule input and output test + * classes. + */ +// XXX: Drop this extractor if/when the Refaster test framework is reimplemented such that tests can +// be located alongside rules, rather than in two additional resource files as currently required by +// `RefasterRuleCollection`. +@Immutable +@AutoService(Extractor.class) +@SuppressWarnings("rawtypes" /* See https://github.com/google/auto/issues/870. */) +public final class RefasterRuleCollectionTestExtractor implements Extractor { + private static final Matcher IS_REFASTER_RULE_COLLECTION_TEST_CASE = + isSubtypeOf("tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase"); + private static final Pattern TEST_CLASS_NAME_PATTERN = Pattern.compile("(.*)Test"); + private static final Pattern TEST_CLASS_FILE_NAME_PATTERN = + Pattern.compile(".*(Input|Output)\\.java"); + private static final Pattern TEST_METHOD_NAME_PATTERN = Pattern.compile("test(.*)"); + private static final String LINE_SEPARATOR = "\n"; + private static final Splitter LINE_SPLITTER = Splitter.on(LINE_SEPARATOR); + + /** Instantiates a new {@link RefasterRuleCollectionTestExtractor} instance. */ + public RefasterRuleCollectionTestExtractor() {} + + @Override + public String identifier() { + return "refaster-rule-collection-test"; + } + + @Override + public Optional tryExtract(ClassTree tree, VisitorState state) { + if (!IS_REFASTER_RULE_COLLECTION_TEST_CASE.matches(tree, state)) { + return Optional.empty(); + } + + URI sourceFile = state.getPath().getCompilationUnit().getSourceFile().toUri(); + return Optional.of( + RefasterTestCases.create( + sourceFile, + getRuleCollectionName(tree), + isInputFile(sourceFile), + getRefasterTestCases(tree, state))); + } + + private static String getRuleCollectionName(ClassTree tree) { + String className = tree.getSimpleName().toString(); + + // XXX: Instead of throwing an error here, it'd be nicer to have a bug checker validate key + // aspects of `RefasterRuleCollectionTestCase` subtypes. + return tryExtractPatternGroup(className, TEST_CLASS_NAME_PATTERN) + .orElseThrow( + violation( + "Refaster rule collection test class name '%s' does not match '%s'", + className, TEST_CLASS_NAME_PATTERN)); + } + + private static boolean isInputFile(URI sourceFile) { + String path = sourceFile.getPath(); + + // XXX: Instead of throwing an error here, it'd be nicer to have a bug checker validate key + // aspects of `RefasterRuleCollectionTestCase` subtypes. + return "Input" + .equals( + tryExtractPatternGroup(path, TEST_CLASS_FILE_NAME_PATTERN) + .orElseThrow( + violation( + "Refaster rule collection test file name '%s' does not match '%s'", + path, TEST_CLASS_FILE_NAME_PATTERN))); + } + + private static ImmutableList getRefasterTestCases( + ClassTree tree, VisitorState state) { + return tree.getMembers().stream() + .filter(MethodTree.class::isInstance) + .map(MethodTree.class::cast) + .flatMap(m -> tryExtractRefasterTestCase(m, state).stream()) + .collect(toImmutableList()); + } + + private static Optional tryExtractRefasterTestCase( + MethodTree method, VisitorState state) { + return tryExtractPatternGroup(method.getName().toString(), TEST_METHOD_NAME_PATTERN) + .map(name -> RefasterTestCase.create(name, getFormattedSource(method, state))); + } + + /** + * Returns the source code for the specified method. + * + * @implNote This operation attempts to trim leading whitespace, such that the start and end of + * the method declaration are aligned. The implemented heuristic assumes that the code is + * formatted using Google Java Format. + */ + // XXX: Leading Javadoc and other comments are currently not extracted. Consider fixing this. + private static String getFormattedSource(MethodTree method, VisitorState state) { + String source = SourceCode.treeToString(method, state); + int finalNewline = source.lastIndexOf(LINE_SEPARATOR); + if (finalNewline < 0) { + return source; + } + + int indentation = Math.max(0, source.lastIndexOf(' ') - finalNewline); + String prefixToStrip = " ".repeat(indentation); + + return LINE_SPLITTER + .splitToStream(source) + .map(line -> line.startsWith(prefixToStrip) ? line.substring(indentation) : line) + .collect(joining(LINE_SEPARATOR)); + } + + private static Optional tryExtractPatternGroup(String input, Pattern pattern) { + java.util.regex.Matcher matcher = pattern.matcher(input); + return matcher.matches() ? Optional.of(matcher.group(1)) : Optional.empty(); + } + + @FormatMethod + private static Supplier violation(String format, Object... args) { + return () -> new VerifyException(String.format(format, args)); + } + + @AutoValue + @JsonDeserialize(as = AutoValue_RefasterRuleCollectionTestExtractor_RefasterTestCases.class) + abstract static class RefasterTestCases { + static RefasterTestCases create( + URI source, + String ruleCollection, + boolean isInput, + ImmutableList testCases) { + return new AutoValue_RefasterRuleCollectionTestExtractor_RefasterTestCases( + source, ruleCollection, isInput, testCases); + } + + abstract URI source(); + + abstract String ruleCollection(); + + abstract boolean isInput(); + + abstract ImmutableList testCases(); + } + + @AutoValue + @JsonDeserialize(as = AutoValue_RefasterRuleCollectionTestExtractor_RefasterTestCase.class) + abstract static class RefasterTestCase { + static RefasterTestCase create(String name, String content) { + return new AutoValue_RefasterRuleCollectionTestExtractor_RefasterTestCase(name, content); + } + + abstract String name(); + + abstract String content(); + } +} diff --git a/documentation-support/src/test/java/tech/picnic/errorprone/documentation/BugPatternTestExtractorTest.java b/documentation-support/src/test/java/tech/picnic/errorprone/documentation/BugPatternTestExtractorTest.java index 02d59b8146..c876128113 100644 --- a/documentation-support/src/test/java/tech/picnic/errorprone/documentation/BugPatternTestExtractorTest.java +++ b/documentation-support/src/test/java/tech/picnic/errorprone/documentation/BugPatternTestExtractorTest.java @@ -7,10 +7,10 @@ import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import tech.picnic.errorprone.documentation.BugPatternTestExtractor.BugPatternTestCase; +import tech.picnic.errorprone.documentation.BugPatternTestExtractor.BugPatternTestCases; import tech.picnic.errorprone.documentation.BugPatternTestExtractor.IdentificationTestEntry; import tech.picnic.errorprone.documentation.BugPatternTestExtractor.ReplacementTestEntry; -import tech.picnic.errorprone.documentation.BugPatternTestExtractor.TestCase; -import tech.picnic.errorprone.documentation.BugPatternTestExtractor.TestCases; final class BugPatternTestExtractorTest { @Test @@ -269,11 +269,11 @@ void singleFileCompilationTestHelper(@TempDir Path outputDirectory) { verifyGeneratedFileContent( outputDirectory, "SingleFileCompilationTestHelperTest", - TestCases.create( + BugPatternTestCases.create( URI.create("file:///SingleFileCompilationTestHelperTest.java"), "SingleFileCompilationTestHelperTest", ImmutableList.of( - TestCase.create( + BugPatternTestCase.create( "SingleFileCompilationTestHelperTest.TestChecker", ImmutableList.of( IdentificationTestEntry.create( @@ -302,11 +302,11 @@ void singleFileCompilationTestHelperWithSetArgs(@TempDir Path outputDirectory) { verifyGeneratedFileContent( outputDirectory, "SingleFileCompilationTestHelperWithSetArgsTest", - TestCases.create( + BugPatternTestCases.create( URI.create("file:///SingleFileCompilationTestHelperWithSetArgsTest.java"), "SingleFileCompilationTestHelperWithSetArgsTest", ImmutableList.of( - TestCase.create( + BugPatternTestCase.create( "SingleFileCompilationTestHelperWithSetArgsTest.TestChecker", ImmutableList.of( IdentificationTestEntry.create( @@ -335,11 +335,11 @@ void multiFileCompilationTestHelper(@TempDir Path outputDirectory) { verifyGeneratedFileContent( outputDirectory, "MultiFileCompilationTestHelperTest", - TestCases.create( + BugPatternTestCases.create( URI.create("file:///MultiFileCompilationTestHelperTest.java"), "MultiFileCompilationTestHelperTest", ImmutableList.of( - TestCase.create( + BugPatternTestCase.create( "MultiFileCompilationTestHelperTest.TestChecker", ImmutableList.of( IdentificationTestEntry.create( @@ -370,11 +370,11 @@ void singleFileBugCheckerRefactoringTestHelper(@TempDir Path outputDirectory) { verifyGeneratedFileContent( outputDirectory, "SingleFileBugCheckerRefactoringTestHelperTest", - TestCases.create( + BugPatternTestCases.create( URI.create("file:///SingleFileBugCheckerRefactoringTestHelperTest.java"), "SingleFileBugCheckerRefactoringTestHelperTest", ImmutableList.of( - TestCase.create( + BugPatternTestCase.create( "SingleFileBugCheckerRefactoringTestHelperTest.TestChecker", ImmutableList.of( ReplacementTestEntry.create( @@ -408,12 +408,12 @@ void singleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTest verifyGeneratedFileContent( outputDirectory, "SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest", - TestCases.create( + BugPatternTestCases.create( URI.create( "file:///SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest.java"), "SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest", ImmutableList.of( - TestCase.create( + BugPatternTestCase.create( "SingleFileBugCheckerRefactoringTestHelperWithSetArgsFixChooserAndCustomTestModeTest.TestChecker", ImmutableList.of( ReplacementTestEntry.create( @@ -444,11 +444,11 @@ void multiFileBugCheckerRefactoringTestHelper(@TempDir Path outputDirectory) { verifyGeneratedFileContent( outputDirectory, "MultiFileBugCheckerRefactoringTestHelperTest", - TestCases.create( + BugPatternTestCases.create( URI.create("file:///MultiFileBugCheckerRefactoringTestHelperTest.java"), "MultiFileBugCheckerRefactoringTestHelperTest", ImmutableList.of( - TestCase.create( + BugPatternTestCase.create( "MultiFileBugCheckerRefactoringTestHelperTest.TestChecker", ImmutableList.of( ReplacementTestEntry.create( @@ -484,16 +484,16 @@ void compilationAndBugCheckerRefactoringTestHelpers(@TempDir Path outputDirector verifyGeneratedFileContent( outputDirectory, "CompilationAndBugCheckerRefactoringTestHelpersTest", - TestCases.create( + BugPatternTestCases.create( URI.create("file:///CompilationAndBugCheckerRefactoringTestHelpersTest.java"), "CompilationAndBugCheckerRefactoringTestHelpersTest", ImmutableList.of( - TestCase.create( + BugPatternTestCase.create( "CompilationAndBugCheckerRefactoringTestHelpersTest.TestChecker", ImmutableList.of( IdentificationTestEntry.create( "A.java", "// BUG: Diagnostic contains:\nclass A {}\n"))), - TestCase.create( + BugPatternTestCase.create( "CompilationAndBugCheckerRefactoringTestHelpersTest.TestChecker", ImmutableList.of( ReplacementTestEntry.create( @@ -532,17 +532,17 @@ void compilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNa verifyGeneratedFileContent( outputDirectory, "CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest", - TestCases.create( + BugPatternTestCases.create( URI.create( "file:///CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest.java"), "pkg.CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest", ImmutableList.of( - TestCase.create( + BugPatternTestCase.create( "pkg.CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest.CustomTestChecker", ImmutableList.of( IdentificationTestEntry.create( "A.java", "// BUG: Diagnostic contains:\nclass A {}\n"))), - TestCase.create( + BugPatternTestCase.create( "pkg.CompilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNamesTest.CustomTestChecker2", ImmutableList.of( ReplacementTestEntry.create( @@ -550,9 +550,9 @@ void compilationAndBugCheckerRefactoringTestHelpersWithCustomCheckerPackageAndNa } private static void verifyGeneratedFileContent( - Path outputDirectory, String testClass, TestCases expected) { + Path outputDirectory, String testClass, BugPatternTestCases expected) { assertThat(outputDirectory.resolve(String.format("bugpattern-test-%s.json", testClass))) .exists() - .returns(expected, path -> Json.read(path, TestCases.class)); + .returns(expected, path -> Json.read(path, BugPatternTestCases.class)); } } diff --git a/documentation-support/src/test/java/tech/picnic/errorprone/documentation/RefasterRuleCollectionTestExtractorTest.java b/documentation-support/src/test/java/tech/picnic/errorprone/documentation/RefasterRuleCollectionTestExtractorTest.java new file mode 100644 index 0000000000..020bf7d962 --- /dev/null +++ b/documentation-support/src/test/java/tech/picnic/errorprone/documentation/RefasterRuleCollectionTestExtractorTest.java @@ -0,0 +1,166 @@ +package tech.picnic.errorprone.documentation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.google.common.base.VerifyException; +import com.google.common.collect.ImmutableList; +import java.net.URI; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import tech.picnic.errorprone.documentation.RefasterRuleCollectionTestExtractor.RefasterTestCase; +import tech.picnic.errorprone.documentation.RefasterRuleCollectionTestExtractor.RefasterTestCases; + +final class RefasterRuleCollectionTestExtractorTest { + @Test + void noRefasterRuleTest(@TempDir Path outputDirectory) { + Compilation.compileWithDocumentationGenerator( + outputDirectory, "NoRefasterRuleTest.java", "public final class NoRefasterRuleTest {}"); + + assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory(); + } + + @Test + void invalidTestClassName(@TempDir Path outputDirectory) { + assertThatThrownBy( + () -> + Compilation.compileWithDocumentationGenerator( + outputDirectory, + "InvalidTestClassNameInput.java", + "import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;", + "", + "final class InvalidTestClassName implements RefasterRuleCollectionTestCase {}")) + .cause() + .isInstanceOf(VerifyException.class) + .hasMessage( + "Refaster rule collection test class name 'InvalidTestClassName' does not match '(.*)Test'"); + } + + @Test + void invalidFileName(@TempDir Path outputDirectory) { + assertThatThrownBy( + () -> + Compilation.compileWithDocumentationGenerator( + outputDirectory, + "InvalidFileNameTest.java", + "import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;", + "", + "final class InvalidFileNameTest implements RefasterRuleCollectionTestCase {}")) + .cause() + .isInstanceOf(VerifyException.class) + .hasMessage( + "Refaster rule collection test file name '/InvalidFileNameTest.java' does not match '.*(Input|Output)\\.java'"); + } + + @Test + void emptyRefasterRuleCollectionTestInput(@TempDir Path outputDirectory) { + Compilation.compileWithDocumentationGenerator( + outputDirectory, + "EmptyRefasterRuleCollectionTestInput.java", + "import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;", + "", + "final class EmptyRefasterRuleCollectionTest implements RefasterRuleCollectionTestCase {}"); + + verifyGeneratedFileContent( + outputDirectory, + "EmptyRefasterRuleCollectionTestInput", + RefasterTestCases.create( + URI.create("file:///EmptyRefasterRuleCollectionTestInput.java"), + "EmptyRefasterRuleCollection", + /* isInput= */ true, + ImmutableList.of())); + } + + @Test + void singletonRefasterRuleCollectionTestOutput(@TempDir Path outputDirectory) { + Compilation.compileWithDocumentationGenerator( + outputDirectory, + "SingletonRefasterRuleCollectionTestOutput.java", + "import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;", + "", + "final class SingletonRefasterRuleCollectionTest implements RefasterRuleCollectionTestCase {", + " int testMyRule() {", + " return 42;", + " }", + "}"); + + verifyGeneratedFileContent( + outputDirectory, + "SingletonRefasterRuleCollectionTestOutput", + RefasterTestCases.create( + URI.create("file:///SingletonRefasterRuleCollectionTestOutput.java"), + "SingletonRefasterRuleCollection", + /* isInput= */ false, + ImmutableList.of( + RefasterTestCase.create( + "MyRule", + """ + int testMyRule() { + return 42; + }""")))); + } + + @Test + void complexRefasterRuleCollectionTestOutput(@TempDir Path outputDirectory) { + Compilation.compileWithDocumentationGenerator( + outputDirectory, + "pkg/ComplexRefasterRuleCollectionTestInput.java", + "package pkg;", + "", + "import com.google.common.collect.ImmutableSet;", + "import tech.picnic.errorprone.refaster.test.RefasterRuleCollectionTestCase;", + "", + "final class ComplexRefasterRuleCollectionTest implements RefasterRuleCollectionTestCase {", + " private static final String IGNORED_CONSTANT = \"constant\";", + "", + " @Override", + " public ImmutableSet elidedTypesAndStaticImports() {", + " return ImmutableSet.of();", + " }", + "", + " /** Javadoc. */", + " String testFirstRule() {", + " return \"Don't panic\";", + " }", + "", + " // Comment.", + " String testSecondRule() {", + " return \"Carry a towel\";", + " }", + "", + " void testEmptyRule() {}", + "}"); + + verifyGeneratedFileContent( + outputDirectory, + "ComplexRefasterRuleCollectionTestInput", + RefasterTestCases.create( + URI.create("file:///pkg/ComplexRefasterRuleCollectionTestInput.java"), + "ComplexRefasterRuleCollection", + /* isInput= */ true, + ImmutableList.of( + RefasterTestCase.create( + "FirstRule", + """ + String testFirstRule() { + return "Don't panic"; + }"""), + RefasterTestCase.create( + "SecondRule", + """ + String testSecondRule() { + return "Carry a towel"; + }"""), + RefasterTestCase.create("EmptyRule", "void testEmptyRule() {}")))); + } + + private static void verifyGeneratedFileContent( + Path outputDirectory, String testIdentifier, RefasterTestCases expected) { + assertThat( + outputDirectory.resolve( + String.format("refaster-rule-collection-test-%s.json", testIdentifier))) + .exists() + .returns(expected, path -> Json.read(path, RefasterTestCases.class)); + } +} diff --git a/error-prone-contrib/pom.xml b/error-prone-contrib/pom.xml index 0800167925..cc442db207 100644 --- a/error-prone-contrib/pom.xml +++ b/error-prone-contrib/pom.xml @@ -292,6 +292,55 @@ -Xplugin:DocumentationGenerator -XoutputDirectory=${project.build.directory}/docs + + + + + compile-refaster-test-input + + testCompile + + process-test-resources + + + ${project.basedir}/src/test/resources + + + **/*Input.java + + ${project.build.directory}/refaster-test-input + + + + compile-refaster-test-output + + testCompile + + process-test-resources + + + ${project.basedir}/src/test/resources + + + **/*Output.java + + ${project.build.directory}/refaster-test-output + + + diff --git a/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableMultisetRulesTestInput.java b/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableMultisetRulesTestInput.java index 96231993ea..dc462c7ef6 100644 --- a/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableMultisetRulesTestInput.java +++ b/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableMultisetRulesTestInput.java @@ -26,6 +26,7 @@ ImmutableMultiset> testEmptyImmutableMultiset() { Stream.empty().collect(toImmutableMultiset())); } + @SuppressWarnings("unchecked") ImmutableMultiset> testIterableToImmutableMultiset() { return ImmutableMultiset.of( ImmutableList.of(1).stream().collect(toImmutableMultiset()), diff --git a/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableMultisetRulesTestOutput.java b/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableMultisetRulesTestOutput.java index 76af043a5b..3e4c01d0a5 100644 --- a/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableMultisetRulesTestOutput.java +++ b/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableMultisetRulesTestOutput.java @@ -24,6 +24,7 @@ ImmutableMultiset> testEmptyImmutableMultiset() { return ImmutableMultiset.of(ImmutableMultiset.of(), ImmutableMultiset.of()); } + @SuppressWarnings("unchecked") ImmutableMultiset> testIterableToImmutableMultiset() { return ImmutableMultiset.of( ImmutableMultiset.copyOf(ImmutableList.of(1)), diff --git a/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableSortedMultisetRulesTestInput.java b/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableSortedMultisetRulesTestInput.java index 36c0b35f9d..7424a9b6ba 100644 --- a/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableSortedMultisetRulesTestInput.java +++ b/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableSortedMultisetRulesTestInput.java @@ -37,6 +37,7 @@ ImmutableMultiset> testEmptyImmutableSortedMult Stream.empty().collect(toImmutableSortedMultiset(naturalOrder()))); } + @SuppressWarnings("unchecked") ImmutableMultiset> testIterableToImmutableSortedMultiset() { return ImmutableMultiset.of( ImmutableSortedMultiset.copyOf(naturalOrder(), ImmutableList.of(1)), diff --git a/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableSortedMultisetRulesTestOutput.java b/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableSortedMultisetRulesTestOutput.java index 36fca66a09..e430c8da70 100644 --- a/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableSortedMultisetRulesTestOutput.java +++ b/error-prone-contrib/src/test/resources/tech/picnic/errorprone/refasterrules/ImmutableSortedMultisetRulesTestOutput.java @@ -35,6 +35,7 @@ ImmutableMultiset> testEmptyImmutableSortedMult return ImmutableMultiset.of(ImmutableSortedMultiset.of(), ImmutableSortedMultiset.of()); } + @SuppressWarnings("unchecked") ImmutableMultiset> testIterableToImmutableSortedMultiset() { return ImmutableMultiset.of( ImmutableSortedMultiset.copyOf(ImmutableList.of(1)), diff --git a/refaster-test-support/src/main/java/tech/picnic/errorprone/refaster/test/RefasterRuleCollection.java b/refaster-test-support/src/main/java/tech/picnic/errorprone/refaster/test/RefasterRuleCollection.java index 77991f095e..99673e20e5 100644 --- a/refaster-test-support/src/main/java/tech/picnic/errorprone/refaster/test/RefasterRuleCollection.java +++ b/refaster-test-support/src/main/java/tech/picnic/errorprone/refaster/test/RefasterRuleCollection.java @@ -61,6 +61,9 @@ // XXX: This check currently only validates that one `Refaster.anyOf` branch in one // `@BeforeTemplate` method is covered by a test. Review how we can make sure that _all_ // `@BeforeTemplate` methods and `Refaster.anyOf` branches are covered. +// XXX: Look into replacing this setup with another that allows test cases to be co-located +// with/nested within the rules. This way any rule change only requires modifications in a single +// place, rather than in three. @BugPattern(summary = "Exercises a Refaster rule collection", linkType = NONE, severity = ERROR) @SuppressWarnings("java:S2160" /* Super class equality definition suffices. */) public final class RefasterRuleCollection extends BugChecker implements CompilationUnitTreeMatcher {