findSuperMethod(MethodSymbol method) {
+ return ASTHelpers.findSuperMethod(method, getTypes());
+ }
+
+ private Types getTypes() {
+ return scanner.getState().getTypes();
+ }
+
+ /**
+ * A quite hacky class used to assert things in {@link ASTHelpersFindSuperMethodsTest}.
+ *
+ * This does two things: it builds a mapping from class names and method names to method
+ * symbols (for easy assertions) and keeps track of the last visited {@link VisitorState}.
+ *
+ *
We cannot do assertions in the {@link Scanner#scan(Tree, VisitorState)} like all the other
+ * test cases do because we need data from all processed classes (and {@link Scanner#scan(Tree,
+ * VisitorState)} is triggered for every single class). Therefore we need to make all assertions
+ * in the test method itself but {@link ASTHelpers#findSuperMethods(MethodSymbol, Types)} requires
+ * a {@link VisitorState} to be used. So we simply remember last state passed to the {@link
+ * Scanner#scan(Tree, VisitorState)}.
+ */
+ private static class FindSuperMethodsTestScanner extends Scanner {
+
+ // A `class name -> method name -> method symbol` structure mapping of given files.
+ private final Table methods;
+
+ // Last state passed to the `Scanner#scan` method.
+ private VisitorState state;
+
+ public FindSuperMethodsTestScanner() {
+ this.methods = HashBasedTable.create();
+ }
+
+ public MethodSymbol getMethod(String className, String methodName) {
+ return methods.get(className, methodName);
+ }
+
+ public VisitorState getState() {
+ return state;
+ }
+
+ @Override
+ public Void scan(Tree tree, VisitorState state) {
+ this.state = state;
+ return super.scan(tree, state);
+ }
+
+ @Override
+ public Void visitMethod(MethodTree methodTree, VisitorState state) {
+ String classContext = ASTHelpers.getSymbol(methodTree).owner.getSimpleName().toString();
+ String methodContext = methodTree.getName().toString();
+
+ MethodSymbol method = ASTHelpers.getSymbol(methodTree);
+ if (!methods.contains(classContext, methodContext)) {
+ methods.put(classContext, methodContext, method);
+ }
+
+ return super.visitMethod(methodTree, state);
+ }
+ }
+}
diff --git a/check_api/src/test/java/com/google/errorprone/util/ASTHelpersTest.java b/check_api/src/test/java/com/google/errorprone/util/ASTHelpersTest.java
new file mode 100644
index 00000000000..6b3a968c8e5
--- /dev/null
+++ b/check_api/src/test/java/com/google/errorprone/util/ASTHelpersTest.java
@@ -0,0 +1,2002 @@
+/*
+ * Copyright 2013 The Error Prone Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.errorprone.util;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
+import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
+import static com.google.errorprone.util.ASTHelpers.getStartPosition;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.util.stream.Collectors.joining;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Verify;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.BugPattern.SeverityLevel;
+import com.google.errorprone.CompilationTestHelper;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.IdentifierTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.MemberReferenceTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.ParameterizedTypeTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
+import com.google.errorprone.fixes.SuggestedFix;
+import com.google.errorprone.matchers.CompilerBasedAbstractTest;
+import com.google.errorprone.matchers.Description;
+import com.google.errorprone.matchers.Matcher;
+import com.google.errorprone.matchers.Matchers;
+import com.google.errorprone.matchers.method.MethodMatchers;
+import com.google.errorprone.scanner.Scanner;
+import com.google.errorprone.util.ASTHelpers.TargetType;
+import com.google.errorprone.util.ASTHelpers.TargetTypeVisitor;
+import com.sun.source.tree.AnnotatedTypeTree;
+import com.sun.source.tree.AnnotationTree;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.ExpressionStatementTree;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.IdentifierTree;
+import com.sun.source.tree.LiteralTree;
+import com.sun.source.tree.MemberReferenceTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.source.tree.MethodTree;
+import com.sun.source.tree.ModifiersTree;
+import com.sun.source.tree.ModuleTree;
+import com.sun.source.tree.NewArrayTree;
+import com.sun.source.tree.NewClassTree;
+import com.sun.source.tree.PackageTree;
+import com.sun.source.tree.ParameterizedTypeTree;
+import com.sun.source.tree.ReturnTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.TypeParameterTree;
+import com.sun.source.tree.VariableTree;
+import com.sun.source.util.TreePathScanner;
+import com.sun.tools.javac.api.BasicJavacTask;
+import com.sun.tools.javac.api.JavacTool;
+import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.code.Symbol.ClassSymbol;
+import com.sun.tools.javac.code.Symbol.MethodSymbol;
+import com.sun.tools.javac.code.Symbol.PackageSymbol;
+import com.sun.tools.javac.code.Symbol.VarSymbol;
+import com.sun.tools.javac.code.Type;
+import com.sun.tools.javac.code.Type.TypeVar;
+import com.sun.tools.javac.main.Main.Result;
+import com.sun.tools.javac.model.JavacElements;
+import com.sun.tools.javac.tree.JCTree.JCLiteral;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import javax.lang.model.element.ElementKind;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link ASTHelpers}. */
+@RunWith(JUnit4.class)
+public class ASTHelpersTest extends CompilerBasedAbstractTest {
+
+ // For tests that expect a specific offset in the file, we test with both Windows and UNIX
+ // line separators, but we hardcode the line separator in the tests to ensure the tests are
+ // hermetic and do not depend on the platform on which they are run.
+ private static final Joiner UNIX_LINE_JOINER = Joiner.on("\n");
+ private static final Joiner WINDOWS_LINE_JOINER = Joiner.on("\r\n");
+
+ final List tests = new ArrayList<>();
+
+ @After
+ public void tearDown() {
+ for (TestScanner test : tests) {
+ test.verifyAssertionsComplete();
+ }
+ }
+
+ @Test
+ public void getStartPositionUnix() {
+ String fileContent =
+ UNIX_LINE_JOINER.join(
+ "public class A { ", " public void foo() {", " int i;", " i = -1;", " }", "}");
+ writeFile("A.java", fileContent);
+ assertCompiles(literalExpressionMatches(literalHasStartPosition(59)));
+ }
+
+ @Test
+ public void getStartPositionWindows() {
+ String fileContent =
+ WINDOWS_LINE_JOINER.join(
+ "public class A { ", " public void foo() {", " int i;", " i = -1;", " }", "}");
+ writeFile("A.java", fileContent);
+ assertCompiles(literalExpressionMatches(literalHasStartPosition(62)));
+ }
+
+ @Test
+ public void getStartPositionWithWhitespaceUnix() {
+ String fileContent =
+ UNIX_LINE_JOINER.join(
+ "public class A { ",
+ " public void foo() {",
+ " int i;",
+ " i = - 1;",
+ " }",
+ "}");
+ writeFile("A.java", fileContent);
+ assertCompiles(literalExpressionMatches(literalHasStartPosition(59)));
+ }
+
+ @Test
+ public void getStartPositionWithWhitespaceWindows() {
+ String fileContent =
+ WINDOWS_LINE_JOINER.join(
+ "public class A { ",
+ " public void foo() {",
+ " int i;",
+ " i = - 1;",
+ " }",
+ "}");
+ writeFile("A.java", fileContent);
+ assertCompiles(literalExpressionMatches(literalHasStartPosition(62)));
+ }
+
+ private Matcher literalHasStartPosition(int startPosition) {
+ return (LiteralTree tree, VisitorState state) -> {
+ JCLiteral literal = (JCLiteral) tree;
+ return literal.getStartPosition() == startPosition;
+ };
+ }
+
+ private Scanner literalExpressionMatches(Matcher matcher) {
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitLiteral(LiteralTree node, VisitorState state) {
+ assertMatch(node, state, matcher);
+ setAssertionsComplete();
+ return super.visitLiteral(node, state);
+ }
+ };
+ tests.add(scanner);
+ return scanner;
+ }
+
+ @Test
+ public void getReceiver() {
+ writeFile(
+ "A.java",
+ """
+ package p;
+ public class A {
+ public B b;
+ public void foo() {}
+ public B bar() {
+ return null;
+ }
+ }
+ """);
+ writeFile(
+ "B.java",
+ """
+ package p;
+ public class B {
+ public static void bar() {}
+ public void foo() {}
+ }
+ """);
+ writeFile(
+ "C.java",
+ "package p;",
+ "import static p.B.bar;",
+ "public class C { ",
+ " public static void foo() {}",
+ " public void test() {",
+ " A a = new A();",
+ " a.foo();", // a
+ " a.b.foo();", // a.b
+ " a.bar().foo();", // a.bar()
+ " this.test();", // this
+ " test();", // null
+ " C.foo();", // C
+ " foo();", // null
+ " C c = new C();",
+ " c.foo();", // c
+ " bar();", // null
+ " }",
+ "}");
+ assertCompiles(expressionStatementMatches("a.foo()", expressionHasReceiverAndType("a", "p.A")));
+ assertCompiles(
+ expressionStatementMatches("a.b.foo()", expressionHasReceiverAndType("a.b", "p.B")));
+ assertCompiles(
+ expressionStatementMatches(
+ "a.bar().foo()", expressionHasReceiverAndType("a.bar()", "p.B")));
+ assertCompiles(
+ expressionStatementMatches("this.test()", expressionHasReceiverAndType("this", "p.C")));
+ assertCompiles(expressionStatementMatches("test()", expressionHasReceiverAndType(null, "p.C")));
+ assertCompiles(expressionStatementMatches("C.foo()", expressionHasReceiverAndType("C", "p.C")));
+ assertCompiles(expressionStatementMatches("foo()", expressionHasReceiverAndType(null, "p.C")));
+ assertCompiles(expressionStatementMatches("c.foo()", expressionHasReceiverAndType("c", "p.C")));
+ assertCompiles(expressionStatementMatches("bar()", expressionHasReceiverAndType(null, "p.B")));
+ }
+
+ private Matcher expressionHasReceiverAndType(
+ String expectedReceiver, String expectedType) {
+ return Matchers.allOf(
+ (ExpressionTree t, VisitorState state) -> {
+ ExpressionTree receiver = ASTHelpers.getReceiver(t);
+ return expectedReceiver != null
+ ? receiver.toString().equals(expectedReceiver)
+ : receiver == null;
+ },
+ (ExpressionTree t, VisitorState state) -> {
+ Type type = ASTHelpers.getReceiverType(t);
+ return ASTHelpers.isSameType(state.getTypeFromString(expectedType), type, state);
+ });
+ }
+
+ private Scanner expressionStatementMatches(
+ String expectedExpression, Matcher matcher) {
+ return new TestScanner() {
+ @Override
+ public Void visitExpressionStatement(ExpressionStatementTree node, VisitorState state) {
+ ExpressionTree expression = node.getExpression();
+ if (expression.toString().equals(expectedExpression)) {
+ assertMatch(node.getExpression(), state, matcher);
+ setAssertionsComplete();
+ }
+ return super.visitExpressionStatement(node, state);
+ }
+ };
+ }
+
+ @Test
+ public void annotationHelpers() {
+ writeFile(
+ "com/google/errorprone/util/InheritedAnnotation.java",
+ """
+ package com.google.errorprone.util;
+ import java.lang.annotation.Inherited;
+ @Inherited
+ public @interface InheritedAnnotation {}
+ """);
+ writeFile(
+ "B.java",
+ """
+ import com.google.errorprone.util.InheritedAnnotation;
+ @InheritedAnnotation
+ public class B {}
+ """);
+ writeFile("C.java", "public class C extends B {}");
+
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitClass(ClassTree tree, VisitorState state) {
+ if (tree.getSimpleName().contentEquals("C")) {
+ assertMatch(
+ tree,
+ state,
+ new Matcher() {
+ @Override
+ public boolean matches(ClassTree t, VisitorState state) {
+ return ASTHelpers.hasAnnotation(t, InheritedAnnotation.class, state);
+ }
+ });
+ setAssertionsComplete();
+ }
+ return super.visitClass(tree, state);
+ }
+ };
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void annotationHelpersWrongValueCached() {
+ writeFile("D.java", "public class D{}");
+
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitClass(ClassTree tree, VisitorState state) {
+ if (tree.getSimpleName().contentEquals("D")) {
+ assertThat(
+ ASTHelpers.hasAnnotation(
+ tree, "TestAnnotation", state.withPath(getCurrentPath())))
+ .isFalse();
+ setAssertionsComplete();
+ }
+ return super.visitClass(tree, state);
+ }
+ };
+ tests.add(scanner);
+ assertCompiles(scanner);
+
+ writeFile(
+ "TestAnnotation.java",
+ """
+ import java.lang.annotation.Inherited;
+ @Inherited
+ public @interface TestAnnotation {}
+ """);
+ writeFile(
+ "B.java",
+ """
+ @TestAnnotation
+ public class B {}
+ """);
+ writeFile("C.java", "public class C extends B {}");
+
+ TestScanner scannerWithAnnotation =
+ new TestScanner() {
+ @Override
+ public Void visitClass(ClassTree tree, VisitorState state) {
+ if (tree.getSimpleName().contentEquals("C")) {
+ assertMatch(
+ tree,
+ state,
+ new Matcher() {
+ @Override
+ public boolean matches(ClassTree t, VisitorState state) {
+ return ASTHelpers.hasAnnotation(t, "TestAnnotation", state);
+ }
+ });
+ setAssertionsComplete();
+ }
+ return super.visitClass(tree, state);
+ }
+ };
+ tests.add(scannerWithAnnotation);
+ assertCompiles(scannerWithAnnotation);
+ }
+
+ @Test
+ public void inheritedMethodAnnotation() {
+ writeFile(
+ "com/google/errorprone/util/InheritedAnnotation.java",
+ """
+ package com.google.errorprone.util;
+ import java.lang.annotation.Inherited;
+ @Inherited
+ public @interface InheritedAnnotation {}
+ """);
+ writeFile(
+ "B.java",
+ """
+ import com.google.errorprone.util.InheritedAnnotation;
+ public class B {
+ @InheritedAnnotation
+ void f() {}
+ }
+ """);
+ writeFile("C.java", "public class C extends B {}");
+
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitMethod(MethodTree tree, VisitorState state) {
+ if (tree.getName().contentEquals("f")) {
+ assertMatch(
+ tree,
+ state,
+ (MethodTree t, VisitorState s) ->
+ ASTHelpers.hasAnnotation(t, InheritedAnnotation.class, s));
+ setAssertionsComplete();
+ }
+ return super.visitMethod(tree, state);
+ }
+ };
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ // verify that hasAnnotation(Symbol, String, VisitorState) uses binary names for inner classes
+ @Test
+ public void innerAnnotationType() {
+ writeFile(
+ "test/Lib.java",
+ """
+ package test;
+ public class Lib {
+ public @interface MyAnnotation {}
+ }
+ """);
+ writeFile(
+ "test/Test.java",
+ """
+ package test;
+ import test.Lib.MyAnnotation;
+ @MyAnnotation
+ public class Test {}
+ """);
+
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitClass(ClassTree tree, VisitorState state) {
+ if (tree.getSimpleName().contentEquals("Test")) {
+ assertMatch(
+ tree,
+ state,
+ new Matcher() {
+ @Override
+ public boolean matches(ClassTree t, VisitorState state) {
+ return ASTHelpers.hasAnnotation(
+ ASTHelpers.getSymbol(t), "test.Lib$MyAnnotation", state);
+ }
+ });
+ setAssertionsComplete();
+ }
+ return super.visitClass(tree, state);
+ }
+ };
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ /* Tests for ASTHelpers#getType */
+
+ @Test
+ public void getTypeOnNestedAnnotationType() {
+ writeFile(
+ "A.java",
+ """
+ public class A {
+ @B.MyAnnotation
+ public void bar() {}
+ }
+ """);
+ writeFile(
+ "B.java",
+ """
+ public class B {
+ @interface MyAnnotation {}
+ }
+ """);
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitAnnotation(AnnotationTree tree, VisitorState state) {
+ setAssertionsComplete();
+ assertThat(ASTHelpers.getType(tree.getAnnotationType()).toString())
+ .isEqualTo("B.MyAnnotation");
+ return super.visitAnnotation(tree, state);
+ }
+ };
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void getTypeOnNestedClassType() {
+ writeFile(
+ "A.java",
+ """
+ public class A {
+ public void bar() {
+ B.C foo;
+ }
+ }
+ """);
+ writeFile(
+ "B.java",
+ """
+ public class B {
+ public static class C {}
+ }
+ """);
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitVariable(VariableTree tree, VisitorState state) {
+ setAssertionsComplete();
+ assertThat(ASTHelpers.getType(tree.getType()).toString()).isEqualTo("B.C");
+ return super.visitVariable(tree, state);
+ }
+ };
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void enclosingElements() {
+ writeFile(
+ "Outer.java",
+ """
+ package com.google.test;
+ public class Outer {
+ static class Middle {
+ private class Inner {
+ int foo() {int x = 1; return x;}
+ }
+ }
+ }
+ """);
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitVariable(VariableTree variable, VisitorState state) {
+ setAssertionsComplete();
+ assertThat(
+ ASTHelpers.enclosingElements(ASTHelpers.getSymbol(variable))
+ .map(sym -> sym.getSimpleName().toString()))
+ .containsExactly(
+ "x", "foo", "Inner", "Middle", "Outer",
+ "test", /* the ModuleSymbol for the unnamed module */ "")
+ .inOrder();
+ return super.visitVariable(variable, state);
+ }
+ };
+
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void getTypeOnParameterizedType() {
+ writeFile(
+ "Pair.java",
+ """
+ public class Pair {
+ public A first;
+ public B second;
+ }
+ """);
+ writeFile(
+ "Test.java",
+ """
+ public class Test {
+ public Integer doSomething(Pair pair) {
+ return pair.first;
+ }
+ }
+ """);
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitReturn(ReturnTree tree, VisitorState state) {
+ setAssertionsComplete();
+ assertThat(ASTHelpers.getType(tree.getExpression()).toString())
+ .isEqualTo("java.lang.Integer");
+ return super.visitReturn(tree, state);
+ }
+ };
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ /* Tests for ASTHelpers#isCheckedExceptionType */
+
+ private TestScanner isCheckedExceptionTypeScanner(boolean expectChecked) {
+ return new TestScanner() {
+ @Override
+ public Void visitMethod(MethodTree tree, VisitorState state) {
+ setAssertionsComplete();
+ List extends ExpressionTree> throwz = tree.getThrows();
+
+ for (ExpressionTree expr : throwz) {
+ Type exType = ASTHelpers.getType(expr);
+ assertThat(ASTHelpers.isCheckedExceptionType(exType, state)).isEqualTo(expectChecked);
+ }
+ return super.visitMethod(tree, state);
+ }
+ };
+ }
+
+ @Test
+ public void isCheckedExceptionType_yes() {
+ writeFile(
+ "A.java",
+ """
+ import java.text.ParseException;
+ public class A {
+ static class MyException extends Exception {}
+ void foo() throws Exception, ParseException {}
+ }
+ """);
+ TestScanner scanner = isCheckedExceptionTypeScanner(true);
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void isCheckedExceptionType_no() {
+ writeFile(
+ "A.java",
+ """
+ public class A {
+ static class MyException extends RuntimeException {}
+ void foo() throws RuntimeException, IllegalArgumentException, MyException,
+ Error, VerifyError {}
+ }
+ """);
+ TestScanner scanner = isCheckedExceptionTypeScanner(false);
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ /* Tests for ASTHelpers#getUpperBound */
+
+ private TestScanner getUpperBoundScanner(String expectedBound) {
+ return new TestScanner() {
+ @Override
+ public Void visitVariable(VariableTree tree, VisitorState state) {
+ setAssertionsComplete();
+ Type varType = ASTHelpers.getType(tree.getType());
+ assertThat(
+ ASTHelpers.getUpperBound(varType.getTypeArguments().get(0), state.getTypes())
+ .toString())
+ .isEqualTo(expectedBound);
+ return super.visitVariable(tree, state);
+ }
+ };
+ }
+
+ @Test
+ public void getUpperBoundConcreteType() {
+ writeFile(
+ "A.java",
+ """
+ import java.lang.Number;
+ import java.util.List;
+ public class A {
+ public List myList;
+ }
+ """);
+ TestScanner scanner = getUpperBoundScanner("java.lang.Number");
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void getUpperBoundUpperBoundedWildcard() {
+ writeFile(
+ "A.java",
+ """
+ import java.lang.Number;
+ import java.util.List;
+ public class A {
+ public List extends Number> myList;
+ }
+ """);
+ TestScanner scanner = getUpperBoundScanner("java.lang.Number");
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void getUpperBoundUnboundedWildcard() {
+ writeFile(
+ "A.java",
+ """
+ import java.util.List;
+ public class A {
+ public List> myList;
+ }
+ """);
+ TestScanner scanner = getUpperBoundScanner("java.lang.Object");
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void getUpperBoundLowerBoundedWildcard() {
+ writeFile(
+ "A.java",
+ """
+ import java.lang.Number;
+ import java.util.List;
+ public class A {
+ public List super Number> myList;
+ }
+ """);
+ TestScanner scanner = getUpperBoundScanner("java.lang.Object");
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void getUpperBoundTypeVariable() {
+ writeFile(
+ "A.java",
+ """
+ import java.util.List;
+ public class A {
+ public List myList;
+ }
+ """);
+ TestScanner scanner = getUpperBoundScanner("java.lang.Object");
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void getUpperBoundCapturedTypeVariable() {
+ writeFile(
+ "A.java",
+ """
+ import java.lang.Number;
+ import java.util.List;
+ public class A {
+ public void doSomething(List extends Number> list) {
+ list.get(0);
+ }
+ }
+ """);
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ if (!tree.toString().equals("super()")) { // ignore synthetic super call
+ setAssertionsComplete();
+ Type type = ASTHelpers.getType(tree);
+ assertThat(type).isInstanceOf(TypeVar.class);
+ assertThat(((TypeVar) type).isCaptured()).isTrue();
+ assertThat(ASTHelpers.getUpperBound(type, state.getTypes()).toString())
+ .isEqualTo("java.lang.Number");
+ }
+ return super.visitMethodInvocation(tree, state);
+ }
+ };
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void commentTokens() {
+ writeFile(
+ "A.java",
+ """
+ public class A {
+ Runnable theRunnable = new Runnable() {
+ /**
+ * foo
+ */
+ public void run() {
+ /* bar1 */
+ /* bar2 */
+ System.err.println("Hi");
+ }
+ // baz number 1
+ // baz number 2
+ };
+ }
+ """);
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitNewClass(NewClassTree tree, VisitorState state) {
+ setAssertionsComplete();
+ List comments = new ArrayList<>();
+ int startPos = getStartPosition(tree);
+ for (ErrorProneToken t : state.getOffsetTokensForNode(tree)) {
+ if (!t.comments().isEmpty()) {
+ for (ErrorProneComment c : t.comments()) {
+ Verify.verify(c.getSourcePos(0) >= startPos);
+ comments.add(c.getText());
+ }
+ }
+ }
+ assertThat(comments)
+ .containsExactly(
+ "/**\n * foo\n */",
+ "/* bar1 */",
+ "/* bar2 */",
+ "// baz number 1",
+ "// baz number 2")
+ .inOrder();
+ return super.visitNewClass(tree, state);
+ }
+ };
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void hasDirectAnnotationWithSimpleName() {
+ writeFile(
+ "A.java",
+ """
+ public class A {
+ @Deprecated public void doIt() {}
+ }
+ """);
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitMethod(MethodTree tree, VisitorState state) {
+ if (tree.getName().contentEquals("doIt")) {
+ setAssertionsComplete();
+ Symbol sym = ASTHelpers.getSymbol(tree);
+ assertThat(ASTHelpers.hasDirectAnnotationWithSimpleName(sym, "Deprecated")).isTrue();
+ assertThat(ASTHelpers.hasDirectAnnotationWithSimpleName(sym, "Nullable")).isFalse();
+ }
+ return super.visitMethod(tree, state);
+ }
+ };
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void getAnnotationsWithSimpleName() {
+ writeFile(
+ "com/google/errorprone/util/RepeatableAnnotations.java",
+ """
+ package com.google.errorprone.util;
+ public @interface RepeatableAnnotations {RepeatableAnnotation[] value();}
+ """);
+ writeFile(
+ "com/google/errorprone/util/RepeatableAnnotation.java",
+ """
+ package com.google.errorprone.util;
+ import java.lang.annotation.Repeatable;
+ @Repeatable(RepeatableAnnotations.class)
+ public @interface RepeatableAnnotation {}
+ """);
+ writeFile(
+ "A.java",
+ """
+ import com.google.errorprone.util.RepeatableAnnotation;
+ public class A {
+ @RepeatableAnnotation
+ @RepeatableAnnotation
+ public void bar() {}
+ }
+ """);
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitMethod(MethodTree tree, VisitorState state) {
+ if (tree.getName().contentEquals("bar")) {
+ setAssertionsComplete();
+ assertThat(
+ ASTHelpers.getAnnotationsWithSimpleName(
+ tree.getModifiers().getAnnotations(), "RepeatableAnnotation"))
+ .hasSize(2);
+ }
+ return super.visitMethod(tree, state);
+ }
+ };
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void hasAnnotationOnMethodInvocation() {
+ writeFile(
+ "A.java",
+ """
+ public class A {
+ @Deprecated public void doIt() {}
+ void caller() { doIt(); }
+ }
+ """);
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ if (ASTHelpers.getSymbol(tree).toString().equals("doIt()")) {
+ setAssertionsComplete();
+ assertThat(ASTHelpers.hasAnnotation(tree, Deprecated.class, state)).isFalse();
+ }
+ return super.visitMethodInvocation(tree, state);
+ }
+ };
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ /**
+ * Test checker to ensure that ASTHelpers.hasDirectAnnotationWithSimpleName() does require the
+ * annotation symbol to be on the classpath.
+ */
+ @BugPattern(
+ severity = SeverityLevel.ERROR,
+ summary =
+ "Test checker to ensure that ASTHelpers.hasDirectAnnotationWithSimpleName() "
+ + "does require the annotation symbol to be on the classpath")
+ public static class HasDirectAnnotationWithSimpleNameChecker extends BugChecker
+ implements MethodInvocationTreeMatcher {
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ if (ASTHelpers.hasDirectAnnotationWithSimpleName(
+ ASTHelpers.getSymbol(tree), "CheckReturnValue")) {
+ return describeMatch(tree);
+ }
+ return Description.NO_MATCH;
+ }
+ }
+
+ /** Test class containing a method annotated with a custom @CheckReturnValue. */
+ public static final class CustomCRVTest {
+ /** A custom @CRV annotation. */
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface CheckReturnValue {}
+
+ @CheckReturnValue
+ public static String hello() {
+ return "Hello!";
+ }
+
+ private CustomCRVTest() {}
+ }
+
+ @Test
+ public void hasDirectAnnotationWithSimpleNameWithoutAnnotationOnClasspath() {
+ CompilationTestHelper.newInstance(HasDirectAnnotationWithSimpleNameChecker.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ void m() {
+ // BUG: Diagnostic contains:
+ com.google.errorprone.util.ASTHelpersTest.CustomCRVTest.hello();
+ }
+ }
+ """)
+ .withClasspath(CustomCRVTest.class, ASTHelpersTest.class, CompilerBasedAbstractTest.class)
+ .doTest();
+ }
+
+ /* Tests inSamePackage. */
+
+ private TestScanner inSamePackageScanner(boolean expectedBoolean) {
+ return new TestScanner() {
+ @Override
+ public Void visitMemberSelect(MemberSelectTree tree, VisitorState state) {
+ setAssertionsComplete();
+ Symbol targetSymbol = ASTHelpers.getSymbol(tree);
+ assertThat(ASTHelpers.inSamePackage(targetSymbol, state)).isEqualTo(expectedBoolean);
+ return super.visitMemberSelect(tree, state);
+ }
+ };
+ }
+
+ @Test
+ public void samePackagePositive() {
+ writeFile(
+ "A.java",
+ """
+ package p;
+ public class A {
+ public static final String BAR = "BAR";
+ }
+ """);
+ writeFile(
+ "B.java",
+ """
+ package p;
+ public class B {
+ public String bar() {
+ return A.BAR;
+ }
+ }
+ """);
+ TestScanner scanner = inSamePackageScanner(true);
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void samePackageNegative() {
+ writeFile(
+ "A.java",
+ """
+ package p;
+ public class A {
+ public static final String BAR = "BAR";
+ }
+ """);
+ writeFile(
+ "B.java",
+ """
+ package q;
+ import p.A;
+ public class B {
+ public String bar() {
+ return A.BAR;
+ }
+ }
+ """);
+ TestScanner scanner = inSamePackageScanner(false);
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ /* Test infrastructure */
+
+ private abstract static class TestScanner extends Scanner {
+ private boolean assertionsComplete = false;
+
+ /**
+ * Subclasses of {@link TestScanner} are expected to call this method within their overridden
+ * visitXYZ() method in order to verify that the method has run at least once.
+ */
+ protected void setAssertionsComplete() {
+ this.assertionsComplete = true;
+ }
+
+ void assertMatch(T node, VisitorState visitorState, Matcher matcher) {
+ VisitorState state = visitorState.withPath(getCurrentPath());
+ assertThat(matcher.matches(node, state)).isTrue();
+ }
+
+ public void verifyAssertionsComplete() {
+ assertWithMessage("Expected the visitor to call setAssertionsComplete().")
+ .that(assertionsComplete)
+ .isTrue();
+ }
+ }
+
+ /** A checker that reports the constant value of fields. */
+ @BugPattern(summary = "", severity = ERROR)
+ public static class ConstChecker extends BugChecker implements VariableTreeMatcher {
+ @Override
+ public Description matchVariable(VariableTree tree, VisitorState state) {
+ Object value = ASTHelpers.constValue(tree.getInitializer());
+ return buildDescription(tree)
+ .setMessage(String.format("%s(%s)", value.getClass().getSimpleName(), value))
+ .build();
+ }
+ }
+
+ @Test
+ public void constValue() {
+ CompilationTestHelper.newInstance(ConstChecker.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ // BUG: Diagnostic contains: Integer(42)
+ static final int A = 42;
+ // BUG: Diagnostic contains: Boolean(false)
+ static final boolean B = false;
+ }
+ """)
+ .doTest();
+ }
+
+ /** A {@link BugChecker} that prints the result type of the first argument in method calls. */
+ @BugPattern(
+ severity = SeverityLevel.ERROR,
+ summary = "Prints the type of the first argument in method calls")
+ public static class PrintResultTypeOfFirstArgument extends BugChecker
+ implements MethodInvocationTreeMatcher {
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ List extends ExpressionTree> arguments = tree.getArguments();
+ if (arguments.isEmpty()) {
+ return Description.NO_MATCH;
+ }
+ return buildDescription(tree)
+ .setMessage(ASTHelpers.getResultType(Iterables.getFirst(arguments, null)).toString())
+ .build();
+ }
+ }
+
+ @Test
+ public void getResultType_findsConcreteType_withGenericMethodCall() {
+ CompilationTestHelper.newInstance(PrintResultTypeOfFirstArgument.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract T get(T obj);
+ abstract void target(Object param);
+ private void test() {
+ // BUG: Diagnostic contains: java.lang.Integer
+ target(get(1));
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void getResultType_findsIntType_withPrimitiveInt() {
+ CompilationTestHelper.newInstance(PrintResultTypeOfFirstArgument.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(int i);
+ private void test(int j) {
+ // BUG: Diagnostic contains: int
+ target(j);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void getResultType_findsConstructedType_withConstructor() {
+ CompilationTestHelper.newInstance(PrintResultTypeOfFirstArgument.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(String s);
+ private void test() {
+ // BUG: Diagnostic contains: java.lang.String
+ target(new String());
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void getResultType_findsNullType_withNull() {
+ CompilationTestHelper.newInstance(PrintResultTypeOfFirstArgument.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(String s);
+ private void test() {
+ // BUG: Diagnostic contains:
+ target(null);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void getResultType_findsConcreteType_withGenericConstructorCall() {
+ CompilationTestHelper.newInstance(PrintResultTypeOfFirstArgument.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class GenericTest {}
+ abstract class Test {
+ abstract void target(Object param);
+ private void test() {
+ // BUG: Diagnostic contains: GenericTest
+ target(new GenericTest());
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ /** A {@link BugChecker} that prints the target type of matched method invocations. */
+ @BugPattern(severity = SeverityLevel.ERROR, summary = "Prints the target type")
+ public static class TargetTypeChecker extends BugChecker
+ implements MethodInvocationTreeMatcher, IdentifierTreeMatcher {
+ private static final Matcher METHOD_MATCHER =
+ MethodMatchers.staticMethod().anyClass().withNameMatching(Pattern.compile("^detect.*"));
+
+ private static final Matcher LOCAL_VARIABLE_MATCHER =
+ ((identifierTree, state) -> {
+ Symbol symbol = ASTHelpers.getSymbol(identifierTree);
+ return symbol != null
+ && symbol.getKind() == ElementKind.LOCAL_VARIABLE
+ && identifierTree.getName().toString().matches("detect.*");
+ });
+
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ if (!METHOD_MATCHER.matches(tree, state)) {
+ return Description.NO_MATCH;
+ }
+ TargetType targetType = ASTHelpers.targetType(state);
+ return buildDescription(tree)
+ .setMessage(String.valueOf(targetType != null ? targetType.type() : null))
+ .build();
+ }
+
+ @Override
+ public Description matchIdentifier(IdentifierTree tree, VisitorState state) {
+ if (!LOCAL_VARIABLE_MATCHER.matches(tree, state)) {
+ return Description.NO_MATCH;
+ }
+ TargetType targetType = ASTHelpers.targetType(state);
+ return buildDescription(tree)
+ .setMessage(String.valueOf(targetType != null ? targetType.type() : null))
+ .build();
+ }
+ }
+
+ @Test
+ public void targetType() {
+ CompilationTestHelper.newInstance(TargetTypeChecker.class, getClass())
+ .addSourceFile("testdata/TargetTypeTest.java")
+ .setArgs(ImmutableList.of("-Xmaxerrs", "200", "-Xmaxwarns", "200"))
+ .doTest();
+ }
+
+ /** A {@link BugChecker} that prints the target type of a parameterized type. */
+ @BugPattern(
+ severity = SeverityLevel.ERROR,
+ summary =
+ "Prints the target type for ParameterizedTypeTree, which is not handled explicitly.")
+ public static class TargetTypeCheckerParentTypeNotMatched extends BugChecker
+ implements ParameterizedTypeTreeMatcher {
+
+ @Override
+ public Description matchParameterizedType(ParameterizedTypeTree tree, VisitorState state) {
+ TargetType targetType = ASTHelpers.targetType(state);
+ return buildDescription(tree)
+ .setMessage(
+ "Target type of "
+ + state.getSourceForNode(tree)
+ + " is "
+ + (targetType != null ? targetType.type() : null))
+ .build();
+ }
+ }
+
+ @Test
+ public void targetType_parentTypeNotMatched() {
+ // Make sure that the method isn't implemented in the visitor; that would make this test
+ // meaningless.
+ List methodNames =
+ Arrays.stream(TargetTypeVisitor.class.getDeclaredMethods())
+ .map(Method::getName)
+ .collect(Collectors.toList());
+ assertThat(methodNames).doesNotContain("visitParameterizedType");
+
+ CompilationTestHelper.newInstance(TargetTypeCheckerParentTypeNotMatched.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ import java.util.ArrayList;
+ class Foo {
+ // BUG: Diagnostic contains: Target type of ArrayList is null
+ Object obj = new ArrayList() {
+ int foo() { return 0; }
+ };
+ }
+ """)
+ .expectResult(Result.ERROR)
+ .doTest();
+ }
+
+ @Target(TYPE_USE)
+ @interface A {
+ int value();
+ }
+
+ @Target({METHOD, FIELD, LOCAL_VARIABLE, PARAMETER})
+ @interface B {
+ String value();
+ }
+
+ @interface ExpectedAnnotation {
+ String expected();
+
+ Target target();
+ }
+
+ /** A {@link Lib}rary for testing. */
+ public static class Lib {
+ @B("one")
+ volatile Map.@A(1) Entry<@A(2) ?, ? extends @A(3) Object> field;
+
+ @B("two")
+ Map.@A(4) Entry<@A(5) ?, ? extends @A(6) Object> method(
+ @B("three") Map.@A(7) Entry<@A(8) ?, ? extends @A(9) Object> param1,
+ @B("four") @A(11) Object @A(10) [] param2) {
+ return null;
+ }
+ }
+
+ @Test
+ public void getDeclarationAndTypeAttributesTest() {
+ BasicJavacTask tool =
+ (BasicJavacTask) JavacTool.create().getTask(null, null, null, null, null, null);
+ ClassSymbol element =
+ JavacElements.instance(tool.getContext()).getTypeElement(Lib.class.getCanonicalName());
+ VarSymbol field =
+ (VarSymbol)
+ element.getEnclosedElements().stream()
+ .filter(e -> e.getSimpleName().contentEquals("field"))
+ .findAny()
+ .get();
+ assertThat(ASTHelpers.getDeclarationAndTypeAttributes(field).map(String::valueOf))
+ .containsExactly(
+ "@com.google.errorprone.util.ASTHelpersTest.B(\"one\")",
+ "@com.google.errorprone.util.ASTHelpersTest.A(1)");
+
+ MethodSymbol method =
+ (MethodSymbol)
+ element.getEnclosedElements().stream()
+ .filter(e -> e.getSimpleName().contentEquals("method"))
+ .findAny()
+ .get();
+ assertThat(ASTHelpers.getDeclarationAndTypeAttributes(method).map(String::valueOf))
+ .containsExactly(
+ "@com.google.errorprone.util.ASTHelpersTest.B(\"two\")",
+ "@com.google.errorprone.util.ASTHelpersTest.A(4)");
+ assertThat(
+ ASTHelpers.getDeclarationAndTypeAttributes(method.getParameters().get(0))
+ .map(String::valueOf))
+ .containsExactly(
+ "@com.google.errorprone.util.ASTHelpersTest.B(\"three\")",
+ "@com.google.errorprone.util.ASTHelpersTest.A(7)");
+ assertThat(
+ ASTHelpers.getDeclarationAndTypeAttributes(method.getParameters().get(1))
+ .map(String::valueOf))
+ .containsExactly(
+ "@com.google.errorprone.util.ASTHelpersTest.B(\"four\")",
+ "@com.google.errorprone.util.ASTHelpersTest.A(10)");
+ }
+
+ /** A {@link BugChecker} that prints if the method can be overridden. */
+ @BugPattern(
+ severity = SeverityLevel.ERROR,
+ summary = "Prints whether the method can be overridden.")
+ public static class MethodCanBeOverriddenChecker extends BugChecker implements MethodTreeMatcher {
+ @Override
+ public Description matchMethod(MethodTree tree, VisitorState state) {
+ boolean methodCanBeOverridden = ASTHelpers.methodCanBeOverridden(ASTHelpers.getSymbol(tree));
+ String description = methodCanBeOverridden ? "Can be overridden" : "Cannot be overridden";
+ return describeMatch(tree, SuggestedFix.prefixWith(tree, "/* " + description + " */\n"));
+ }
+ }
+
+ @Test
+ public void methodCanBeOverridden_class() {
+ CompilationTestHelper.newInstance(MethodCanBeOverriddenChecker.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ // BUG: Diagnostic contains: Cannot be overridden
+ Test() {}
+
+ // BUG: Diagnostic contains: Can be overridden
+ void canBeOverridden() {}
+
+ // BUG: Diagnostic contains: Cannot be overridden
+ final void cannotBeOverriddenBecauseFinal() {}
+
+ // BUG: Diagnostic contains: Cannot be overridden
+ static void cannotBeOverriddenBecauseStatic() {}
+
+ // BUG: Diagnostic contains: Cannot be overridden
+ private void cannotBeOverriddenBecausePrivate() {}
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void methodCanBeOverridden_interface() {
+ CompilationTestHelper.newInstance(MethodCanBeOverriddenChecker.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ interface Test {
+ // BUG: Diagnostic contains: Can be overridden
+ void canBeOverridden();
+
+ // BUG: Diagnostic contains: Can be overridden
+ default void defaultCanBeOverridden() {}
+
+ // BUG: Diagnostic contains: Cannot be overridden
+ static void cannotBeOverriddenBecauseStatic() {}
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void methodCanBeOverridden_enum() {
+ CompilationTestHelper.newInstance(MethodCanBeOverriddenChecker.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ enum Test {
+ VALUE {
+ // BUG: Diagnostic contains: Cannot be overridden
+ @Override void abstractCanBeOverridden() {}
+
+ // BUG: Diagnostic contains: Cannot be overridden
+ void declaredOnlyInValue() {}
+ };
+
+ // BUG: Diagnostic contains: Can be overridden
+ abstract void abstractCanBeOverridden();
+
+ // BUG: Diagnostic contains: Can be overridden
+ void canBeOverridden() {}
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void methodCanBeOverridden_anonymous() {
+ CompilationTestHelper.newInstance(MethodCanBeOverriddenChecker.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ Object obj = new Object() {
+ // BUG: Diagnostic contains: Cannot be overridden
+ void inAnonymousClass() {}
+ };
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void deprecatedClassDeclaration() {
+ writeFile(
+ "A.java",
+ """
+ @Deprecated
+ public class A {
+ }
+ """);
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitClass(ClassTree node, VisitorState visitorState) {
+ // we specifically want to test getSymbol(Tree), not getSymbol(ClassTree)
+ Tree tree = node;
+ assertThat(ASTHelpers.getSymbol(tree).isDeprecated()).isTrue();
+ setAssertionsComplete();
+ return super.visitClass(node, visitorState);
+ }
+ };
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ @Test
+ public void outermostclass_dotClass() {
+ writeFile(
+ "Foo.java",
+ """
+ class Foo {
+ void f() {
+ Object unused = boolean.class;
+ }
+ }
+ """);
+
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitMemberSelect(MemberSelectTree tree, VisitorState state) {
+ Symbol targetSymbol = ASTHelpers.getSymbol(tree);
+ // ASTHelpers#outermostClass shouldn't itself NPE
+ assertThat(ASTHelpers.outermostClass(targetSymbol)).isNull();
+ setAssertionsComplete();
+ return super.visitMemberSelect(tree, state);
+ }
+ };
+ tests.add(scanner);
+
+ assertCompiles(scanner);
+ }
+
+ /** Replaces all throws clauses with more specific exceptions. */
+ @BugPattern(
+ summary = "Replaces all throws clauses with more specific exceptions.",
+ severity = WARNING)
+ public static final class ReplaceWithExceptions extends BugChecker implements MethodTreeMatcher {
+ @Override
+ public Description matchMethod(MethodTree tree, VisitorState state) {
+ return buildDescription(tree)
+ .setMessage(
+ ASTHelpers.getThrownExceptions(tree.getBody(), state).stream()
+ .map(t -> t.tsym.getSimpleName().toString())
+ .sorted()
+ .collect(joining(" ", "[", "]")))
+ .build();
+ }
+ }
+
+ private final CompilationTestHelper replaceExceptionHelper =
+ CompilationTestHelper.newInstance(ReplaceWithExceptions.class, getClass());
+
+ @Test
+ public void getThrownExceptions_trivialThrow() {
+ replaceExceptionHelper
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ // BUG: Diagnostic contains: [IllegalStateException]
+ void test() throws Exception {
+ throw new IllegalStateException();
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void getThrownExceptions_caught() {
+ replaceExceptionHelper
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ // BUG: Diagnostic contains: []
+ void test() throws Exception {
+ try {
+ throw new IllegalStateException();
+ } catch (Exception e) {}
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void getThrownExceptions_caughtAndRethrown() {
+ replaceExceptionHelper
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ // BUG: Diagnostic contains: [IllegalStateException]
+ void test() throws Exception {
+ try {
+ throw new IllegalStateException();
+ } catch (Exception e) {
+ throw e;
+ }
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void getThrownExceptions_genericThrow() {
+ replaceExceptionHelper
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ // BUG: Diagnostic contains: [IllegalStateException]
+ int test(java.util.Optional x) {
+ return x.orElseThrow(() -> new IllegalStateException());
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void getThrownExceptions_rethrown() {
+ replaceExceptionHelper
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ // BUG: Diagnostic contains: [InterruptedException]
+ void test() throws Exception {
+ try {
+ test();
+ } catch (InterruptedException e) {
+ throw e;
+ } catch (Exception e) {}
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void getThrownExceptions_rethrownUnion() {
+ replaceExceptionHelper
+ .addSourceLines(
+ "Test.java",
+ """
+import java.util.concurrent.Callable;
+import java.io.FileNotFoundException;
+import java.io.UnsupportedEncodingException;
+class Test {
+ // BUG: Diagnostic contains: [FileNotFoundException UnsupportedEncodingException]
+ void test(Callable c) throws FileNotFoundException, UnsupportedEncodingException {
+ try {
+ if (hashCode() == 1) {
+ throw new FileNotFoundException("File not found");
+ }
+ c.call();
+ } catch (FileNotFoundException | UnsupportedEncodingException e) {
+ throw e;
+ } catch (Exception e) {
+ return;
+ }
+ }
+}
+""")
+ .doTest();
+ }
+
+ @Test
+ public void getThrownExceptions_tryWithResources() {
+ replaceExceptionHelper
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ // BUG: Diagnostic contains: [InterruptedException]
+ void test() throws Exception {
+ try (var x = c()) {}
+ }
+ // BUG: Diagnostic contains:
+ abstract C c();
+ abstract class C implements AutoCloseable {
+ // BUG: Diagnostic contains:
+ @Override public abstract void close() throws InterruptedException;
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void getThrownExceptions_tryWithResourcesVariable() {
+ replaceExceptionHelper
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ // BUG: Diagnostic contains: [InterruptedException]
+ void test() throws Exception {
+ var x = c();
+ try (x) {}
+ }
+ // BUG: Diagnostic contains:
+ abstract C c();
+ abstract class C implements AutoCloseable {
+ // BUG: Diagnostic contains:
+ @Override public abstract void close() throws InterruptedException;
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void isSubtype_onMethods_isFalse() {
+ writeFile(
+ "A.java",
+ """
+ public class A {
+ public void doIt(T t) {}
+ public int doItAgain(int i) {return 42;}
+ private static void doSomething() {}
+ }
+ class B extends A {
+ @Override
+ public void doIt(T t) {}
+ @Override
+ public int doItAgain(int i) {return 42;}
+ }
+ """);
+ TestScanner scanner =
+ new TestScanner() {
+ @Override
+ public Void visitMethod(MethodTree tree, VisitorState state) {
+ setAssertionsComplete();
+ Symbol sym = ASTHelpers.getSymbol(tree);
+ assertThat(ASTHelpers.isSubtype(sym.asType(), sym.asType(), state)).isFalse();
+ return super.visitMethod(tree, state);
+ }
+ };
+ tests.add(scanner);
+ assertCompiles(scanner);
+ }
+
+ /** Comments on method invocations with their receiver chain. */
+ @BugPattern(
+ summary = "Comments on method invocations with their receiver chain.",
+ severity = WARNING)
+ public static final class ReceiverStream extends BugChecker
+ implements MethodInvocationTreeMatcher {
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ return buildDescription(tree)
+ .setMessage(
+ ASTHelpers.streamReceivers(tree)
+ .map(state::getSourceForNode)
+ .collect(joining(", ", "[", "]")))
+ .build();
+ }
+ }
+
+ @Test
+ public void receiverStream() {
+ CompilationTestHelper.newInstance(ReceiverStream.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private Test t;
+ private void t() {
+ // BUG: Diagnostic contains: []
+ t();
+ // BUG: Diagnostic contains: [t.t, t]
+ t.t.t();
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ /** Helper for testing {@link ASTHelpers#canBeRemoved}. */
+ @BugPattern(summary = "", severity = WARNING)
+ public static final class VisibleMembers extends BugChecker
+ implements MethodTreeMatcher, VariableTreeMatcher {
+
+ @Override
+ public Description matchMethod(MethodTree tree, VisitorState state) {
+ return ASTHelpers.canBeRemoved(ASTHelpers.getSymbol(tree), state)
+ ? describeMatch(tree)
+ : Description.NO_MATCH;
+ }
+
+ @Override
+ public Description matchVariable(VariableTree tree, VisitorState state) {
+ return ASTHelpers.canBeRemoved(ASTHelpers.getSymbol(tree))
+ ? describeMatch(tree)
+ : Description.NO_MATCH;
+ }
+ }
+
+ @Test
+ public void visibleMembers() {
+ CompilationTestHelper.newInstance(VisibleMembers.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ // BUG: Diagnostic contains:
+ private Test t;
+ private class Inner {
+ // BUG: Diagnostic contains:
+ public Test t;
+ // BUG: Diagnostic contains:
+ public void test() {}
+ @Override public String toString() { return null; }
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ private static final ImmutableSet> ALL_TREE_TYPES =
+ Arrays.stream(TreePathScanner.class.getMethods())
+ .filter(m -> m.getName().startsWith("visit"))
+ .filter(m -> m.getParameters().length == 2)
+ .map(m -> m.getParameters()[0].getType())
+ .collect(toImmutableSet());
+
+ @Test
+ public void typesWithModifiersTree() {
+ for (Class> clazz : ALL_TREE_TYPES) {
+ switch (clazz.getSimpleName()) {
+ case "MethodTree":
+ assertGetModifiersInvoked(MethodTree.class, MethodTree::getModifiers);
+ break;
+ case "ClassTree":
+ assertGetModifiersInvoked(ClassTree.class, ClassTree::getModifiers);
+ break;
+ case "VariableTree":
+ assertGetModifiersInvoked(VariableTree.class, VariableTree::getModifiers);
+ break;
+ case "ModifiersTree":
+ ModifiersTree modifiersTree = mock(ModifiersTree.class);
+ assertThat(ASTHelpers.getModifiers(modifiersTree)).isSameInstanceAs(modifiersTree);
+ break;
+ default:
+ assertMethodNotFound(clazz, "getModifiers");
+ }
+ }
+ }
+
+ private static void assertGetModifiersInvoked(
+ Class clazz, Function getter) {
+ T tree = mock(clazz);
+ ASTHelpers.getModifiers(tree);
+
+ // This effectively means the same as {@code verify(tree).getModifiers()}.
+ ModifiersTree ignored = getter.apply(verify(tree));
+ }
+
+ @Test
+ public void typesWithGetAnnotations() {
+ for (Class> clazz : ALL_TREE_TYPES) {
+ switch (clazz.getSimpleName()) {
+ case "TypeParameterTree":
+ assertGetAnnotationsInvoked(TypeParameterTree.class, TypeParameterTree::getAnnotations);
+ break;
+ case "ModuleTree":
+ assertGetAnnotationsInvoked(ModuleTree.class, ModuleTree::getAnnotations);
+ break;
+ case "PackageTree":
+ assertGetAnnotationsInvoked(PackageTree.class, PackageTree::getAnnotations);
+ break;
+ case "NewArrayTree":
+ assertGetAnnotationsInvoked(NewArrayTree.class, NewArrayTree::getAnnotations);
+ break;
+ case "ModifiersTree":
+ assertGetAnnotationsInvoked(ModifiersTree.class, ModifiersTree::getAnnotations);
+ break;
+ case "AnnotatedTypeTree":
+ assertGetAnnotationsInvoked(AnnotatedTypeTree.class, AnnotatedTypeTree::getAnnotations);
+ break;
+ default:
+ assertMethodNotFound(clazz, "getAnnotations");
+ }
+ }
+ }
+
+ private static void assertGetAnnotationsInvoked(
+ Class clazz, Function> getter) {
+ T tree = mock(clazz);
+ ASTHelpers.getAnnotations(tree);
+ // This effectively means the same as {@code verify(tree).getAnnotations()}.
+ List extends AnnotationTree> ignored = getter.apply(verify(tree));
+ }
+
+ private static void assertMethodNotFound(Class> clazz, String method, Class>... params) {
+ assertThrows(NoSuchMethodException.class, () -> clazz.getMethod(method, params));
+ }
+
+ /** Helper for testing {@link ASTHelpers#enclosingPackage}. */
+ @BugPattern(summary = "", severity = WARNING)
+ public static final class EnclosingPackage extends BugChecker
+ implements VariableTreeMatcher, MemberReferenceTreeMatcher {
+ @Override
+ public Description matchVariable(VariableTree tree, VisitorState state) {
+ Symbol sym = ASTHelpers.getSymbol(tree);
+ if (sym.getKind().equals(ElementKind.FIELD)) {
+ return check(tree.getType());
+ }
+ return Description.NO_MATCH;
+ }
+
+ @Override
+ public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) {
+ return check(tree);
+ }
+
+ Description check(Tree tree) {
+ PackageSymbol packageSymbol = ASTHelpers.enclosingPackage(ASTHelpers.getSymbol(tree));
+ return buildDescription(tree).setMessage(String.format("[%s]", packageSymbol)).build();
+ }
+ }
+
+ @Test
+ public void enclosingPackage() {
+ CompilationTestHelper.newInstance(EnclosingPackage.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ package p;
+ import java.util.function.Function;
+ import java.util.function.IntFunction;
+ import java.util.stream.Stream;
+ class Test {
+ // BUG: Diagnostic contains: [java.util.function]
+ Function, ?> f;
+ // BUG: Diagnostic contains: [p]
+ Test t;
+ {
+ // BUG: Diagnostic contains: []
+ Stream.of().toArray(IntFunction[]::new);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ /**
+ * Test checker to ensure that ASTHelpers.hasDirectAnnotationWithSimpleName handles type
+ * annotations.
+ */
+ @BugPattern(
+ severity = SeverityLevel.ERROR,
+ summary =
+ "Test checker to ensure that ASTHelpers.hasDirectAnnotationWithSimpleName handles type"
+ + " annotations")
+ public static class HasNullableAnnotationChecker extends BugChecker
+ implements MethodTreeMatcher, VariableTreeMatcher {
+ @Override
+ public Description matchVariable(VariableTree tree, VisitorState state) {
+ return match(tree, state);
+ }
+
+ @Override
+ public Description matchMethod(MethodTree tree, VisitorState state) {
+ return match(tree, state);
+ }
+
+ Description match(Tree tree, VisitorState state) {
+ if (ASTHelpers.hasDirectAnnotationWithSimpleName(ASTHelpers.getSymbol(tree), "Nullable")) {
+ return describeMatch(tree);
+ }
+ return Description.NO_MATCH;
+ }
+ }
+
+ @Test
+ public void testHasDirectOrTypeAnnotationWithSimpleName() {
+ CompilationTestHelper.newInstance(HasNullableAnnotationChecker.class, getClass())
+ .addSourceLines(
+ "Declaration.java",
+ """
+ import static java.lang.annotation.ElementType.METHOD;
+ import static java.lang.annotation.ElementType.FIELD;
+ import java.lang.annotation.Target;
+ class Declaration {
+ @Target({METHOD, FIELD})
+ @interface Nullable {}
+ }
+ """)
+ .addSourceLines(
+ "TypeUse.java",
+ """
+ import static java.lang.annotation.ElementType.TYPE_USE;
+ import java.lang.annotation.Target;
+ class TypeUse {
+ @Target(TYPE_USE)
+ @interface Nullable {}
+ }
+ """)
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ // BUG: Diagnostic contains:
+ @Declaration.Nullable public abstract Integer f();
+ // BUG: Diagnostic contains:
+ public abstract @TypeUse.Nullable Integer g();
+ public abstract Integer i();
+ // BUG: Diagnostic contains:
+ @Declaration.Nullable public Integer x;
+ // BUG: Diagnostic contains:
+ public @TypeUse.Nullable Integer y;
+ public Integer z;
+ }
+ """)
+ .doTest();
+ }
+}
diff --git a/check_api/src/test/java/com/google/errorprone/util/CommentsTest.java b/check_api/src/test/java/com/google/errorprone/util/CommentsTest.java
new file mode 100644
index 00000000000..087305ace71
--- /dev/null
+++ b/check_api/src/test/java/com/google/errorprone/util/CommentsTest.java
@@ -0,0 +1,595 @@
+/*
+ * Copyright 2017 The Error Prone Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.errorprone.util;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.BugPattern.SeverityLevel;
+import com.google.errorprone.CompilationTestHelper;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher;
+import com.google.errorprone.matchers.Description;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.source.tree.NewClassTree;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test code for {@code Comments} */
+@RunWith(JUnit4.class)
+public class CommentsTest {
+
+ /**
+ * A {@link BugChecker} that calls computeEndPosition for each invocation and prints the final
+ * line of source ending at that position
+ */
+ @BugPattern(
+ severity = SeverityLevel.ERROR,
+ summary = "Calls computeEndPosition and prints results")
+ public static class ComputeEndPosition extends BugChecker implements MethodInvocationTreeMatcher {
+
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ CharSequence sourceCode = state.getSourceCode();
+ Optional endPosition = Comments.computeEndPosition(tree, sourceCode, state);
+ if (!endPosition.isPresent()) {
+ return Description.NO_MATCH;
+ }
+ int startPosition = endPosition.get();
+ do {
+ startPosition--;
+ } while (sourceCode.charAt(startPosition) != '\n');
+
+ return buildDescription(tree)
+ .setMessage(sourceCode.subSequence(startPosition + 1, endPosition.get()).toString())
+ .build();
+ }
+ }
+
+ @Test
+ public void computeEndPosition_returnsEndOfInvocation_whenNoTrailingComment() {
+ CompilationTestHelper.newInstance(ComputeEndPosition.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(Object param);
+ private void test(Object param) {
+ // BUG: Diagnostic contains: target(param)
+ target(param);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void computeEndPosition_returnsEndOfNextStatement_whenTrailingComment() {
+ CompilationTestHelper.newInstance(ComputeEndPosition.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(Object param);
+ private void test(Object param) {
+ // BUG: Diagnostic contains: int i;
+ target(param); // 1
+ int i;
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void computeEndPosition_returnsEndOfBlock_whenLastWithTrailingComment() {
+ CompilationTestHelper.newInstance(ComputeEndPosition.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(Object param);
+ private void test(Object param) {
+ // BUG: Diagnostic contains: }
+ target(param); // 1
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ /** A {@link BugChecker} that prints the contents of comments around arguments */
+ @BugPattern(
+ severity = SeverityLevel.ERROR,
+ summary =
+ "Prints comments occurring around arguments. Matches calls to methods named "
+ + "'target' and all constructors")
+ public static class PrintCommentsForArguments extends BugChecker
+ implements MethodInvocationTreeMatcher, NewClassTreeMatcher {
+
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ if (!ASTHelpers.getSymbol(tree).getSimpleName().contentEquals("target")) {
+ return Description.NO_MATCH;
+ }
+ return buildDescription(tree)
+ .setMessage(commentsToString(Comments.findCommentsForArguments(tree, state)))
+ .build();
+ }
+
+ @Override
+ public Description matchNewClass(NewClassTree tree, VisitorState state) {
+ return buildDescription(tree)
+ .setMessage(commentsToString(Comments.findCommentsForArguments(tree, state)))
+ .build();
+ }
+
+ @SuppressWarnings("TreeToString") // explicitly want to strip comments in tree.
+ private static String commentsToString(ImmutableList> comments) {
+ return comments.stream()
+ .map(
+ c ->
+ Stream.of(
+ String.valueOf(getTextFromCommentList(c.beforeComments())),
+ String.valueOf(c.tree()),
+ String.valueOf(getTextFromCommentList(c.afterComments())))
+ .collect(Collectors.joining(" ")))
+ .collect(toImmutableList())
+ .toString();
+ }
+
+ private static ImmutableList getTextFromCommentList(
+ ImmutableList comments) {
+ return comments.stream().map(Comments::getTextFromComment).collect(toImmutableList());
+ }
+ }
+
+ @Test
+ public void findCommentsForArguments_findsBothComments_beforeAndAfter() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(Object param);
+ private void test(Object param) {
+ // BUG: Diagnostic contains: [[1] param [2]]
+ target(/* 1 */ param /* 2 */);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findCommentsForArguments_findsBothComments_before() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(Object param);
+ private void test(Object param) {
+ // BUG: Diagnostic contains: [[1, 2] param []]
+ target(/* 1 */ /* 2 */ param);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findCommentsForArguments_findsBothComments_after() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(Object param);
+ private void test(Object param) {
+ // BUG: Diagnostic contains: [[] param [1, 2]]
+ target(param /* 1 */ /* 2 */);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findCommentsForArguments_assignsToCorrectParameter_adjacentComments() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(Object param1, Object param2);
+ private void test(Object param1, Object param2) {
+ // BUG: Diagnostic contains: [[] param1 [1], [2] param2 []]
+ target(param1 /* 1 */, /* 2 */ param2);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findCommentsForArguments_findsNoComments_whenNoComments() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(Object param1, Object param2);
+ private void test(Object param1, Object param2) {
+ // BUG: Diagnostic contains: [[] param1 [], [] param2 []]
+ target(param1, param2);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findCommentsForArguments_findsNothing_whenNoArguments() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target();
+ private void test() {
+ // BUG: Diagnostic contains: []
+ target();
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findCommentsForArguments_assignToFirstParameter_withLineCommentAfterComma() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(Object param1, Object param2);
+ private void test(Object param1, Object param2) {
+ // BUG: Diagnostic contains: [[] param1 [1], [] param2 []]
+ target(param1, // 1
+ param2);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findCommentsForArguments_assignToFirstParameter_withBlockAfterComma() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(Object param1, Object param2);
+ private void test(Object param1, Object param2) {
+ // BUG: Diagnostic contains: [[] param1 [1], [] param2 []]
+ target(param1, /* 1 */
+ param2);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findCommentsForArguments_assignToSecondParameter_withLineCommentAfterMethod() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(Object param1, Object param2);
+ private void test(Object param1, Object param2) {
+ // BUG: Diagnostic contains: [[] param1 [], [] param2 [2]]
+ target(param1,
+ param2); // 2
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void
+ findCommentsForArguments_assignToSecondParameter_withLineCommentAfterMethodMidBlock() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(Object param1, Object param2);
+ private void test(Object param1, Object param2) {
+ // BUG: Diagnostic contains: [[] param1 [], [] param2 [2]]
+ target(param1,
+ param2); // 2
+ int i = 1;
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findCommentsForArguments_assignToSecondParameter_withLineCommentAfterField() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract Object target(Object param);
+ // BUG: Diagnostic contains: [[] null [1]]
+ private Object test = target(null); // 1
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findCommentsForArguments_findsComments_onConstructor() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ Test(Object param1, Object param2) {}
+ void test(Object param1, Object param2) {
+ // BUG: Diagnostic contains: [[1] param1 [], [] param2 [2]]
+ new Test(/* 1 */ param1, param2 /* 2 */);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findCommentsForArguments_attachesCommentToSecondArgument_whenCommentOnOwnLine() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ Test(Object param1, Object param2) {}
+ void test(Object param1, Object param2) {
+ // BUG: Diagnostic contains: [[] param1 [], [1] param2 []]
+ new Test(param1,
+ // 1
+ param2);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void
+ findCommentsForArguments_attachesCommentToArgument_whenCommentOnFollowingLineWithinCall() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ Test(Object param1) {}
+ void test(Object param1) {
+ // BUG: Diagnostic contains: [[] param1 [1]]
+ new Test(param1
+ // 1
+ );
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findCommentsForArguments_ignoresNextLineComment_withLineCommentAfterInvocation() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract Object target(Object param);
+ void test(Object param) {
+ // BUG: Diagnostic contains: [[] param [1]]
+ target(param); // 1
+ /* 2 */ int i;
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void
+ findCommentsForArguments_attachesCommentToSecondArgument_whenFollowedByTreeContainingComma() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract void target(Object param1, Object param2);
+ void test(Object param1, Object param2) {
+ // BUG: Diagnostic contains: [[] param1 [], [] param2 [1]]
+ target(param1, param2); // 1
+ // BUG: Diagnostic contains: [[] param1 [], [] param2 []]
+ target(param1, param2);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findCommentsForArguments_attachesCommentToFirstCall_whenMethodIsChained() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract Test chain(Object param1);
+ abstract void target(Object param2);
+ void test(Object param1, Object param2) {
+ // BUG: Diagnostic contains: [[] param2 []]
+ chain(/* 1 */ param1).target(param2);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findCommentsForArguments_noCommentOnOuterMethod_whenCommentOnNestedMethod() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract Object nested(Object param);
+ abstract void target(Object param);
+ void test(Object param) {
+ // BUG: Diagnostic contains: [[] nested(param) []]
+ target(nested(/* 1 */ param));
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findCommentsForArguments_findsCommentOnOuterMethodOnly_whenCommentOnNestedMethod() {
+ CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract Object nested(Object param);
+ abstract void target(Object param);
+ void test(Object param) {
+ // BUG: Diagnostic contains: [[1] nested(param) [4]]
+ target(/* 1 */ nested(/* 2 */ param /* 3 */) /* 4 */);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ /** A {@link BugChecker} that prints the source code at comment positions */
+ @BugPattern(
+ severity = SeverityLevel.ERROR,
+ summary =
+ "Prints the source code text which is under the comment position. Matches calls to "
+ + "methods called target and constructors only")
+ public static class PrintTextAtCommentPosition extends BugChecker
+ implements MethodInvocationTreeMatcher, NewClassTreeMatcher {
+
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ if (!ASTHelpers.getSymbol(tree).getSimpleName().contentEquals("target")) {
+ return Description.NO_MATCH;
+ }
+ return buildDescription(tree)
+ .setMessage(commentsToString(Comments.findCommentsForArguments(tree, state), state))
+ .build();
+ }
+
+ @Override
+ public Description matchNewClass(NewClassTree tree, VisitorState state) {
+ return buildDescription(tree)
+ .setMessage(commentsToString(Comments.findCommentsForArguments(tree, state), state))
+ .build();
+ }
+
+ private static String commentsToString(
+ ImmutableList> comments, VisitorState state) {
+ return comments.stream()
+ .map(
+ c ->
+ Stream.of(
+ String.valueOf(getSourceAtComment(c.beforeComments(), state)),
+ state.getSourceForNode(c.tree()),
+ String.valueOf(getSourceAtComment(c.afterComments(), state)))
+ .collect(Collectors.joining(" ")))
+ .collect(toImmutableList())
+ .toString();
+ }
+
+ private static ImmutableList getSourceAtComment(
+ ImmutableList comments, VisitorState state) {
+ return comments.stream()
+ .map(
+ c ->
+ state
+ .getSourceCode()
+ .subSequence(c.getSourcePos(0), c.getSourcePos(0) + c.getText().length())
+ .toString())
+ .collect(toImmutableList());
+ }
+ }
+
+ @Test
+ public void printTextAtCommentPosition_isCorrect_whenMethodIsChained() {
+ CompilationTestHelper.newInstance(PrintTextAtCommentPosition.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ abstract class Test {
+ abstract Test chain(Object param1);
+ abstract void target(Object param2);
+ void test(Object param1, Object param2) {
+ // BUG: Diagnostic contains: [[/* 1 */] param2 []]
+ chain(param1).target(/* 1 */ param2);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void printTextAtCommentPosition_isCorrect_onConstructor() {
+ CompilationTestHelper.newInstance(PrintTextAtCommentPosition.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ Test(Object param1, Object param2) {}
+ void test(Object param1, Object param2) {
+ // BUG: Diagnostic contains: [[/* 1 */] param1 [], [] param2 [/* 2 */]]
+ new Test(/* 1 */ param1, param2 /* 2 */);
+ }
+ }
+ """)
+ .doTest();
+ }
+}
diff --git a/check_api/src/test/java/com/google/errorprone/util/FindIdentifiersTest.java b/check_api/src/test/java/com/google/errorprone/util/FindIdentifiersTest.java
new file mode 100644
index 00000000000..8834b40282b
--- /dev/null
+++ b/check_api/src/test/java/com/google/errorprone/util/FindIdentifiersTest.java
@@ -0,0 +1,1135 @@
+/*
+ * Copyright 2016 The Error Prone Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.errorprone.util;
+
+import com.google.common.collect.ImmutableList;
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.BugPattern.SeverityLevel;
+import com.google.errorprone.CompilationTestHelper;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.MemberSelectTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
+import com.google.errorprone.matchers.Description;
+import com.google.errorprone.matchers.method.MethodMatchers;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.source.tree.MethodTree;
+import com.sun.tools.javac.code.Kinds.KindSelector;
+import com.sun.tools.javac.code.Symbol.MethodSymbol;
+import com.sun.tools.javac.code.Symbol.VarSymbol;
+import com.sun.tools.javac.code.Type;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link FindIdentifiers}. */
+@RunWith(JUnit4.class)
+public class FindIdentifiersTest {
+
+ /** A {@link BugChecker} that prints all identifiers in scope at a call to String.format(). */
+ @BugPattern(
+ severity = SeverityLevel.ERROR,
+ summary = "Prints all identifiers in scope at a call to String.format()")
+ public static class PrintIdents extends BugChecker implements MethodInvocationTreeMatcher {
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ if (MethodMatchers.staticMethod()
+ .onClass("java.lang.String")
+ .named("format")
+ .matches(tree, state)) {
+ return buildDescription(tree)
+ .setMessage(FindIdentifiers.findAllIdents(state).toString())
+ .build();
+ }
+ return Description.NO_MATCH;
+ }
+ }
+
+ @Test
+ public void findAllIdentsLocals() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private void doIt() {
+ String s1 = "";
+ String s2 = "";
+ // BUG: Diagnostic contains: [s1, s2]
+ String.format(s1 + s2);
+ String s3 = "";
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsLocalsEnclosedTreeNode() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private void doIt() {
+ String s1 = "";
+ String s2 = "";
+ if (true)
+ // BUG: Diagnostic contains: [s1, s2]
+ String.format(s1 + s2);
+ String s3 = "";
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsLocalsOuterScope() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private void doIt() {
+ String s1 = "";
+ while (true) {
+ // BUG: Diagnostic contains: [s1]
+ String.format(s1);
+ }
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsParams() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private void doIt(String s1, String s2) {
+ // BUG: Diagnostic contains: [s1, s2]
+ String.format(s1 + s2);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsMixedLocalsAndParams() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private void doIt(String s1) {
+ String s2 = s1;
+ while (true) {
+ String s3 = s1;
+ // BUG: Diagnostic contains: [s3, s2, s1]
+ String.format(s3 + s2 + s1);
+ String s4 = s1;
+ }
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsFields() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private static String s1;
+ private String s2;
+ private void doIt() {
+ // BUG: Diagnostic contains: [s1, s2]
+ String.format(s1 + s2);
+ }
+ private static void staticDoIt() {
+ // BUG: Diagnostic contains: [s1]
+ String.format(s1);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsInheritedFieldsSamePackage() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "pkg/Super.java",
+ """
+ package pkg;
+ public class Super {
+ public String s1;
+ protected String s2;
+ String s3;
+ private String s4;
+ public static String s5;
+ protected static String s6;
+ static String s7;
+ private static String s8;
+ }
+ """)
+ .addSourceLines(
+ "pkg/Sub.java",
+ """
+ package pkg;
+ public class Sub extends Super {
+ private void doIt() {
+ // BUG: Diagnostic contains: [s1, s2, s3, s5, s6, s7]
+ String.format(s1 + s2 + s3 + s5 + s6 + s7);
+ }
+ private static void doItStatically() {
+ // BUG: Diagnostic contains: [s5, s6, s7]
+ String.format(s5 + s6 + s7);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsInheritedFieldsDifferentPackage() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "pkg1/Super.java",
+ """
+ package pkg1;
+ public class Super {
+ public String s1;
+ protected String s2;
+ String s3;
+ private String s4;
+ public static String s5;
+ protected static String s6;
+ static String s7;
+ private static String s8;
+ }
+ """)
+ .addSourceLines(
+ "pkg2/Sub.java",
+ """
+ package pkg2;
+ import pkg1.Super;
+ public class Sub extends Super {
+ private void doIt() {
+ // BUG: Diagnostic contains: [s1, s2, s5, s6]
+ String.format(s1 + s2 + s5 + s6);
+ }
+ private static void doItStatically() {
+ // BUG: Diagnostic contains: [s5, s6]
+ String.format(s5 + s6);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsInheritedFieldsSameTopLevel() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Holder.java",
+ """
+ public class Holder {
+ class Super {
+ private String s1;
+ }
+ class Sub extends Super {
+ private String s2;
+ private void doIt() {
+ // BUG: Diagnostic contains: [s2]
+ String.format(s2);
+ }
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsStaticNestedClass() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Outer.java",
+ """
+ class Outer {
+ private String s1;
+ private static String s2;
+ static class Inner {
+ private String s3;
+ private static final String s4 = "s4";
+ private void doIt() {
+ // BUG: Diagnostic contains: [s3, s4, s2]
+ String.format(s3 + s4 + s2);
+ }
+ private static void doItStatically() {
+ // BUG: Diagnostic contains: [s4, s2]
+ String.format(s4 + s2);
+ }
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsInnerClass() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Outer.java",
+ """
+ class Outer {
+ private String s1;
+ private static String s2;
+ class Inner {
+ private String s3;
+ private static final String s4 = "s4";
+ class EvenMoreInner {
+ private String s5;
+ private static final String s6 = "s6";
+ private void doIt() {
+ // BUG: Diagnostic contains: [s5, s6, s3, s4, s1, s2]
+ String.format(s5 + s6 + s3 + s4 + s1 + s2);
+ }
+ }
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsStaticMethodWithLocalClass() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private String s1;
+ private static String s2;
+ private static void doIt() {
+ class Helper {
+ private String s3;
+ private static final String s4 = "s6";
+ void reallyDoIt() {
+ // BUG: Diagnostic contains: [s3, s4, s2]
+ String.format(s4 + s3 + s2);
+ }
+ }
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsStaticLambda() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private String s1;
+ private static String s2;
+ // BUG: Diagnostic contains: [s2]
+ private static Runnable r = () -> String.format(s2);
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsStaticLambdaWithLocalClass() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private String s1;
+ private static String s2;
+ private static Runnable doIt = () -> {
+ class Helper {
+ private String s3;
+ void reallyDoIt() {
+ // BUG: Diagnostic contains: [s3, s2]
+ String.format(s3 + s2);
+ }
+ }
+ };
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsInnerClassWhereTopLevelExtends() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "ToExtend.java",
+ """
+ class ToExtend {
+ String s1;
+ }
+ """)
+ .addSourceLines(
+ "Outer.java",
+ """
+ class Outer extends ToExtend {
+ private String s2;
+ class Inner {
+ void doIt() {
+ // BUG: Diagnostic contains: [s2, s1]
+ String.format(s2 + s1);
+ }
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsMixedStaticAndNonStaticNestedClasses() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Outer.java",
+ """
+ class Outer {
+ String s1;
+ static class Inner {
+ String s2;
+ class EvenMoreInner {
+ String s3;
+ void doIt() {
+ // BUG: Diagnostic contains: [s3, s2]
+ String.format(s3 + s2);
+ }
+ }
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsAnonymousClass() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ public String s1;
+ private void doIt(final String s2) {
+ String s3 = "";
+ String s4 = "";
+ new Thread(
+ new Runnable() {
+ @Override public void run() {
+ String s5 = "";
+ s5 = "foo";
+ // BUG: Diagnostic contains: [s5, s3, s2, s1]
+ String.format(s5 + s3 + s2 + s1);
+ }
+ }
+ ).start();
+ s4 = "foo";
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsLambda() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ public String s1;
+ private void doIt(final String s2) {
+ String s3 = "";
+ String s4 = "";
+ // BUG: Diagnostic contains: [s3, s2, s1]
+ new Thread(() -> String.format(s3 + s2 + s1)).start();
+ s4 = "foo";
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsLambdaWithParams() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ import java.util.function.Function;
+ class Test {
+ public String s1;
+ private void doIt(final String s2) {
+ String s3 = "";
+ String s4 = "";
+ // BUG: Diagnostic contains: [name, s3, s2, s1]
+ Function f = (String name) -> String.format(name + s3 + s2 + s1);
+ s4 = "foo";
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsExceptionParameter() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ public String s1;
+ private void doIt(final String s2) {
+ String s3 = "";
+ try {
+ s3 = s1;
+ } catch (Exception e) {
+ // BUG: Diagnostic contains: [e, s3, s2, s1]
+ String.format(e + s3 + s2 + s1);
+ }
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsLocalClass() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private void doIt() {
+ String s1 = "";
+ final String s2 = "";
+ String s3 = "";
+ class Local {
+ public void doIt() {
+ // BUG: Diagnostic contains: [s1, s2]
+ String.format(s2 + s1);
+ }
+ }
+ s3 = "foo";
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsStaticInitializer() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ public static String s1;
+ public String s2;
+ static {
+ // BUG: Diagnostic contains: [s1]
+ String.format(s1);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsStaticVariableInitializer() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ public static String s1;
+ public String s2;
+ // BUG: Diagnostic contains: [s1]
+ public static String s3 = String.format(s1);
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsExplicitConstructorInvocation() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private String s1;
+ private static String s2;
+ public Test(String s1) {
+ this.s1 = s1;
+ }
+ public Test() {
+ // BUG: Diagnostic contains: [s2]
+ this(String.format(s2));
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsExplicitConstructorInvocation2() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Super.java",
+ """
+ class Super {
+ protected String s1;
+ protected static String s2;
+ public Super(String s1) {
+ this.s1 = s1;
+ }
+ }
+ """)
+ .addSourceLines(
+ "Sub.java",
+ """
+ class Sub extends Super {
+ public Sub(String s3) {
+ // BUG: Diagnostic contains: [s3, s2]
+ super(String.format(s3 + s2));
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsInterface() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "MyInterface.java",
+ """
+ interface MyInterface {
+ String EMPTY_STRING = "";
+ }
+ """)
+ .addSourceLines(
+ "MyImpl.java",
+ """
+ class MyImpl implements MyInterface {
+ void doIt() {
+ // BUG: Diagnostic contains: [EMPTY_STRING]
+ String.format(EMPTY_STRING);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsEnumConstant() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "MyEnum.java",
+ """
+ enum MyEnum {
+ FOO,
+ BAR,
+ BAZ;
+ static class Nested {
+ void doIt() {
+ // BUG: Diagnostic contains: [FOO, BAR, BAZ]
+ String.format(FOO.toString() + BAR.toString() + BAZ.toString());
+ }
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsForLoop() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ void doIt() {
+ for (int i = 0; i < 10; i++) {
+ // BUG: Diagnostic contains: [i]
+ String.format(Integer.toString(i));
+ }
+ for (int j : new int[]{0, 1, 2}) {
+ // BUG: Diagnostic contains: [j]
+ String.format(Integer.toString(j));
+ }
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsTryWithResources() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.*;
+class Test {
+ void doIt() {
+ try (
+ BufferedReader reader = Files.newBufferedReader(Paths.get("foo"));
+ // BUG: Diagnostic contains: [reader]
+ InputStream is = new ByteArrayInputStream(String.format(reader.readLine()).getBytes(StandardCharsets.UTF_8))
+ ) {
+ // BUG: Diagnostic contains: [reader, is]
+ String.format(reader.readLine() + is.toString());
+ } catch (IOException e) {
+ // BUG: Diagnostic contains: [e]
+ String.format(e.toString());
+ }
+ }
+}
+""")
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsStaticImport() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ import static java.nio.charset.StandardCharsets.UTF_8;
+ class Test {
+ void doIt() {
+ // BUG: Diagnostic contains: [UTF_8]
+ String.format(UTF_8.toString());
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findAllIdentsInheritedStaticImport() {
+ CompilationTestHelper.newInstance(PrintIdents.class, getClass())
+ .addSourceLines(
+ "pkg/MyInterface.java",
+ """
+ package pkg;
+ public interface MyInterface {
+ String EMPTY = "";
+ }
+ """)
+ .addSourceLines(
+ "pkg/Super.java",
+ """
+ package pkg;
+ public class Super {
+ public static final String FOO = "foo";
+ }
+ """)
+ .addSourceLines(
+ "pkg/Impl.java",
+ """
+ package pkg;
+ public class Impl extends Super implements MyInterface {}
+ """)
+ .addSourceLines(
+ "Test.java",
+ """
+ import static pkg.Impl.EMPTY;
+ import static pkg.Impl.FOO;
+ public class Test {
+ void doIt() {
+ // BUG: Diagnostic contains: [EMPTY, FOO]
+ String.format(EMPTY + FOO);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ /** A {@link BugChecker} that prints all identifiers in scope at a method declaration. */
+ @BugPattern(
+ severity = SeverityLevel.ERROR,
+ summary = "Prints all identifiers in scope at a method declaration.")
+ public static class PrintIdentsAtMethodDeclaration extends BugChecker
+ implements MethodTreeMatcher {
+ @Override
+ public Description matchMethod(MethodTree tree, VisitorState state) {
+ return buildDescription(tree)
+ .setMessage(FindIdentifiers.findAllIdents(state).toString())
+ .build();
+ }
+ }
+
+ @Test
+ public void findAllIdentsAtDoItMethod() {
+ CompilationTestHelper.newInstance(PrintIdentsAtMethodDeclaration.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private String s1;
+ private String s2;
+ // BUG: Diagnostic contains: [s1, s2]
+ private void doIt() {}
+ }
+ """)
+ .doTest();
+ }
+
+ /**
+ * A {@link BugChecker} that prints all unused variables in scope at a call to String.format().
+ */
+ @BugPattern(
+ severity = SeverityLevel.ERROR,
+ summary = "Prints all unused variabled in scope at a call to String.format()")
+ public static class PrintUnusedVariables extends BugChecker
+ implements MethodInvocationTreeMatcher {
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ if (MethodMatchers.staticMethod()
+ .onClass("java.lang.String")
+ .named("format")
+ .matches(tree, state)) {
+ return buildDescription(tree)
+ .setMessage(FindIdentifiers.findUnusedIdentifiers(state).toString())
+ .build();
+ }
+ return Description.NO_MATCH;
+ }
+ }
+
+ @Test
+ public void returnsVariable_findUnusedVariables_whenVariableUnusedInBlock() {
+ CompilationTestHelper.newInstance(PrintUnusedVariables.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private void doIt() {
+ String a = null;
+ // BUG: Diagnostic contains: [a]
+ String.format(a);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void ignoresVariable_findUnusedVariables_whenVariableUsedAsArgumentInBlock() {
+ CompilationTestHelper.newInstance(PrintUnusedVariables.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ void test(String arg) {}
+ private void doIt() {
+ String a = null;
+ test(a);
+ // BUG: Diagnostic contains: []
+ String.format(null);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void ignoresVariable_findUnusedVariables_whenVariableUsedInExpressionInBlock() {
+ CompilationTestHelper.newInstance(PrintUnusedVariables.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ void test(String arg) {}
+ private void doIt() {
+ String a = null;
+ if (a == null) {}
+ // BUG: Diagnostic contains: []
+ String.format(null);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void returnsVariable_findUnusedVariables_whenVariableUsedLaterInBlock() {
+ CompilationTestHelper.newInstance(PrintUnusedVariables.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ void test(String arg) {}
+ private void doIt() {
+ String a = null;
+ // BUG: Diagnostic contains: [a]
+ String.format(null);
+ test(a);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void returnsVariable_findUnusedVariables_whenVariableUnusedButSameNameAsField() {
+ CompilationTestHelper.newInstance(PrintUnusedVariables.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ String a = null;
+ void test(String arg) {}
+ private void doIt() {
+ test(a);
+ String a = null;
+ // BUG: Diagnostic contains: [a]
+ String.format(null);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void returnsVariable_findUnusedVariables_whenVariableDefinedInEnhancedFor() {
+ CompilationTestHelper.newInstance(PrintUnusedVariables.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private void doIt() {
+ for(String a : new String[] {}) {
+ // BUG: Diagnostic contains: [a]
+ String.format(a);
+ }
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void returnsVariable_findUnusedVariables_whenLaterUseInAssignment() {
+ CompilationTestHelper.newInstance(PrintUnusedVariables.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private void doIt() {
+ String a = null;
+ // BUG: Diagnostic contains: [a]
+ String b = String.format(a);
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ /** A {@link BugChecker} that prints all fields in receiver class on method invocations. */
+ @BugPattern(
+ severity = SeverityLevel.ERROR,
+ summary = "Prints all fields in receivers of method invocations")
+ public static class PrintFields extends BugChecker implements MethodInvocationTreeMatcher {
+ @Override
+ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+ MethodSymbol symbol = ASTHelpers.getSymbol(tree);
+ Type receiverType = symbol.owner.asType();
+ ImmutableList fields = FindIdentifiers.findAllFields(receiverType, state);
+ return buildDescription(tree).setMessage(fields.toString()).build();
+ }
+ }
+
+ @Test
+ public void findsStaticAndInstanceFields_findAllFields() {
+ CompilationTestHelper.newInstance(PrintFields.class, getClass())
+ .addSourceLines(
+ "Reference.java",
+ """
+ class Reference {
+ static String staticField;
+ String instanceField;
+ static void test() {}
+ }
+ """)
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private void doIt() {
+ // BUG: Diagnostic contains: [staticField, instanceField]
+ Reference.test();
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void findsInheritedStaticAndInstanceFields_findAllFields() {
+ CompilationTestHelper.newInstance(PrintFields.class, getClass())
+ .addSourceLines(
+ "Super.java",
+ """
+ class Super {
+ static String staticField;
+ String instanceField;
+ }
+ """)
+ .addSourceLines(
+ "Reference.java",
+ """
+ class Reference extends Super {
+ static void test() {}
+ }
+ """)
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ private void doIt() {
+ // BUG: Diagnostic contains: [staticField, instanceField]
+ Reference.test();
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ /** A {@link BugChecker} that prints whether {@code A} is visible on each member select. */
+ @BugPattern(severity = SeverityLevel.ERROR, summary = "A is visible")
+ public static class IsAVisible extends BugChecker implements MemberSelectTreeMatcher {
+ @Override
+ public Description matchMemberSelect(MemberSelectTree tree, VisitorState state) {
+ return FindIdentifiers.findIdent("A", state, KindSelector.TYP) == null
+ ? Description.NO_MATCH
+ : describeMatch(tree);
+ }
+ }
+
+ @Test
+ public void aNotVisibleOutsideClass() {
+ CompilationTestHelper.newInstance(IsAVisible.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test extends java.lang.Object {
+ // BUG: Diagnostic contains:
+ Test.A foo() {
+ return null;
+ }
+ class A {}
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void aVisibleOutsideClass() {
+ CompilationTestHelper.newInstance(IsAVisible.class, getClass())
+ .addSourceLines(
+ "A.java",
+ """
+ package pkg;
+ class A {}
+ """)
+ .addSourceLines(
+ "Test.java",
+ """
+ package pkg;
+ // BUG: Diagnostic contains:
+ class Test extends java.lang.Object {
+ // BUG: Diagnostic contains:
+ pkg.A foo() {
+ return null;
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ /** A {@link BugChecker} that prints whether {@code A} is visible on each class tree. */
+ @BugPattern(severity = SeverityLevel.ERROR, summary = "A is visible on ClassTree")
+ public static class IsAVisibleOnClassTree extends BugChecker implements ClassTreeMatcher {
+ @Override
+ public Description matchClass(ClassTree tree, VisitorState state) {
+ return FindIdentifiers.findIdent("A", state, KindSelector.TYP) == null
+ ? Description.NO_MATCH
+ : describeMatch(tree);
+ }
+ }
+
+ @Test
+ public void aVisibleOnClassTree() {
+ CompilationTestHelper.newInstance(IsAVisibleOnClassTree.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ // BUG: Diagnostic contains:
+ class Test {
+ // BUG: Diagnostic contains:
+ class A {}
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void aNotVisibleOnClassTree() {
+ CompilationTestHelper.newInstance(IsAVisibleOnClassTree.class, getClass())
+ .addSourceLines(
+ "Test.java",
+ """
+ class Test {
+ // BUG: Diagnostic contains:
+ class Sub {
+ // BUG: Diagnostic contains:
+ class A {}
+ }
+ }
+ """)
+ .doTest();
+ }
+}
diff --git a/check_api/src/test/java/com/google/errorprone/util/MoreAnnotationsTest.java b/check_api/src/test/java/com/google/errorprone/util/MoreAnnotationsTest.java
new file mode 100644
index 00000000000..3c8bdeef03a
--- /dev/null
+++ b/check_api/src/test/java/com/google/errorprone/util/MoreAnnotationsTest.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2018 The Error Prone Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.errorprone.util;
+
+import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
+import static com.google.errorprone.matchers.Description.NO_MATCH;
+import static java.util.stream.Collectors.joining;
+
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.CompilationTestHelper;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
+import com.google.errorprone.matchers.Description;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.MethodTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.VariableTree;
+import com.sun.tools.javac.code.Attribute.Compound;
+import com.sun.tools.javac.code.Attribute.TypeCompound;
+import com.sun.tools.javac.code.Symbol;
+import java.util.stream.Stream;
+import javax.lang.model.element.ElementKind;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** {@link MoreAnnotations}Test */
+@RunWith(JUnit4.class)
+public final class MoreAnnotationsTest {
+
+ abstract static class MoreAnnotationsTester extends BugChecker
+ implements ClassTreeMatcher, MethodTreeMatcher, VariableTreeMatcher {
+
+ private Description process(Tree tree) {
+ Symbol sym = ASTHelpers.getSymbol(tree);
+ if (sym == null) {
+ return NO_MATCH;
+ }
+ if (sym.getKind() == ElementKind.ANNOTATION_TYPE) {
+ return NO_MATCH;
+ }
+ String annos =
+ getAnnotations(sym)
+ .map(c -> c.type.asElement().getSimpleName().toString())
+ .collect(joining(", "));
+ if (annos.isEmpty()) {
+ return NO_MATCH;
+ }
+ return buildDescription(tree).setMessage(annos).build();
+ }
+
+ protected abstract Stream extends Compound> getAnnotations(Symbol sym);
+
+ @Override
+ public Description matchClass(ClassTree tree, VisitorState state) {
+ return process(tree);
+ }
+
+ @Override
+ public Description matchMethod(MethodTree tree, VisitorState state) {
+ return process(tree);
+ }
+
+ @Override
+ public Description matchVariable(VariableTree tree, VisitorState state) {
+ return process(tree);
+ }
+ }
+
+ @BugPattern(summary = "A test checker.", severity = ERROR)
+ public static class GetDeclarationAndTypeAttributesTester extends MoreAnnotationsTester {
+ @Override
+ protected Stream getAnnotations(Symbol sym) {
+ return MoreAnnotations.getDeclarationAndTypeAttributes(sym);
+ }
+ }
+
+ @BugPattern(summary = "A test checker.", severity = ERROR)
+ public static class GetTopLevelTypeAttributesTester extends MoreAnnotationsTester {
+ @Override
+ protected Stream getAnnotations(Symbol sym) {
+ return MoreAnnotations.getTopLevelTypeAttributes(sym);
+ }
+ }
+
+ @Test
+ public void getDeclarationAndTypeAttributesTest() {
+ CompilationTestHelper.newInstance(GetDeclarationAndTypeAttributesTester.class, getClass())
+ .addSourceLines(
+ "Annos.java",
+ """
+import static java.lang.annotation.ElementType.*;
+import java.lang.annotation.Target;
+@Target(TYPE_USE)
+@interface Other {}
+@Target(TYPE_USE)
+@interface TA {}
+@Target({TYPE, CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PARAMETER, TYPE_PARAMETER})
+@interface DA {}
+@Target({TYPE, TYPE_USE, CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PARAMETER, TYPE_PARAMETER})
+@interface A {}
+""")
+ .addSourceLines(
+ "Test.java",
+ """
+ import java.util.List;
+ // BUG: Diagnostic contains: DA, A
+ @DA @A class Test {
+ // BUG: Diagnostic contains: DA, A, TA
+ @TA @DA @A Test() {}
+ // BUG: Diagnostic contains: DA, A, TA
+ @TA @DA @A List<@Other String> field;
+ {
+ // BUG: Diagnostic contains: DA, A, TA
+ @TA @DA @A List<@Other String> local;
+ }
+ // BUG: Diagnostic contains: DA, A, TA
+ @TA @DA @A List<@Other String> f(
+ // BUG: Diagnostic contains: DA, A, TA
+ @TA @DA @A List<@Other String> param) {
+ return null;
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void getTopLevelTypeAttributesTest() {
+ CompilationTestHelper.newInstance(GetTopLevelTypeAttributesTester.class, getClass())
+ .addSourceLines(
+ "Annos.java",
+ """
+import static java.lang.annotation.ElementType.*;
+import java.lang.annotation.Target;
+@Target(TYPE_USE)
+@interface Other {}
+@Target(TYPE_USE)
+@interface TA {}
+@Target({TYPE, CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PARAMETER, TYPE_PARAMETER})
+@interface DA {}
+@Target({TYPE, TYPE_USE, CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PARAMETER, TYPE_PARAMETER})
+@interface A {}
+""")
+ .addSourceLines(
+ "Test.java",
+ """
+ import java.util.List;
+ @DA @A class Test {
+ // BUG: Diagnostic contains: TA, A
+ @TA @DA @A Test() {}
+ // BUG: Diagnostic contains: TA, A
+ @TA @DA @A List<@Other String> field;
+ {
+ // BUG: Diagnostic contains: TA, A
+ @TA @DA @A List<@Other String> local;
+ }
+ // BUG: Diagnostic contains: TA, A
+ @TA @DA @A List<@Other String> f(
+ // BUG: Diagnostic contains: TA, A
+ @TA @DA @A List<@Other String> param) {
+ return null;
+ }
+ }
+ """)
+ .doTest();
+ }
+
+ @Test
+ public void nestedClassesTest() {
+ CompilationTestHelper.newInstance(GetTopLevelTypeAttributesTester.class, getClass())
+ .addSourceLines(
+ "Annos.java",
+ """
+ import static java.lang.annotation.ElementType.TYPE_USE;
+ import java.lang.annotation.Target;
+ @Target(TYPE_USE) @interface A {}
+ @Target(TYPE_USE) @interface B {}
+ @Target(TYPE_USE) @interface C {}
+ """)
+ .addSourceLines(
+ "Test.java",
+ """
+ import java.util.List;
+ abstract class Outer {
+ class Middle {
+ class Inner {}
+ }
+ class MiddleStatic {
+ class Inner {}
+ class InnerStatic {}
+ }
+ // BUG: Diagnostic contains: C
+ @A Outer . @B Middle . @C Inner x;
+ // BUG: Diagnostic contains: B
+ Outer . @A MiddleStatic . @B Inner y;
+ // BUG: Diagnostic contains: A
+ Outer . MiddleStatic . @A InnerStatic z;
+ // BUG: Diagnostic contains: C
+ abstract @A Outer . @B Middle . @C Inner f();
+ // BUG: Diagnostic contains: B
+ abstract Outer . @A MiddleStatic . @B Inner g();
+ // BUG: Diagnostic contains: A
+ abstract Outer . MiddleStatic . @A InnerStatic h();
+ }
+ """)
+ .doTest();
+ }
+}
diff --git a/check_api/src/test/java/com/google/errorprone/util/ReachabilityTest.java b/check_api/src/test/java/com/google/errorprone/util/ReachabilityTest.java
new file mode 100644
index 00000000000..3189312ab1a
--- /dev/null
+++ b/check_api/src/test/java/com/google/errorprone/util/ReachabilityTest.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright 2017 The Error Prone Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.errorprone.util;
+
+import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
+import static com.google.errorprone.matchers.Description.NO_MATCH;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Iterables;
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.CompilationTestHelper;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.SwitchTreeMatcher;
+import com.google.errorprone.matchers.Description;
+import com.sun.source.tree.SwitchTree;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** {@link Reachability}Test. */
+@RunWith(Parameterized.class)
+public class ReachabilityTest {
+
+ /** Reports an error if the first case in a switch falls through to the second. */
+ @BugPattern(summary = "", severity = ERROR)
+ public static class FirstCaseFallsThrough extends BugChecker implements SwitchTreeMatcher {
+
+ @Override
+ public Description matchSwitch(SwitchTree tree, VisitorState state) {
+ if (tree.getCases().size() != 2 || tree.getCases().get(0).getStatements().isEmpty()) {
+ return NO_MATCH;
+ }
+ return Reachability.canCompleteNormally(
+ Iterables.getLast(tree.getCases().get(0).getStatements()))
+ ? describeMatch(tree.getCases().get(1))
+ : NO_MATCH;
+ }
+ }
+
+ private final String[] lines;
+
+ public ReachabilityTest(String[] lines) {
+ this.lines = lines;
+ }
+
+ @Test
+ public void test() {
+ CompilationTestHelper.newInstance(FirstCaseFallsThrough.class, getClass())
+ .addSourceLines(
+ "in/Test.java",
+ "import java.io.*;",
+ "import java.nio.file.*;",
+ "class Test {",
+ " void f(int x) {",
+ " switch (x) {",
+ " case 1:",
+ Joiner.on('\n').join(lines),
+ " default:",
+ " break;",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Parameters
+ public static List parameters() {
+ String[][] parameters = {
+ {
+ "int a = 1;", //
+ "int b = 2;",
+ "break;",
+ },
+ {
+ "System.err.println();", //
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "int a = 1;",
+ "int b = 2;",
+ "class L {}",
+ ";;",
+ "assert false;",
+ "label: System.err.println();",
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "if (true) {", //
+ "} else {",
+ " break;",
+ "}",
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "if (true) {", //
+ " break;",
+ "}",
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "if (true) {", //
+ " break;",
+ "} else {",
+ "}",
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "if (true) {", //
+ " break;",
+ "} else {",
+ " break;",
+ "}",
+ },
+ {
+ "switch (42) {", //
+ " case 0:",
+ " case 1:",
+ " case 2:",
+ "}",
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "switch (42) {", //
+ "}",
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "switch (42) {",
+ " case 0:",
+ " break;",
+ " case 1:",
+ "}",
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "switch (42) {", //
+ " default:",
+ " break;",
+ "}",
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "switch (42) {", //
+ " default:",
+ " return;",
+ "}",
+ },
+ {
+ "while (true) {", //
+ "}",
+ },
+ {
+ "while (true) {", //
+ " break;",
+ "}",
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "while (true) {", //
+ " return;",
+ "}",
+ },
+ {
+ "while (x == 0) {}", //
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "loop: do {", //
+ " continue loop;",
+ "} while (x == 0);",
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "loop: do {", //
+ " continue loop;",
+ "} while (true);",
+ },
+ {
+ "int i = 1;",
+ "loop: do {",
+ " i++;",
+ " continue loop;",
+ "} while (i == 0);",
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "try {",
+ " Files.readAllBytes(Paths.get(\"file\"));",
+ " return;",
+ "} catch (IOException e) {",
+ " return;",
+ "}",
+ },
+ {
+ "try {",
+ " try {",
+ " Files.readAllBytes(Paths.get(\"file\"));",
+ " return;",
+ " } catch (NullPointerException e) {",
+ " return;",
+ " }",
+ "} catch (IOException e) {",
+ " return;",
+ "}",
+ },
+ {
+ "try {",
+ " Files.readAllBytes(Paths.get(\"file\"));",
+ " return;",
+ "} catch (IOException e) {",
+ " return;",
+ "} finally {",
+ "}",
+ },
+ {
+ "try {", //
+ " //",
+ "} catch (Throwable t) {",
+ " return;",
+ "} finally {",
+ " return;",
+ "}",
+ },
+ {
+ "try {", //
+ " return;",
+ "} catch (Throwable t) {",
+ " //",
+ "} finally {",
+ " return;",
+ "}",
+ },
+ {
+ "int y = 0;",
+ "while (true) {",
+ " if (y++ > 10) {",
+ " return;",
+ " }",
+ " if (y-- < 10) {",
+ " return;",
+ " }",
+ "}",
+ },
+ {
+ "int y = 0;",
+ "while (true) {",
+ " do {",
+ " switch (y) {",
+ " case 0:",
+ " continue;", // continue target is do/while, not switch or outer while
+ " }",
+ " } while (y > 0);",
+ " break;",
+ "}",
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "try {",
+ " if (Files.readAllBytes(Paths.get(\"file\")) != null) {}",
+ " return;",
+ "} catch (IOException e) {",
+ "}",
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "try {",
+ " Files.readAllBytes(Paths.get(\"file\"));",
+ "} catch (IOException e) {",
+ " throw new IOError(e);",
+ "}",
+ "// BUG: Diagnostic contains:",
+ },
+ {
+ "try {",
+ " Files.readAllBytes(Paths.get(\"file\"));",
+ " return;",
+ "} catch (IOException e) {",
+ " throw new IOError(e);",
+ "}",
+ },
+ {
+ "throw new AssertionError();",
+ },
+ {
+ "for (;;) {}",
+ },
+ {
+ "System.exit(1);", //
+ },
+ {
+ "{", //
+ " System.exit(1);",
+ " throw new AssertionError();",
+ "}",
+ },
+ {
+ "l:", //
+ "do {",
+ " break l;",
+ "} while (true);",
+ "System.err.println();",
+ "// BUG: Diagnostic contains:",
+ },
+ };
+ return Arrays.stream(parameters).map(x -> new Object[] {x}).collect(toList());
+ }
+}