diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MissingRefasterAnnotationCheck.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MissingRefasterAnnotationCheck.java new file mode 100644 index 0000000000..2abc895abd --- /dev/null +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MissingRefasterAnnotationCheck.java @@ -0,0 +1,53 @@ +package tech.picnic.errorprone.bugpatterns; + +import static com.google.errorprone.matchers.ChildMultiMatcher.MatchType.AT_LEAST_ONE; +import static com.google.errorprone.matchers.Matchers.annotations; +import static com.google.errorprone.matchers.Matchers.anyOf; +import static com.google.errorprone.matchers.Matchers.isType; + +import com.google.auto.service.AutoService; +import com.google.errorprone.BugPattern; +import com.google.errorprone.BugPattern.LinkType; +import com.google.errorprone.BugPattern.SeverityLevel; +import com.google.errorprone.BugPattern.StandardTags; +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.google.errorprone.matchers.MultiMatcher; +import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; + +/** A {@link BugChecker} that flags likely missing Refaster annotations. */ +@AutoService(BugChecker.class) +@BugPattern( + name = "MissingRefasterAnnotation", + summary = "The Refaster template contains a method without any Refaster annotations", + linkType = LinkType.NONE, + severity = SeverityLevel.WARNING, + tags = StandardTags.LIKELY_ERROR) +public final class MissingRefasterAnnotationCheck extends BugChecker implements ClassTreeMatcher { + private static final long serialVersionUID = 1L; + private static final MultiMatcher HAS_REFASTER_ANNOTATION = + annotations( + AT_LEAST_ONE, + anyOf( + isType("com.google.errorprone.refaster.annotation.Placeholder"), + isType("com.google.errorprone.refaster.annotation.BeforeTemplate"), + isType("com.google.errorprone.refaster.annotation.AfterTemplate"))); + + @Override + public Description matchClass(ClassTree tree, VisitorState state) { + long methodTypes = + tree.getMembers().stream() + .filter(member -> member.getKind() == Tree.Kind.METHOD) + .map(MethodTree.class::cast) + .map(method -> HAS_REFASTER_ANNOTATION.matches(method, state)) + .distinct() + .count(); + + return methodTypes < 2 ? Description.NO_MATCH : buildDescription(tree).build(); + } +} diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MissingRefasterAnnotationCheckTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MissingRefasterAnnotationCheckTest.java new file mode 100644 index 0000000000..1fb5b2adcc --- /dev/null +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MissingRefasterAnnotationCheckTest.java @@ -0,0 +1,78 @@ +package tech.picnic.errorprone.bugpatterns; + +import com.google.common.base.Predicates; +import com.google.errorprone.CompilationTestHelper; +import org.junit.jupiter.api.Test; + +public final class MissingRefasterAnnotationCheckTest { + private final CompilationTestHelper compilationTestHelper = + CompilationTestHelper.newInstance(MissingRefasterAnnotationCheck.class, getClass()) + .expectErrorMessage( + "X", + Predicates.containsPattern( + "The Refaster template contains a method without any Refaster annotations")); + + @Test + public void testIdentification() { + compilationTestHelper + .addSourceLines( + "A.java", + "import com.google.errorprone.refaster.annotation.AfterTemplate;", + "import com.google.errorprone.refaster.annotation.AlsoNegation;", + "import com.google.errorprone.refaster.annotation.BeforeTemplate;", + "import java.util.Map;", + "", + "class A {", + " // BUG: Diagnostic matches: X", + " static final class MethodLacksBeforeTemplateAnnotation {", + " @BeforeTemplate", + " boolean before1(String string) {", + " return string.equals(\"\");", + " }", + "", + " // @BeforeTemplate is missing", + " boolean before2(String string) {", + " return string.length() == 0;", + " }", + "", + " @AfterTemplate", + " @AlsoNegation", + " boolean after(String string) {", + " return string.isEmpty();", + " }", + " }", + "", + " // BUG: Diagnostic matches: X", + " static final class MethodLacksAfterTemplateAnnotation {", + " @BeforeTemplate", + " boolean before(String string) {", + " return string.equals(\"\");", + " }", + "", + " // @AfterTemplate is missing", + " boolean after(String string) {", + " return string.isEmpty();", + " }", + " }", + "", + " // BUG: Diagnostic matches: X", + " abstract class MethodLacksPlaceholderAnnotation {", + " // @Placeholder is missing", + " abstract V function(K key);", + "", + " @BeforeTemplate", + " void before(Map map, K key) {", + " if (!map.containsKey(key)) {", + " map.put(key, function(key));", + " }", + " }", + "", + " @AfterTemplate", + " void after(Map map, K key) {", + " map.computeIfAbsent(key, k -> function(k));", + " }", + " }", + "}") + .doTest(); + } +}