diff --git a/refaster-runner/src/main/java/tech/picnic/errorprone/refaster/runner/Refaster.java b/refaster-runner/src/main/java/tech/picnic/errorprone/refaster/runner/Refaster.java
index 203e2fe25b..ef3b70161f 100644
--- a/refaster-runner/src/main/java/tech/picnic/errorprone/refaster/runner/Refaster.java
+++ b/refaster-runner/src/main/java/tech/picnic/errorprone/refaster/runner/Refaster.java
@@ -3,7 +3,9 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableRangeSet.toImmutableRangeSet;
import static com.google.errorprone.BugPattern.LinkType.NONE;
+import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
+import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static java.util.function.Predicate.not;
@@ -16,9 +18,11 @@
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeSet;
import com.google.errorprone.BugPattern;
+import com.google.errorprone.BugPattern.SeverityLevel;
import com.google.errorprone.CodeTransformer;
import com.google.errorprone.CompositeCodeTransformer;
import com.google.errorprone.ErrorProneFlags;
+import com.google.errorprone.ErrorProneOptions.Severity;
import com.google.errorprone.SubContext;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
@@ -33,6 +37,7 @@
import java.util.Comparator;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@@ -40,8 +45,8 @@
* A {@link BugChecker} that flags code that can be simplified using Refaster rules located on the
* classpath.
*
- * This checker locates all {@code *.refaster} classpath resources and assumes they contain a
- * {@link CodeTransformer}. The set of loaded Refaster rules can be restricted by passing {@code
+ *
This checker locates all {@code *.refaster} classpath resources and assumes that they contain
+ * a {@link CodeTransformer}. The set of loaded Refaster rules can be restricted by passing {@code
* -XepOpt:Refaster:NamePattern=}.
*/
@AutoService(BugChecker.class)
@@ -104,7 +109,7 @@ public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState s
*/
// XXX: This selection logic solves an issue described in
// https://github.com/google/error-prone/issues/559. Consider contributing it back upstream.
- private static void applyMatches(
+ private void applyMatches(
Iterable allMatches, EndPosTable endPositions, VisitorState state) {
ImmutableList byReplacementSize =
ImmutableList.sortedCopyOf(
@@ -118,12 +123,55 @@ private static void applyMatches(
ImmutableRangeSet ranges = getReplacementRanges(description, endPositions);
if (ranges.asRanges().stream().noneMatch(replacedSections::intersects)) {
/* This suggested fix does not overlap with any ("larger") replacement seen until now. Apply it. */
- state.reportMatch(description);
+ state.reportMatch(augmentDescription(description, getSeverityOverride(state)));
replacedSections.addAll(ranges);
}
}
}
+ private Optional getSeverityOverride(VisitorState state) {
+ return Optional.ofNullable(state.errorProneOptions().getSeverityMap().get(canonicalName()))
+ .flatMap(Refaster::toSeverityLevel);
+ }
+
+ private static Optional toSeverityLevel(Severity severity) {
+ switch (severity) {
+ case DEFAULT:
+ return Optional.empty();
+ case WARN:
+ return Optional.of(WARNING);
+ case ERROR:
+ return Optional.of(ERROR);
+ default:
+ throw new IllegalStateException(String.format("Unsupported severity='%s'", severity));
+ }
+ }
+
+ /**
+ * Updates the given {@link Description}'s details by standardizing the reported check name,
+ * updating the associated message, and optionally overriding its severity.
+ *
+ * The assigned severity is overridden only if this bug checker's severity was explicitly
+ * configured.
+ *
+ *
The original check name (i.e. the Refaster rule name) is prepended to the {@link
+ * Description}'s message. The replacement check name ("Refaster Rule", a name which includes a
+ * space) is chosen such that it is guaranteed not to match any canonical bug checker name (as
+ * that could cause {@link VisitorState#reportMatch(Description)}} to override the reported
+ * severity).
+ */
+ private static Description augmentDescription(
+ Description description, Optional severityOverride) {
+ return Description.builder(
+ description.position,
+ "Refaster Rule",
+ description.getLink(),
+ severityOverride.orElse(description.severity),
+ String.join(": ", description.checkName, description.getRawMessage()))
+ .addAllFixes(description.fixes)
+ .build();
+ }
+
private static int getReplacedCodeSize(Description description, EndPosTable endPositions) {
return getReplacements(description, endPositions).mapToInt(Replacement::length).sum();
}
diff --git a/refaster-runner/src/test/java/tech/picnic/errorprone/refaster/runner/CodeTransformersTest.java b/refaster-runner/src/test/java/tech/picnic/errorprone/refaster/runner/CodeTransformersTest.java
index 5888f03422..5a2a71980d 100644
--- a/refaster-runner/src/test/java/tech/picnic/errorprone/refaster/runner/CodeTransformersTest.java
+++ b/refaster-runner/src/test/java/tech/picnic/errorprone/refaster/runner/CodeTransformersTest.java
@@ -6,12 +6,17 @@
final class CodeTransformersTest {
/**
- * Verifies that {@link CodeTransformers#getAllCodeTransformers()} finds the code transformer
+ * Verifies that {@link CodeTransformers#getAllCodeTransformers()} finds the code transformers
* compiled from {@link FooRules} on the classpath.
*/
@Test
void getAllCodeTransformers() {
assertThat(CodeTransformers.getAllCodeTransformers().keySet())
- .containsExactly("FooRules$SimpleRule");
+ .containsExactlyInAnyOrder(
+ "FooRules$StringOfSizeZeroRule",
+ "FooRules$StringOfSizeZeroVerboseRule",
+ "FooRules$StringOfSizeOneRule",
+ "FooRules$ExtraGrouping$StringOfSizeTwoRule",
+ "FooRules$ExtraGrouping$StringOfSizeThreeRule");
}
}
diff --git a/refaster-runner/src/test/java/tech/picnic/errorprone/refaster/runner/FooRules.java b/refaster-runner/src/test/java/tech/picnic/errorprone/refaster/runner/FooRules.java
index be79875a47..df20e66eb7 100644
--- a/refaster-runner/src/test/java/tech/picnic/errorprone/refaster/runner/FooRules.java
+++ b/refaster-runner/src/test/java/tech/picnic/errorprone/refaster/runner/FooRules.java
@@ -1,14 +1,21 @@
package tech.picnic.errorprone.refaster.runner;
+import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
+import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
+import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
+
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
+import tech.picnic.errorprone.refaster.annotation.Description;
+import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
+import tech.picnic.errorprone.refaster.annotation.Severity;
-/** An example rule collection used to test {@link CodeTransformers}. */
+/** An example rule collection used to test {@link CodeTransformers} and {@link Refaster}. */
final class FooRules {
private FooRules() {}
- /** Simple rule for testing purposes. */
- static final class SimpleRule {
+ /** A simple rule for testing purposes, lacking any custom annotations. */
+ static final class StringOfSizeZeroRule {
@BeforeTemplate
boolean before(String string) {
return string.toCharArray().length == 0;
@@ -19,4 +26,73 @@ boolean after(String string) {
return string.isEmpty();
}
}
+
+ /**
+ * A simple rule for testing purposes, matching the same set of expressions as {@link
+ * StringOfSizeZeroRule}, but producing a larger replacement string.
+ */
+ static final class StringOfSizeZeroVerboseRule {
+ @BeforeTemplate
+ boolean before(String string) {
+ return string.toCharArray().length == 0;
+ }
+
+ @AfterTemplate
+ boolean after(String string) {
+ return string.length() + 1 == 1;
+ }
+ }
+
+ /** A simple rule for testing purposes, having several custom annotations. */
+ @Description("A custom description about matching single-char strings")
+ @OnlineDocumentation
+ @Severity(WARNING)
+ static final class StringOfSizeOneRule {
+ @BeforeTemplate
+ boolean before(String string) {
+ return string.toCharArray().length == 1;
+ }
+
+ @AfterTemplate
+ boolean after(String string) {
+ return string.length() == 1;
+ }
+ }
+
+ /** A nested class with annotations that are inherited by the Refaster rules contained in it. */
+ @Description("A custom subgroup description")
+ @OnlineDocumentation("https://example.com/rule/${topLevelClassName}#${nestedClassName}")
+ @Severity(ERROR)
+ static final class ExtraGrouping {
+ private ExtraGrouping() {}
+
+ /** A simple rule for testing purposes, inheriting custom annotations. */
+ static final class StringOfSizeTwoRule {
+ @BeforeTemplate
+ boolean before(String string) {
+ return string.toCharArray().length == 2;
+ }
+
+ @AfterTemplate
+ boolean after(String string) {
+ return string.length() == 2;
+ }
+ }
+
+ /** A simple rule for testing purposes, overriding custom annotations. */
+ @Description("A custom description about matching three-char strings")
+ @OnlineDocumentation("https://example.com/custom")
+ @Severity(SUGGESTION)
+ static final class StringOfSizeThreeRule {
+ @BeforeTemplate
+ boolean before(String string) {
+ return string.toCharArray().length == 3;
+ }
+
+ @AfterTemplate
+ boolean after(String string) {
+ return string.length() == 3;
+ }
+ }
+ }
}
diff --git a/refaster-runner/src/test/java/tech/picnic/errorprone/refaster/runner/RefasterTest.java b/refaster-runner/src/test/java/tech/picnic/errorprone/refaster/runner/RefasterTest.java
new file mode 100644
index 0000000000..8ec4e5122c
--- /dev/null
+++ b/refaster-runner/src/test/java/tech/picnic/errorprone/refaster/runner/RefasterTest.java
@@ -0,0 +1,272 @@
+package tech.picnic.errorprone.refaster.runner;
+
+import static com.google.common.base.Predicates.containsPattern;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
+import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
+import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
+import static java.util.Comparator.comparingInt;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import com.google.common.collect.ImmutableList;
+import com.google.errorprone.BugCheckerInfo;
+import com.google.errorprone.BugCheckerRefactoringTestHelper;
+import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
+import com.google.errorprone.BugPattern.SeverityLevel;
+import com.google.errorprone.CompilationTestHelper;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import tech.picnic.errorprone.refaster.ErrorProneFork;
+
+final class RefasterTest {
+ private final CompilationTestHelper compilationHelper =
+ CompilationTestHelper.newInstance(Refaster.class, getClass())
+ .matchAllDiagnostics()
+ .expectErrorMessage(
+ "StringOfSizeZeroRule",
+ containsPattern(
+ "\\[Refaster Rule\\] FooRules\\.StringOfSizeZeroRule: Refactoring opportunity\\s+.+\\s+"))
+ .expectErrorMessage(
+ "StringOfSizeOneRule",
+ containsPattern(
+ "\\[Refaster Rule\\] FooRules\\.StringOfSizeOneRule: "
+ + "A custom description about matching single-char strings\\s+.+\\s+"
+ + "\\(see https://error-prone.picnic.tech/refasterrules/FooRules#StringOfSizeOneRule\\)"))
+ .expectErrorMessage(
+ "StringOfSizeTwoRule",
+ containsPattern(
+ "\\[Refaster Rule\\] FooRules\\.ExtraGrouping\\.StringOfSizeTwoRule: "
+ + "A custom subgroup description\\s+.+\\s+"
+ + "\\(see https://example.com/rule/FooRules#ExtraGrouping.StringOfSizeTwoRule\\)"))
+ .expectErrorMessage(
+ "StringOfSizeThreeRule",
+ containsPattern(
+ "\\[Refaster Rule\\] FooRules\\.ExtraGrouping\\.StringOfSizeThreeRule: "
+ + "A custom description about matching three-char strings\\s+.+\\s+"
+ + "\\(see https://example.com/custom\\)"));
+ private final BugCheckerRefactoringTestHelper refactoringTestHelper =
+ BugCheckerRefactoringTestHelper.newInstance(Refaster.class, getClass());
+ private final BugCheckerRefactoringTestHelper restrictedRefactoringTestHelper =
+ BugCheckerRefactoringTestHelper.newInstance(Refaster.class, getClass())
+ .setArgs(
+ "-XepOpt:Refaster:NamePattern=.*\\$(StringOfSizeZeroVerboseRule|StringOfSizeTwoRule)$");
+
+ @Test
+ void identification() {
+ compilationHelper
+ .addSourceLines(
+ "A.java",
+ "class A {",
+ " void m() {",
+ " // BUG: Diagnostic matches: StringOfSizeZeroRule",
+ " boolean b1 = \"foo\".toCharArray().length == 0;",
+ " // BUG: Diagnostic matches: StringOfSizeOneRule",
+ " boolean b2 = \"bar\".toCharArray().length == 1;",
+ " // BUG: Diagnostic matches: StringOfSizeTwoRule",
+ " boolean b3 = \"baz\".toCharArray().length == 2;",
+ " // BUG: Diagnostic matches: StringOfSizeThreeRule",
+ " boolean b4 = \"qux\".toCharArray().length == 3;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ private static Stream severityAssignmentTestCases() {
+ /*
+ * The _actual_ default severity is assigned by the `CodeTransformer`s to which the `Refaster`
+ * bug checker delegates. Here we verify that the absence of an `@Severity` annotation yields
+ * the same severity as the bug checker's declared severity.
+ */
+ SeverityLevel defaultSeverity = BugCheckerInfo.create(Refaster.class).defaultSeverity();
+
+ /* { arguments, expectedSeverities } */
+ return Stream.concat(
+ Stream.of(
+ arguments(
+ ImmutableList.of(), ImmutableList.of(defaultSeverity, WARNING, ERROR, SUGGESTION)),
+ arguments(ImmutableList.of("-Xep:Refaster:OFF"), ImmutableList.of()),
+ arguments(
+ ImmutableList.of("-Xep:Refaster:DEFAULT"),
+ ImmutableList.of(defaultSeverity, WARNING, ERROR, SUGGESTION)),
+ arguments(
+ ImmutableList.of("-Xep:Refaster:WARN"),
+ ImmutableList.of(WARNING, WARNING, WARNING, WARNING)),
+ arguments(
+ ImmutableList.of("-Xep:Refaster:ERROR"),
+ ImmutableList.of(ERROR, ERROR, ERROR, ERROR)),
+ arguments(
+ ImmutableList.of("-XepAllErrorsAsWarnings"),
+ ImmutableList.of(defaultSeverity, WARNING, WARNING, SUGGESTION)),
+ arguments(
+ ImmutableList.of("-Xep:Refaster:OFF", "-XepAllErrorsAsWarnings"),
+ ImmutableList.of()),
+ arguments(
+ ImmutableList.of("-Xep:Refaster:DEFAULT", "-XepAllErrorsAsWarnings"),
+ ImmutableList.of(defaultSeverity, WARNING, WARNING, SUGGESTION)),
+ arguments(
+ ImmutableList.of("-Xep:Refaster:WARN", "-XepAllErrorsAsWarnings"),
+ ImmutableList.of(WARNING, WARNING, WARNING, WARNING)),
+ arguments(
+ ImmutableList.of("-Xep:Refaster:ERROR", "-XepAllErrorsAsWarnings"),
+ ImmutableList.of(WARNING, WARNING, WARNING, WARNING))),
+ ErrorProneFork.isErrorProneForkAvailable()
+ ? Stream.of(
+ arguments(
+ ImmutableList.of("-Xep:Refaster:OFF", "-XepAllSuggestionsAsWarnings"),
+ ImmutableList.of()),
+ arguments(
+ ImmutableList.of("-Xep:Refaster:DEFAULT", "-XepAllSuggestionsAsWarnings"),
+ ImmutableList.of(WARNING, WARNING, ERROR, WARNING)),
+ arguments(
+ ImmutableList.of("-Xep:Refaster:WARN", "-XepAllSuggestionsAsWarnings"),
+ ImmutableList.of(WARNING, WARNING, WARNING, WARNING)),
+ arguments(
+ ImmutableList.of("-Xep:Refaster:ERROR", "-XepAllSuggestionsAsWarnings"),
+ ImmutableList.of(ERROR, ERROR, ERROR, ERROR)),
+ arguments(
+ ImmutableList.of(
+ "-Xep:Refaster:OFF",
+ "-XepAllErrorsAsWarnings",
+ "-XepAllSuggestionsAsWarnings"),
+ ImmutableList.of()),
+ arguments(
+ ImmutableList.of(
+ "-Xep:Refaster:DEFAULT",
+ "-XepAllErrorsAsWarnings",
+ "-XepAllSuggestionsAsWarnings"),
+ ImmutableList.of(WARNING, WARNING, WARNING, WARNING)),
+ arguments(
+ ImmutableList.of(
+ "-Xep:Refaster:WARN",
+ "-XepAllErrorsAsWarnings",
+ "-XepAllSuggestionsAsWarnings"),
+ ImmutableList.of(WARNING, WARNING, WARNING, WARNING)),
+ arguments(
+ ImmutableList.of(
+ "-Xep:Refaster:ERROR",
+ "-XepAllErrorsAsWarnings",
+ "-XepAllSuggestionsAsWarnings"),
+ ImmutableList.of(WARNING, WARNING, WARNING, WARNING)))
+ : Stream.empty());
+ }
+
+ /**
+ * Verifies that the bug checker flags refactoring opportunities with the appropriate severity
+ * level.
+ *
+ * @implNote This test setup is rather cumbersome, because {@link CompilationTestHelper} does not
+ * enable direct assertions against the severity of collected diagnostics output.
+ */
+ @MethodSource("severityAssignmentTestCases")
+ @ParameterizedTest
+ void severityAssignment(
+ ImmutableList arguments, ImmutableList expectedSeverities) {
+ assertThatThrownBy(
+ () ->
+ compilationHelper
+ .setArgs(arguments)
+ .addSourceLines(
+ "A.java",
+ "class A {",
+ " void m() {",
+ " boolean[] bs = {",
+ " \"foo\".toCharArray().length == 0,",
+ " \"bar\".toCharArray().length == 1,",
+ " \"baz\".toCharArray().length == 2,",
+ " \"qux\".toCharArray().length == 3",
+ " };",
+ " }",
+ "}")
+ .doTest())
+ .isInstanceOf(AssertionError.class)
+ .message()
+ .satisfies(
+ message ->
+ assertThat(extractRefasterSeverities("A.java", message))
+ .containsExactlyElementsOf(expectedSeverities));
+ }
+
+ private static ImmutableList extractRefasterSeverities(
+ String fileName, String message) {
+ return Pattern.compile(
+ String.format(
+ "/%s:(\\d+): (Note|warning|error): \\[Refaster Rule\\]", Pattern.quote(fileName)))
+ .matcher(message)
+ .results()
+ .sorted(comparingInt(r -> Integer.parseInt(r.group(1))))
+ .map(r -> toSeverityLevel(r.group(2)))
+ .collect(toImmutableList());
+ }
+
+ private static SeverityLevel toSeverityLevel(String compilerDiagnosticsPrefix) {
+ switch (compilerDiagnosticsPrefix) {
+ case "Note":
+ return SUGGESTION;
+ case "warning":
+ return WARNING;
+ case "error":
+ return ERROR;
+ default:
+ throw new IllegalStateException(
+ String.format("Unrecognized diagnostics prefix '%s'", compilerDiagnosticsPrefix));
+ }
+ }
+
+ @Test
+ void replacement() {
+ refactoringTestHelper
+ .addInputLines(
+ "A.java",
+ "class A {",
+ " void m() {",
+ " boolean b1 = \"foo\".toCharArray().length == 0;",
+ " boolean b2 = \"bar\".toCharArray().length == 1;",
+ " boolean b3 = \"baz\".toCharArray().length == 2;",
+ " boolean b4 = \"qux\".toCharArray().length == 3;",
+ " }",
+ "}")
+ .addOutputLines(
+ "A.java",
+ "class A {",
+ " void m() {",
+ " boolean b1 = \"foo\".isEmpty();",
+ " boolean b2 = \"bar\".length() == 1;",
+ " boolean b3 = \"baz\".length() == 2;",
+ " boolean b4 = \"qux\".length() == 3;",
+ " }",
+ "}")
+ .doTest(TestMode.TEXT_MATCH);
+ }
+
+ @Test
+ void restrictedReplacement() {
+ restrictedRefactoringTestHelper
+ .addInputLines(
+ "A.java",
+ "class A {",
+ " void m() {",
+ " boolean b1 = \"foo\".toCharArray().length == 0;",
+ " boolean b2 = \"bar\".toCharArray().length == 1;",
+ " boolean b3 = \"baz\".toCharArray().length == 2;",
+ " boolean b4 = \"qux\".toCharArray().length == 3;",
+ " }",
+ "}")
+ .addOutputLines(
+ "A.java",
+ "class A {",
+ " void m() {",
+ " boolean b1 = \"foo\".length() + 1 == 1;",
+ " boolean b2 = \"bar\".toCharArray().length == 1;",
+ " boolean b3 = \"baz\".length() == 2;",
+ " boolean b4 = \"qux\".toCharArray().length == 3;",
+ " }",
+ "}")
+ .doTest(TestMode.TEXT_MATCH);
+ }
+}
diff --git a/refaster-support/pom.xml b/refaster-support/pom.xml
index 51680fc3f4..b8a1835630 100644
--- a/refaster-support/pom.xml
+++ b/refaster-support/pom.xml
@@ -46,6 +46,11 @@
error_prone_test_helpers
test
+
+ com.google.auto.value
+ auto-value-annotations
+ provided
+
com.google.code.findbugs
jsr305
@@ -56,6 +61,11 @@
guava
provided
+
+ org.assertj
+ assertj-core
+ test
+
org.junit.jupiter
junit-jupiter-api
@@ -66,5 +76,15 @@
junit-jupiter-engine
test
+
+ org.junit.jupiter
+ junit-jupiter-params
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
diff --git a/refaster-support/src/main/java/tech/picnic/errorprone/refaster/AnnotatedCompositeCodeTransformer.java b/refaster-support/src/main/java/tech/picnic/errorprone/refaster/AnnotatedCompositeCodeTransformer.java
new file mode 100644
index 0000000000..e43f0ea4c5
--- /dev/null
+++ b/refaster-support/src/main/java/tech/picnic/errorprone/refaster/AnnotatedCompositeCodeTransformer.java
@@ -0,0 +1,152 @@
+package tech.picnic.errorprone.refaster;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
+import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
+import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
+import static tech.picnic.errorprone.refaster.annotation.OnlineDocumentation.NESTED_CLASS_URL_PLACEHOLDER;
+import static tech.picnic.errorprone.refaster.annotation.OnlineDocumentation.TOP_LEVEL_CLASS_URL_PLACEHOLDER;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Comparators;
+import com.google.common.collect.ImmutableClassToInstanceMap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
+import com.google.errorprone.BugPattern.SeverityLevel;
+import com.google.errorprone.CodeTransformer;
+import com.google.errorprone.CompositeCodeTransformer;
+import com.google.errorprone.DescriptionListener;
+import com.google.errorprone.ErrorProneOptions;
+import com.google.errorprone.matchers.Description;
+import com.sun.source.util.TreePath;
+import com.sun.tools.javac.util.Context;
+import java.io.Serializable;
+import java.lang.annotation.Annotation;
+import java.util.Iterator;
+import java.util.Optional;
+import java.util.function.Function;
+import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
+import tech.picnic.errorprone.refaster.annotation.Severity;
+
+/**
+ * A {@link CompositeCodeTransformer} that augments the {@link Description} of Refaster rule
+ * matches.
+ *
+ * The content is augmented based on custom {@link tech.picnic.errorprone.refaster.annotation
+ * annotations} available on the matching {@link CodeTransformer} or on this {@link
+ * CompositeCodeTransformer} as a fallback, if any.
+ */
+@AutoValue
+public abstract class AnnotatedCompositeCodeTransformer implements CodeTransformer, Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final Splitter CLASS_NAME_SPLITTER = Splitter.on('.').limit(2);
+
+ abstract String packageName();
+
+ abstract ImmutableList transformers();
+
+ @Override
+ public abstract ImmutableClassToInstanceMap annotations();
+
+ /**
+ * Creates an instance of an {@link AnnotatedCompositeCodeTransformer}.
+ *
+ * @param packageName The package in which the wrapped {@link CodeTransformer}s reside.
+ * @param transformers The {@link CodeTransformer}s to which to delegate.
+ * @param annotations The annotations that are applicable to this {@link CodeTransformer}.
+ * @return A non-{@code null} {@link AnnotatedCompositeCodeTransformer}.
+ */
+ public static AnnotatedCompositeCodeTransformer create(
+ String packageName,
+ ImmutableList transformers,
+ ImmutableClassToInstanceMap annotations) {
+ return new AutoValue_AnnotatedCompositeCodeTransformer(packageName, transformers, annotations);
+ }
+
+ @Override
+ public final void apply(TreePath path, Context context, DescriptionListener listener) {
+ for (CodeTransformer transformer : transformers()) {
+ transformer.apply(
+ path,
+ context,
+ description ->
+ listener.onDescribed(augmentDescription(description, transformer, context)));
+ }
+ }
+
+ private Description augmentDescription(
+ Description description, CodeTransformer delegate, Context context) {
+ String shortCheckName = getShortCheckName(description.checkName);
+ return Description.builder(
+ description.position,
+ shortCheckName,
+ getLinkPattern(delegate, shortCheckName).orElse(null),
+ overrideSeverity(getSeverity(delegate), context),
+ getDescription(delegate))
+ .addAllFixes(description.fixes)
+ .build();
+ }
+
+ private String getShortCheckName(String fullCheckName) {
+ if (packageName().isEmpty()) {
+ return fullCheckName;
+ }
+
+ String prefix = packageName() + '.';
+ checkState(
+ fullCheckName.startsWith(prefix),
+ "Refaster rule class '%s' is not located in package '%s'",
+ fullCheckName,
+ packageName());
+
+ return fullCheckName.substring(prefix.length());
+ }
+
+ private Optional getLinkPattern(CodeTransformer delegate, String checkName) {
+ Iterator nameComponents = CLASS_NAME_SPLITTER.splitToStream(checkName).iterator();
+ return getAnnotationValue(OnlineDocumentation.class, OnlineDocumentation::value, delegate)
+ .map(url -> url.replace(TOP_LEVEL_CLASS_URL_PLACEHOLDER, nameComponents.next()))
+ .map(
+ url ->
+ url.replace(NESTED_CLASS_URL_PLACEHOLDER, Iterators.getNext(nameComponents, "")));
+ }
+
+ private SeverityLevel getSeverity(CodeTransformer delegate) {
+ /*
+ * The default severity should be kept in sync with the default severity of the
+ * `tech.picnic.errorprone.refaster.runner.Refaster` bug checker. (The associated
+ * `RefasterTest#severityAssignment` test verifies this invariant.)
+ */
+ return getAnnotationValue(Severity.class, Severity::value, delegate).orElse(SUGGESTION);
+ }
+
+ private String getDescription(CodeTransformer delegate) {
+ return getAnnotationValue(
+ tech.picnic.errorprone.refaster.annotation.Description.class,
+ tech.picnic.errorprone.refaster.annotation.Description::value,
+ delegate)
+ .orElse("Refactoring opportunity");
+ }
+
+ private Optional getAnnotationValue(
+ Class annotation, Function extractor, CodeTransformer delegate) {
+ return getAnnotationValue(delegate, annotation)
+ .or(() -> getAnnotationValue(this, annotation))
+ .map(extractor);
+ }
+
+ private static Optional getAnnotationValue(
+ CodeTransformer codeTransformer, Class annotation) {
+ return Optional.ofNullable(codeTransformer.annotations().getInstance(annotation));
+ }
+
+ private static SeverityLevel overrideSeverity(SeverityLevel severity, Context context) {
+ ErrorProneOptions options = context.get(ErrorProneOptions.class);
+ SeverityLevel minSeverity =
+ ErrorProneFork.isSuggestionsAsWarningsEnabled(options) ? WARNING : SUGGESTION;
+ SeverityLevel maxSeverity = options.isDropErrorsToWarnings() ? WARNING : ERROR;
+
+ return Comparators.max(Comparators.min(severity, minSeverity), maxSeverity);
+ }
+}
diff --git a/refaster-support/src/main/java/tech/picnic/errorprone/refaster/ErrorProneFork.java b/refaster-support/src/main/java/tech/picnic/errorprone/refaster/ErrorProneFork.java
new file mode 100644
index 0000000000..38d29c403c
--- /dev/null
+++ b/refaster-support/src/main/java/tech/picnic/errorprone/refaster/ErrorProneFork.java
@@ -0,0 +1,56 @@
+package tech.picnic.errorprone.refaster;
+
+import com.google.errorprone.ErrorProneOptions;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Optional;
+
+/**
+ * Utility class that enables the runtime to determine whether Picnic's fork of Error Prone is on
+ * the classpath.
+ *
+ * @see Picnic's Error Prone fork
+ */
+public final class ErrorProneFork {
+ private static final Optional ERROR_PRONE_OPTIONS_IS_SUGGESTIONS_AS_WARNINGS_METHOD =
+ Arrays.stream(ErrorProneOptions.class.getDeclaredMethods())
+ .filter(m -> Modifier.isPublic(m.getModifiers()))
+ .filter(m -> "isSuggestionsAsWarnings".equals(m.getName()))
+ .findFirst();
+
+ private ErrorProneFork() {}
+
+ /**
+ * Tells whether Picnic's fork of Error Prone is available.
+ *
+ * @return {@code true} iff classpath introspection indicates the presence of Error Prone
+ * modifications that are assumed to be present only in Picnic's fork.
+ */
+ public static boolean isErrorProneForkAvailable() {
+ return ERROR_PRONE_OPTIONS_IS_SUGGESTIONS_AS_WARNINGS_METHOD.isPresent();
+ }
+
+ /**
+ * Tells whether the custom {@code -XepAllSuggestionsAsWarnings} flag is set.
+ *
+ * @param options The currently active Error Prone options.
+ * @return {@code true} iff {@link #isErrorProneForkAvailable() the Error Prone fork is available}
+ * and the aforementioned flag is set.
+ * @see google/error-prone#3301
+ */
+ public static boolean isSuggestionsAsWarningsEnabled(ErrorProneOptions options) {
+ return ERROR_PRONE_OPTIONS_IS_SUGGESTIONS_AS_WARNINGS_METHOD
+ .filter(m -> Boolean.TRUE.equals(invoke(m, options)))
+ .isPresent();
+ }
+
+ private static Object invoke(Method method, Object obj, Object... args) {
+ try {
+ return method.invoke(obj, args);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new IllegalStateException(String.format("Failed to invoke method '%s'", method), e);
+ }
+ }
+}
diff --git a/refaster-support/src/main/java/tech/picnic/errorprone/refaster/annotation/Description.java b/refaster-support/src/main/java/tech/picnic/errorprone/refaster/annotation/Description.java
new file mode 100644
index 0000000000..84af8a5727
--- /dev/null
+++ b/refaster-support/src/main/java/tech/picnic/errorprone/refaster/annotation/Description.java
@@ -0,0 +1,22 @@
+package tech.picnic.errorprone.refaster.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Describes the intent of a Refaster rule or group of Refaster rules.
+ *
+ * Annotations on nested classes override the description associated with any enclosing class.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.SOURCE)
+public @interface Description {
+ /**
+ * A description of the annotated Refaster rule(s).
+ *
+ * @return A non-{@code null} string.
+ */
+ String value();
+}
diff --git a/refaster-support/src/main/java/tech/picnic/errorprone/refaster/annotation/OnlineDocumentation.java b/refaster-support/src/main/java/tech/picnic/errorprone/refaster/annotation/OnlineDocumentation.java
new file mode 100644
index 0000000000..f41a2f1b67
--- /dev/null
+++ b/refaster-support/src/main/java/tech/picnic/errorprone/refaster/annotation/OnlineDocumentation.java
@@ -0,0 +1,48 @@
+package tech.picnic.errorprone.refaster.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Signals that a Refaster rule or group of Refaster rules comes with online documentation.
+ *
+ *
The provided value may be a full URL, or a URL pattern containing either or both of the
+ * {@value TOP_LEVEL_CLASS_URL_PLACEHOLDER} and {@value NESTED_CLASS_URL_PLACEHOLDER} placeholders.
+ *
+ *
By default it is assumed that the Refaster rule(s) are documented on the Error Prone Support
+ * website. Annotations on nested classes override the documentation URL associated with any
+ * enclosing class.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.SOURCE)
+public @interface OnlineDocumentation {
+ /**
+ * The URL placeholder value that will be replaced with the name of the top-level class in which
+ * the annotated Refaster rule is located.
+ */
+ String TOP_LEVEL_CLASS_URL_PLACEHOLDER = "${topLevelClassName}";
+ /**
+ * The URL placeholder value that will be replaced with the name of the nested class in which the
+ * annotated Refaster rule is located, if applicable.
+ *
+ *
If the Refaster rule is not defined in a nested class then this placeholder will be replaced
+ * with the empty string. In case the Refaster rule is syntactically nested inside a deeper
+ * hierarchy of classes, then this placeholder will be replaced with concatenation of the names of
+ * all these classes (except the top-level class name), separated by dots.
+ */
+ String NESTED_CLASS_URL_PLACEHOLDER = "${nestedClassName}";
+
+ /**
+ * The URL or URL pattern of the website at which the annotated Refaster rule(s) are documented.
+ *
+ * @return A non-{@code null} string, optionally containing the {@value
+ * TOP_LEVEL_CLASS_URL_PLACEHOLDER} and {@value NESTED_CLASS_URL_PLACEHOLDER} placeholders.
+ */
+ String value() default
+ "https://error-prone.picnic.tech/refasterrules/"
+ + TOP_LEVEL_CLASS_URL_PLACEHOLDER
+ + '#'
+ + NESTED_CLASS_URL_PLACEHOLDER;
+}
diff --git a/refaster-support/src/main/java/tech/picnic/errorprone/refaster/annotation/Severity.java b/refaster-support/src/main/java/tech/picnic/errorprone/refaster/annotation/Severity.java
new file mode 100644
index 0000000000..afa5a6c6e1
--- /dev/null
+++ b/refaster-support/src/main/java/tech/picnic/errorprone/refaster/annotation/Severity.java
@@ -0,0 +1,25 @@
+package tech.picnic.errorprone.refaster.annotation;
+
+import com.google.errorprone.BugPattern.SeverityLevel;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Describes the severity of a Refaster rule or group of Refaster rules.
+ *
+ *
The default severity is the severity assigned to the {@code Refaster} bug checker, which may
+ * be controlled explicitly by running Error Prone with e.g. {@code -Xep:Refaster:WARN}. Annotations
+ * on nested classes override the severity associated with any enclosing class.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.SOURCE)
+public @interface Severity {
+ /**
+ * The expected severity of any match of the annotated Refaster rule(s).
+ *
+ * @return An Error Prone severity level.
+ */
+ SeverityLevel value();
+}
diff --git a/refaster-support/src/main/java/tech/picnic/errorprone/refaster/annotation/package-info.java b/refaster-support/src/main/java/tech/picnic/errorprone/refaster/annotation/package-info.java
new file mode 100644
index 0000000000..53e8e88739
--- /dev/null
+++ b/refaster-support/src/main/java/tech/picnic/errorprone/refaster/annotation/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * A collection of annotations that can be placed on Refaster rule classes and Refaster rule
+ * collection classes, thus influencing the way in which associated rule matches are reported in
+ * non-patch mode.
+ */
+@com.google.errorprone.annotations.CheckReturnValue
+@javax.annotation.ParametersAreNonnullByDefault
+package tech.picnic.errorprone.refaster.annotation;
diff --git a/refaster-support/src/main/java/tech/picnic/errorprone/refaster/package-info.java b/refaster-support/src/main/java/tech/picnic/errorprone/refaster/package-info.java
new file mode 100644
index 0000000000..4955569986
--- /dev/null
+++ b/refaster-support/src/main/java/tech/picnic/errorprone/refaster/package-info.java
@@ -0,0 +1,4 @@
+/** Assorted classes that aid the compilation or evaluation of Refaster rules. */
+@com.google.errorprone.annotations.CheckReturnValue
+@javax.annotation.ParametersAreNonnullByDefault
+package tech.picnic.errorprone.refaster;
diff --git a/refaster-support/src/test/java/tech/picnic/errorprone/refaster/AnnotatedCompositeCodeTransformerTest.java b/refaster-support/src/test/java/tech/picnic/errorprone/refaster/AnnotatedCompositeCodeTransformerTest.java
new file mode 100644
index 0000000000..d49dcf3006
--- /dev/null
+++ b/refaster-support/src/test/java/tech/picnic/errorprone/refaster/AnnotatedCompositeCodeTransformerTest.java
@@ -0,0 +1,204 @@
+package tech.picnic.errorprone.refaster;
+
+import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
+import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
+import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.auto.value.AutoAnnotation;
+import com.google.common.collect.ImmutableClassToInstanceMap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.errorprone.BugPattern.SeverityLevel;
+import com.google.errorprone.CodeTransformer;
+import com.google.errorprone.DescriptionListener;
+import com.google.errorprone.ErrorProneOptions;
+import com.google.errorprone.fixes.Fix;
+import com.google.errorprone.matchers.Description;
+import com.sun.source.util.TreePath;
+import com.sun.tools.javac.util.Context;
+import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+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;
+import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
+import tech.picnic.errorprone.refaster.annotation.Severity;
+
+// XXX: Test the `ErrorProneOptions`-based severity override logic. (Right now that logic is tested
+// through `RefasterTest`, but ideally it is covered by tests in this class, closer to the code that
+// implements the relevant logic.) See the comment in `#context()` below.
+final class AnnotatedCompositeCodeTransformerTest {
+ private static final DiagnosticPosition DUMMY_POSITION = mock(DiagnosticPosition.class);
+ private static final Fix DUMMY_FIX = mock(Fix.class);
+ private static final TreePath DUMMY_PATH = mock(TreePath.class);
+ private static final String DEFAULT_PACKAGE = "";
+ private static final String CUSTOM_PACKAGE = "com.example";
+ private static final String SIMPLE_CLASS_NAME = "MyRefasterRule";
+
+ private static Stream applyTestCases() {
+ /* { context, packageName, ruleName, compositeAnnotations, delegateAnnotations, expectedDescription } */
+ return Stream.of(
+ arguments(
+ context(),
+ DEFAULT_PACKAGE,
+ SIMPLE_CLASS_NAME,
+ ImmutableSet.of(),
+ ImmutableSet.of(),
+ description(
+ SIMPLE_CLASS_NAME, Optional.empty(), SUGGESTION, "Refactoring opportunity")),
+ arguments(
+ context(),
+ CUSTOM_PACKAGE,
+ CUSTOM_PACKAGE + '.' + SIMPLE_CLASS_NAME,
+ ImmutableSet.of(
+ descriptionAnnotation("Composite description"),
+ documentationAnnotation("https://example.com"),
+ severityAnnotation(ERROR)),
+ ImmutableSet.of(),
+ description(
+ SIMPLE_CLASS_NAME,
+ Optional.of("https://example.com"),
+ ERROR,
+ "Composite description")),
+ arguments(
+ context(),
+ DEFAULT_PACKAGE,
+ SIMPLE_CLASS_NAME,
+ ImmutableSet.of(),
+ ImmutableSet.of(
+ descriptionAnnotation("Rule description"),
+ documentationAnnotation("https://example.com/rule/${topLevelClassName}"),
+ severityAnnotation(WARNING)),
+ description(
+ SIMPLE_CLASS_NAME,
+ Optional.of("https://example.com/rule/" + SIMPLE_CLASS_NAME),
+ WARNING,
+ "Rule description")),
+ arguments(
+ context(),
+ CUSTOM_PACKAGE,
+ CUSTOM_PACKAGE + '.' + SIMPLE_CLASS_NAME + ".SomeInnerClass.NestedEvenDeeper",
+ ImmutableSet.of(
+ descriptionAnnotation("Some description"),
+ documentationAnnotation("https://example.com"),
+ severityAnnotation(ERROR)),
+ ImmutableSet.of(
+ descriptionAnnotation("Overriding description"),
+ documentationAnnotation(
+ "https://example.com/rule/${topLevelClassName}/${nestedClassName}"),
+ severityAnnotation(SUGGESTION)),
+ description(
+ SIMPLE_CLASS_NAME + ".SomeInnerClass.NestedEvenDeeper",
+ Optional.of(
+ "https://example.com/rule/"
+ + SIMPLE_CLASS_NAME
+ + "/SomeInnerClass.NestedEvenDeeper"),
+ SUGGESTION,
+ "Overriding description")));
+ }
+
+ @MethodSource("applyTestCases")
+ @ParameterizedTest
+ void apply(
+ Context context,
+ String packageName,
+ String ruleName,
+ ImmutableSet extends Annotation> compositeAnnotations,
+ ImmutableSet extends Annotation> delegateAnnotations,
+ Description expectedDescription) {
+ CodeTransformer codeTransformer =
+ AnnotatedCompositeCodeTransformer.create(
+ packageName,
+ ImmutableList.of(
+ delegateCodeTransformer(
+ delegateAnnotations, context, refasterDescription(ruleName))),
+ indexAnnotations(compositeAnnotations));
+
+ List collected = new ArrayList<>();
+ codeTransformer.apply(DUMMY_PATH, context, collected::add);
+ assertThat(collected)
+ .satisfiesExactly(
+ actual -> {
+ assertThat(actual.position).isEqualTo(expectedDescription.position);
+ assertThat(actual.checkName).isEqualTo(expectedDescription.checkName);
+ assertThat(actual.fixes).containsExactlyElementsOf(expectedDescription.fixes);
+ assertThat(actual.getLink()).isEqualTo(expectedDescription.getLink());
+ assertThat(actual.getRawMessage()).isEqualTo(expectedDescription.getRawMessage());
+ });
+ }
+
+ private static ImmutableClassToInstanceMap indexAnnotations(
+ ImmutableSet extends Annotation> annotations) {
+ return ImmutableClassToInstanceMap.copyOf(
+ Maps.uniqueIndex(annotations, Annotation::annotationType));
+ }
+
+ private static CodeTransformer delegateCodeTransformer(
+ ImmutableSet extends Annotation> annotations,
+ Context expectedContext,
+ Description returnedDescription) {
+ CodeTransformer codeTransformer = mock(CodeTransformer.class);
+
+ when(codeTransformer.annotations()).thenReturn(indexAnnotations(annotations));
+ doAnswer(
+ inv -> {
+ inv.getArgument(2).onDescribed(returnedDescription);
+ return null;
+ })
+ .when(codeTransformer)
+ .apply(eq(DUMMY_PATH), eq(expectedContext), notNull());
+
+ return codeTransformer;
+ }
+
+ /**
+ * Returns a {@link Description} with some default values as produced by {@link
+ * com.google.errorprone.refaster.RefasterScanner}.
+ */
+ private static Description refasterDescription(String name) {
+ return description(name, Optional.of(""), WARNING, "");
+ }
+
+ private static Description description(
+ String name, Optional link, SeverityLevel severityLevel, String message) {
+ return Description.builder(DUMMY_POSITION, name, link.orElse(null), severityLevel, message)
+ .addFix(DUMMY_FIX)
+ .build();
+ }
+
+ private static Context context() {
+ // XXX: Use `ErrorProneOptions#processArgs` to test the
+ // `AnnotatedCompositeCodeTransformer#overrideSeverity` logic.
+ Context context = mock(Context.class);
+ when(context.get(ErrorProneOptions.class)).thenReturn(ErrorProneOptions.empty());
+ return context;
+ }
+
+ @AutoAnnotation
+ private static tech.picnic.errorprone.refaster.annotation.Description descriptionAnnotation(
+ String value) {
+ return new AutoAnnotation_AnnotatedCompositeCodeTransformerTest_descriptionAnnotation(value);
+ }
+
+ @AutoAnnotation
+ private static OnlineDocumentation documentationAnnotation(String value) {
+ return new AutoAnnotation_AnnotatedCompositeCodeTransformerTest_documentationAnnotation(value);
+ }
+
+ @AutoAnnotation
+ private static Severity severityAnnotation(SeverityLevel value) {
+ return new AutoAnnotation_AnnotatedCompositeCodeTransformerTest_severityAnnotation(value);
+ }
+}
diff --git a/refaster-support/src/test/java/tech/picnic/errorprone/refaster/ErrorProneForkTest.java b/refaster-support/src/test/java/tech/picnic/errorprone/refaster/ErrorProneForkTest.java
new file mode 100644
index 0000000000..06b11b8501
--- /dev/null
+++ b/refaster-support/src/test/java/tech/picnic/errorprone/refaster/ErrorProneForkTest.java
@@ -0,0 +1,67 @@
+package tech.picnic.errorprone.refaster;
+
+import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.CompilationTestHelper;
+import com.google.errorprone.ErrorProneOptions;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
+import com.google.errorprone.matchers.Description;
+import com.sun.source.tree.ClassTree;
+import org.junit.jupiter.api.Test;
+
+final class ErrorProneForkTest {
+ @Test
+ void isErrorProneForkAvailable() {
+ assertThat(ErrorProneFork.isErrorProneForkAvailable())
+ .isEqualTo(Boolean.TRUE.toString().equals(System.getProperty("error-prone-fork-in-use")));
+ }
+
+ @Test
+ void isSuggestionsAsWarningsEnabledWithoutFlag() {
+ CompilationTestHelper.newInstance(TestChecker.class, getClass())
+ .addSourceLines(
+ "A.java",
+ "// BUG: Diagnostic contains: Suggestions as warnings enabled: false",
+ "class A {}")
+ .doTest();
+ }
+
+ @Test
+ void isSuggestionsAsWarningsEnabledWithFlag() {
+ assumeTrue(
+ ErrorProneFork.isErrorProneForkAvailable(),
+ "Picnic's Error Prone fork is not on the classpath");
+
+ CompilationTestHelper.newInstance(TestChecker.class, getClass())
+ .setArgs("-XepAllSuggestionsAsWarnings")
+ .addSourceLines(
+ "A.java",
+ "// BUG: Diagnostic contains: Suggestions as warnings enabled: true",
+ "class A {}")
+ .doTest();
+ }
+
+ /**
+ * A {@link BugChecker} that reports the result of {@link
+ * ErrorProneFork#isSuggestionsAsWarningsEnabled(ErrorProneOptions)}.
+ */
+ @BugPattern(summary = "Flags classes with a custom error message", severity = ERROR)
+ public static final class TestChecker extends BugChecker implements ClassTreeMatcher {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Description matchClass(ClassTree tree, VisitorState state) {
+ return buildDescription(tree)
+ .setMessage(
+ String.format(
+ "Suggestions as warnings enabled: %s",
+ ErrorProneFork.isSuggestionsAsWarningsEnabled(state.errorProneOptions())))
+ .build();
+ }
+ }
+}
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 ce01ec9919..daf2d52513 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
@@ -175,11 +175,11 @@ private static ImmutableRangeMap indexRuleMatches(
ImmutableRangeMap.Builder ruleMatches = ImmutableRangeMap.builder();
for (Description description : matches) {
+ String ruleName = extractRefasterRuleName(description);
Set replacements =
Iterables.getOnlyElement(description.fixes).getReplacements(endPositions);
for (Replacement replacement : replacements) {
- ruleMatches.put(
- replacement.range(), getSubstringAfterFinalDelimiter('.', description.checkName));
+ ruleMatches.put(replacement.range(), ruleName);
}
}
@@ -226,6 +226,13 @@ private void reportViolations(
state.reportMatch(describeMatch(tree, fixWithComment));
}
+ private static String extractRefasterRuleName(Description description) {
+ String message = description.getRawMessage();
+ int index = message.indexOf(':');
+ checkState(index >= 0, "Failed to extract Refaster rule name from string '%s'", message);
+ return getSubstringAfterFinalDelimiter('.', message.substring(0, index));
+ }
+
private static String getSubstringAfterFinalDelimiter(char delimiter, String value) {
int index = value.lastIndexOf(delimiter);
checkState(index >= 0, "String '%s' does not contain character '%s'", value, delimiter);