From 2b1dbd98cd482ce2b527b4bc17a8996970d783e8 Mon Sep 17 00:00:00 2001 From: Mohamed Sameh <110535847+mohamedsamehsalah@users.noreply.github.com> Date: Mon, 28 Oct 2024 12:37:10 +0100 Subject: [PATCH] Introduce `Slf4jLoggerDeclaration` check (#783) --- .../errorprone/bugpatterns/EmptyMethod.java | 2 +- .../FormatStringConcatenation.java | 9 +- .../bugpatterns/Slf4jLoggerDeclaration.java | 185 +++++++++++++++ .../Slf4jLoggerDeclarationTest.java | 219 ++++++++++++++++++ .../errorprone/utils/MoreASTHelpers.java | 12 + .../errorprone/utils/MoreASTHelpersTest.java | 46 ++++ 6 files changed, 466 insertions(+), 7 deletions(-) create mode 100644 error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/Slf4jLoggerDeclaration.java create mode 100644 error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/Slf4jLoggerDeclarationTest.java diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/EmptyMethod.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/EmptyMethod.java index 306a27c58f..b88eb2e63b 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/EmptyMethod.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/EmptyMethod.java @@ -62,7 +62,7 @@ public Description matchMethod(MethodTree tree, VisitorState state) { } private static boolean isInPossibleTestHelperClass(VisitorState state) { - return Optional.ofNullable(ASTHelpers.findEnclosingNode(state.getPath(), ClassTree.class)) + return Optional.ofNullable(state.findEnclosing(ClassTree.class)) .map(ClassTree::getSimpleName) .filter(name -> name.toString().contains("Test")) .isPresent(); diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/FormatStringConcatenation.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/FormatStringConcatenation.java index 744c6e732c..e80d69611f 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/FormatStringConcatenation.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/FormatStringConcatenation.java @@ -36,6 +36,7 @@ import java.util.List; import java.util.Optional; import org.jspecify.annotations.Nullable; +import tech.picnic.errorprone.utils.MoreASTHelpers; import tech.picnic.errorprone.utils.SourceCode; /** @@ -203,14 +204,10 @@ private static boolean hasNonConstantStringConcatenationArgument( ExpressionTree argument = ASTHelpers.stripParentheses(arguments.get(argPosition)); return argument instanceof BinaryTree - && isStringTyped(argument, state) + && MoreASTHelpers.isStringTyped(argument, state) && ASTHelpers.constValue(argument, String.class) == null; } - private static boolean isStringTyped(ExpressionTree tree, VisitorState state) { - return ASTHelpers.isSameType(ASTHelpers.getType(tree), state.getSymtab().stringType, state); - } - private static class ReplacementArgumentsConstructor extends SimpleTreeVisitor<@Nullable Void, VisitorState> { private final StringBuilder formatString = new StringBuilder(); @@ -223,7 +220,7 @@ private static class ReplacementArgumentsConstructor @Override public @Nullable Void visitBinary(BinaryTree tree, VisitorState state) { - if (tree.getKind() == Kind.PLUS && isStringTyped(tree, state)) { + if (tree.getKind() == Kind.PLUS && MoreASTHelpers.isStringTyped(tree, state)) { tree.getLeftOperand().accept(this, state); tree.getRightOperand().accept(this, state); } else { diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/Slf4jLoggerDeclaration.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/Slf4jLoggerDeclaration.java new file mode 100644 index 0000000000..8435860751 --- /dev/null +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/Slf4jLoggerDeclaration.java @@ -0,0 +1,185 @@ +package tech.picnic.errorprone.bugpatterns; + +import static com.google.common.base.Verify.verify; +import static com.google.errorprone.BugPattern.LinkType.CUSTOM; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.BugPattern.StandardTags.STYLE; +import static com.google.errorprone.matchers.Matchers.allOf; +import static com.google.errorprone.matchers.Matchers.classLiteral; +import static com.google.errorprone.matchers.Matchers.instanceMethod; +import static com.google.errorprone.matchers.Matchers.staticMethod; +import static com.google.errorprone.matchers.Matchers.toType; +import static java.util.Objects.requireNonNull; +import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL; + +import com.google.auto.service.AutoService; +import com.google.common.base.CaseFormat; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; +import com.google.errorprone.BugPattern; +import com.google.errorprone.ErrorProneFlags; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher; +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.ClassTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.ModifiersTree; +import com.sun.source.tree.Tree.Kind; +import com.sun.source.tree.VariableTree; +import com.sun.tools.javac.code.Symbol; +import java.util.EnumSet; +import javax.inject.Inject; +import javax.lang.model.element.Modifier; +import tech.picnic.errorprone.utils.MoreASTHelpers; + +/** A {@link BugChecker} that flags non-canonical SLF4J logger declarations. */ +@AutoService(BugChecker.class) +@BugPattern( + summary = "SLF4J logger declarations should follow established best-practices", + link = BUG_PATTERNS_BASE_URL + "Slf4jLoggerDeclaration", + linkType = CUSTOM, + severity = WARNING, + tags = STYLE) +@SuppressWarnings("java:S2160" /* Super class equality definition suffices. */) +public final class Slf4jLoggerDeclaration extends BugChecker implements VariableTreeMatcher { + private static final long serialVersionUID = 1L; + private static final Matcher IS_GET_LOGGER = + staticMethod().onDescendantOf("org.slf4j.LoggerFactory").named("getLogger"); + private static final String CANONICAL_STATIC_LOGGER_NAME_FLAG = + "Slf4jLogDeclaration:CanonicalStaticLoggerName"; + private static final String DEFAULT_CANONICAL_LOGGER_NAME = "LOG"; + private static final Matcher IS_STATIC_ENCLOSING_CLASS_REFERENCE = + classLiteral(Slf4jLoggerDeclaration::isEnclosingClassReference); + private static final Matcher IS_DYNAMIC_ENCLOSING_CLASS_REFERENCE = + toType( + MethodInvocationTree.class, + allOf( + instanceMethod().anyClass().named("getClass").withNoParameters(), + Slf4jLoggerDeclaration::getClassReceiverIsEnclosingClassInstance)); + private static final ImmutableSet INSTANCE_DECLARATION_MODIFIERS = + Sets.immutableEnumSet(Modifier.PRIVATE, Modifier.FINAL); + private static final ImmutableSet STATIC_DECLARATION_MODIFIERS = + Sets.immutableEnumSet(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL); + + private final String canonicalStaticFieldName; + private final String canonicalInstanceFieldName; + + /** Instantiates a default {@link Slf4jLoggerDeclaration} instance. */ + public Slf4jLoggerDeclaration() { + this(ErrorProneFlags.empty()); + } + + /** + * Instantiates a customized {@link Slf4jLoggerDeclaration}. + * + * @param flags Any provided command line flags. + */ + @Inject + Slf4jLoggerDeclaration(ErrorProneFlags flags) { + canonicalStaticFieldName = + flags.get(CANONICAL_STATIC_LOGGER_NAME_FLAG).orElse(DEFAULT_CANONICAL_LOGGER_NAME); + canonicalInstanceFieldName = + CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, canonicalStaticFieldName); + } + + @Override + public Description matchVariable(VariableTree tree, VisitorState state) { + ExpressionTree initializer = tree.getInitializer(); + if (!IS_GET_LOGGER.matches(initializer, state)) { + return Description.NO_MATCH; + } + + ClassTree clazz = getEnclosingClass(state); + ExpressionTree factoryArg = + Iterables.getOnlyElement(((MethodInvocationTree) initializer).getArguments()); + + SuggestedFix.Builder fix = SuggestedFix.builder(); + + if (clazz.getModifiers().getFlags().contains(Modifier.ABSTRACT) + && IS_DYNAMIC_ENCLOSING_CLASS_REFERENCE.matches(factoryArg, state)) { + /* + * While generally we prefer `Logger` declarations to be static and named after their + * enclosing class, we allow one exception: loggers in abstract classes with a name derived + * from `getClass()`. + */ + suggestModifiers(tree, INSTANCE_DECLARATION_MODIFIERS, fix, state); + suggestRename(tree, canonicalInstanceFieldName, fix, state); + } else { + suggestModifiers( + tree, + clazz.getKind() == Kind.INTERFACE ? ImmutableSet.of() : STATIC_DECLARATION_MODIFIERS, + fix, + state); + suggestRename(tree, canonicalStaticFieldName, fix, state); + + if (!MoreASTHelpers.isStringTyped(factoryArg, state) + && !IS_STATIC_ENCLOSING_CLASS_REFERENCE.matches(factoryArg, state)) { + /* + * Loggers with a custom string name are generally "special", but those with a name derived + * from a class other than the one that encloses it are likely in error. + */ + fix.merge(SuggestedFix.replace(factoryArg, clazz.getSimpleName() + ".class")); + } + } + + return fix.isEmpty() ? Description.NO_MATCH : describeMatch(tree, fix.build()); + } + + private static void suggestModifiers( + VariableTree tree, + ImmutableSet modifiers, + SuggestedFix.Builder fixBuilder, + VisitorState state) { + ModifiersTree modifiersTree = + requireNonNull(ASTHelpers.getModifiers(tree), "`VariableTree` must have modifiers"); + SuggestedFixes.addModifiers(tree, modifiersTree, state, modifiers).ifPresent(fixBuilder::merge); + SuggestedFixes.removeModifiers( + modifiersTree, state, Sets.difference(EnumSet.allOf(Modifier.class), modifiers)) + .ifPresent(fixBuilder::merge); + } + + private static void suggestRename( + VariableTree variableTree, String name, SuggestedFix.Builder fixBuilder, VisitorState state) { + if (!variableTree.getName().contentEquals(name)) { + fixBuilder.merge(SuggestedFixes.renameVariable(variableTree, name, state)); + } + } + + private static boolean isEnclosingClassReference(ExpressionTree tree, VisitorState state) { + return ASTHelpers.getSymbol(getEnclosingClass(state)).equals(ASTHelpers.getSymbol(tree)); + } + + private static boolean getClassReceiverIsEnclosingClassInstance( + MethodInvocationTree getClassInvocationTree, VisitorState state) { + ExpressionTree receiver = ASTHelpers.getReceiver(getClassInvocationTree); + if (receiver == null) { + /* + * Method invocations without an explicit receiver either involve static methods (possibly + * statically imported), or instance methods invoked on the enclosing class. As the given + * `getClassInvocationTree` is guaranteed to be a nullary `#getClass()` invocation, the latter + * must be the case. + */ + return true; + } + + Symbol symbol = ASTHelpers.getSymbol(receiver); + return symbol != null + && symbol.asType().tsym.equals(ASTHelpers.getSymbol(getEnclosingClass(state))); + } + + private static ClassTree getEnclosingClass(VisitorState state) { + ClassTree clazz = state.findEnclosing(ClassTree.class); + // XXX: Review whether we should relax this constraint in the face of so-called anonymous + // classes. See + // https://docs.oracle.com/en/java/javase/23/language/implicitly-declared-classes-and-instance-main-methods.html + verify(clazz != null, "Variable not defined inside class"); + return clazz; + } +} diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/Slf4jLoggerDeclarationTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/Slf4jLoggerDeclarationTest.java new file mode 100644 index 0000000000..ada419cc4c --- /dev/null +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/Slf4jLoggerDeclarationTest.java @@ -0,0 +1,219 @@ +package tech.picnic.errorprone.bugpatterns; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; +import com.google.errorprone.CompilationTestHelper; +import org.junit.jupiter.api.Test; + +final class Slf4jLoggerDeclarationTest { + @Test + void identification() { + CompilationTestHelper.newInstance(Slf4jLoggerDeclaration.class, getClass()) + .addSourceLines( + "A.java", + "import static java.lang.Class.forName;", + "", + "import org.slf4j.Logger;", + "import org.slf4j.LoggerFactory;", + "", + "class A {", + " private static final long serialVersionUID = 1L;", + " private static final Logger LOG = LoggerFactory.getLogger(A.class);", + "", + " abstract static class DynamicLogger {", + " private final Logger log = LoggerFactory.getLogger(getClass());", + " }", + "", + " abstract static class DynamicLoggerWithExplicitThis {", + " private final Logger log = LoggerFactory.getLogger(this.getClass());", + " }", + "", + " static final class StaticLogger {", + " private static final Logger LOG = LoggerFactory.getLogger(StaticLogger.class);", + " }", + "", + " static final class StaticLoggerWithCustomIdentifier {", + " private static final Logger LOG = LoggerFactory.getLogger(\"custom-identifier\");", + " }", + "", + " interface StaticLoggerForInterface {", + " Logger LOG = LoggerFactory.getLogger(StaticLoggerForInterface.class);", + " }", + "", + " abstract static class DynamicLoggerForWrongTypeWithoutReceiver {", + " // BUG: Diagnostic contains:", + " private final Logger log = LoggerFactory.getLogger(forName(\"A.class\"));", + "", + " DynamicLoggerForWrongTypeWithoutReceiver() throws ClassNotFoundException {}", + " }", + "", + " abstract static class DynamicLoggerForWrongTypeWithoutSymbol {", + " // BUG: Diagnostic contains:", + " private final Logger log = LoggerFactory.getLogger(\"foo\".getClass());", + " }", + "", + " abstract static class DynamicLoggerForWrongTypeWithSymbol {", + " // BUG: Diagnostic contains:", + " private final Logger log = LoggerFactory.getLogger(new A().getClass());", + " }", + "", + " static final class NonAbstractDynamicLogger {", + " // BUG: Diagnostic contains:", + " private final Logger log = LoggerFactory.getLogger(getClass());", + " }", + "", + " abstract static class DynamicLoggerWithMissingModifier {", + " // BUG: Diagnostic contains:", + " final Logger log = LoggerFactory.getLogger(getClass());", + " }", + "", + " abstract static class DynamicLoggerWithExcessModifier {", + " // BUG: Diagnostic contains:", + " private final transient Logger log = LoggerFactory.getLogger(getClass());", + " }", + "", + " abstract static class MisnamedDynamicLogger {", + " // BUG: Diagnostic contains:", + " private final Logger LOG = LoggerFactory.getLogger(getClass());", + " }", + "", + " static final class StaticLoggerWithMissingModifier {", + " // BUG: Diagnostic contains:", + " static final Logger LOG = LoggerFactory.getLogger(StaticLoggerWithMissingModifier.class);", + " }", + "", + " static final class StaticLoggerWithExcessModifier {", + " // BUG: Diagnostic contains:", + " private static final transient Logger LOG =", + " LoggerFactory.getLogger(StaticLoggerWithExcessModifier.class);", + " }", + "", + " static final class MisnamedStaticLogger {", + " // BUG: Diagnostic contains:", + " private static final Logger log = LoggerFactory.getLogger(MisnamedStaticLogger.class);", + " }", + "", + " static final class StaticLoggerWithIncorrectIdentifier {", + " // BUG: Diagnostic contains:", + " private static final Logger LOG = LoggerFactory.getLogger(A.class);", + " }", + "", + " static final class StaticLoggerWithCustomIdentifierAndMissingModifier {", + " // BUG: Diagnostic contains:", + " static final Logger LOG = LoggerFactory.getLogger(\"custom-identifier\");", + " }", + "", + " static final class StaticLoggerWithCustomIdentifierAndExcessModifier {", + " // BUG: Diagnostic contains:", + " private static final transient Logger LOG = LoggerFactory.getLogger(\"custom-identifier\");", + " }", + "", + " static final class MisnamedStaticLoggerWithCustomIdentifier {", + " // BUG: Diagnostic contains:", + " private static final Logger log = LoggerFactory.getLogger(\"custom-identifier\");", + " }", + "", + " interface StaticLoggerForInterfaceWithExcessModifier {", + " // BUG: Diagnostic contains:", + " static Logger LOG = LoggerFactory.getLogger(StaticLoggerForInterfaceWithExcessModifier.class);", + " }", + "", + " interface MisnamedStaticLoggerForInterface {", + " // BUG: Diagnostic contains:", + " Logger log = LoggerFactory.getLogger(MisnamedStaticLoggerForInterface.class);", + " }", + "", + " interface StaticLoggerForInterfaceWithIncorrectIdentifier {", + " // BUG: Diagnostic contains:", + " Logger LOG = LoggerFactory.getLogger(A.class);", + " }", + "}") + .doTest(); + } + + @Test + void replacement() { + BugCheckerRefactoringTestHelper.newInstance(Slf4jLoggerDeclaration.class, getClass()) + .addInputLines( + "A.java", + "import org.slf4j.Logger;", + "import org.slf4j.LoggerFactory;", + "", + "class A {", + " static Logger foo = LoggerFactory.getLogger(Logger.class);", + "", + " abstract static class DynamicLogger {", + " transient Logger BAR = LoggerFactory.getLogger(getClass());", + " }", + "", + " static final class StaticLogger {", + " transient Logger baz = LoggerFactory.getLogger(LoggerFactory.class);", + " }", + "", + " static final class StaticLoggerWithCustomIdentifier {", + " transient Logger qux = LoggerFactory.getLogger(\"custom-identifier\");", + " }", + "", + " interface StaticLoggerForInterface {", + " public static final Logger quux = LoggerFactory.getLogger(A.class);", + " }", + "}") + .addOutputLines( + "A.java", + "import org.slf4j.Logger;", + "import org.slf4j.LoggerFactory;", + "", + "class A {", + " private static final Logger LOG = LoggerFactory.getLogger(A.class);", + "", + " abstract static class DynamicLogger {", + " private final Logger log = LoggerFactory.getLogger(getClass());", + " }", + "", + " static final class StaticLogger {", + " private static final Logger LOG = LoggerFactory.getLogger(StaticLogger.class);", + " }", + "", + " static final class StaticLoggerWithCustomIdentifier {", + " private static final Logger LOG = LoggerFactory.getLogger(\"custom-identifier\");", + " }", + "", + " interface StaticLoggerForInterface {", + " Logger LOG = LoggerFactory.getLogger(StaticLoggerForInterface.class);", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + void replacementWithCustomLoggerName() { + BugCheckerRefactoringTestHelper.newInstance(Slf4jLoggerDeclaration.class, getClass()) + .setArgs(ImmutableList.of("-XepOpt:Slf4jLogDeclaration:CanonicalStaticLoggerName=FOO_BAR")) + .addInputLines( + "A.java", + "import org.slf4j.Logger;", + "import org.slf4j.LoggerFactory;", + "", + "class A {", + " transient Logger LOG = LoggerFactory.getLogger(Logger.class);", + "", + " abstract static class DynamicLogger {", + " transient Logger log = LoggerFactory.getLogger(getClass());", + " }", + "}") + .addOutputLines( + "A.java", + "import org.slf4j.Logger;", + "import org.slf4j.LoggerFactory;", + "", + "class A {", + " private static final Logger FOO_BAR = LoggerFactory.getLogger(A.class);", + "", + " abstract static class DynamicLogger {", + " private final Logger fooBar = LoggerFactory.getLogger(getClass());", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } +} diff --git a/error-prone-utils/src/main/java/tech/picnic/errorprone/utils/MoreASTHelpers.java b/error-prone-utils/src/main/java/tech/picnic/errorprone/utils/MoreASTHelpers.java index 30c12d3c0d..0caa0002fe 100644 --- a/error-prone-utils/src/main/java/tech/picnic/errorprone/utils/MoreASTHelpers.java +++ b/error-prone-utils/src/main/java/tech/picnic/errorprone/utils/MoreASTHelpers.java @@ -78,4 +78,16 @@ public static Optional findMethodExitedOnReturn(VisitorState state) public static boolean areSameType(Tree treeA, Tree treeB, VisitorState state) { return ASTHelpers.isSameType(ASTHelpers.getType(treeA), ASTHelpers.getType(treeB), state); } + + /** + * Tells whether the given tree is of type {@link String}. + * + * @param tree The tree of interest. + * @param state The {@link VisitorState} describing the context in which the given tree was found. + * @return Whether the specified tree has the same type as {@link + * com.sun.tools.javac.code.Symtab#stringType}. + */ + public static boolean isStringTyped(Tree tree, VisitorState state) { + return ASTHelpers.isSameType(ASTHelpers.getType(tree), state.getSymtab().stringType, state); + } } diff --git a/error-prone-utils/src/test/java/tech/picnic/errorprone/utils/MoreASTHelpersTest.java b/error-prone-utils/src/test/java/tech/picnic/errorprone/utils/MoreASTHelpersTest.java index efd0a33bff..905023bbaf 100644 --- a/error-prone-utils/src/test/java/tech/picnic/errorprone/utils/MoreASTHelpersTest.java +++ b/error-prone-utils/src/test/java/tech/picnic/errorprone/utils/MoreASTHelpersTest.java @@ -9,10 +9,13 @@ import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.ExpressionStatementTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.ReturnTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher; import com.google.errorprone.matchers.Description; import com.sun.source.tree.ExpressionStatementTree; +import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.ReturnTree; import com.sun.source.tree.Tree; @@ -137,6 +140,25 @@ void areSameType() { .doTest(); } + @Test + void isStringTyped() { + CompilationTestHelper.newInstance(IsStringTypedTestChecker.class, getClass()) + .addSourceLines( + "A.java", + "class A {", + " void m() {", + " int foo = 1;", + " // BUG: Diagnostic contains:", + " String s = \"foo\";", + "", + " hashCode();", + " // BUG: Diagnostic contains:", + " toString();", + " }", + "}") + .doTest(); + } + private static String createMethodSearchDiagnosticsMessage( BiFunction valueFunction, VisitorState state) { return Maps.toMap(ImmutableSet.of("foo", "bar", "baz"), key -> valueFunction.apply(key, state)) @@ -224,4 +246,28 @@ public Description matchMethod(MethodTree tree, VisitorState state) { : Description.NO_MATCH; } } + + /** + * A {@link BugChecker} that delegates to {@link MoreASTHelpers#isStringTyped(Tree, + * VisitorState)}. + */ + @BugPattern(summary = "Interacts with `MoreASTHelpers` for testing purposes", severity = ERROR) + public static final class IsStringTypedTestChecker extends BugChecker + implements MethodInvocationTreeMatcher, VariableTreeMatcher { + private static final long serialVersionUID = 1L; + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + return getDescription(tree, state); + } + + @Override + public Description matchVariable(VariableTree tree, VisitorState state) { + return getDescription(tree, state); + } + + private Description getDescription(Tree tree, VisitorState state) { + return MoreASTHelpers.isStringTyped(tree, state) ? describeMatch(tree) : Description.NO_MATCH; + } + } }