diff --git a/check_api/pom.xml b/check_api/pom.xml index 8a24c1a48fd..061e83e0e6b 100644 --- a/check_api/pom.xml +++ b/check_api/pom.xml @@ -162,6 +162,15 @@ ${autoservice.version} + + + **/ASTHelpersFindSuperMethodsTest.java + **/ASTHelpersTest.java + **/CommentsTest.java + **/FindIdentifiersTest.java + **/MoreAnnotationsTest.java + **/ReachabilityTest.java + diff --git a/check_api/src/test/java/com/google/errorprone/util/ASTHelpersFindSuperMethodsTest.java b/check_api/src/test/java/com/google/errorprone/util/ASTHelpersFindSuperMethodsTest.java new file mode 100644 index 00000000000..980f1ddc2ff --- /dev/null +++ b/check_api/src/test/java/com/google/errorprone/util/ASTHelpersFindSuperMethodsTest.java @@ -0,0 +1,229 @@ +/* + * 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.truth.Truth.assertThat; + +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Table; +import com.google.errorprone.VisitorState; +import com.google.errorprone.matchers.CompilerBasedAbstractTest; +import com.google.errorprone.scanner.Scanner; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Types; +import java.util.Optional; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Test cases for {@link ASTHelpers#findSuperMethod(MethodSymbol, Types)} and {@link + * ASTHelpers#findSuperMethod(MethodSymbol, Types)}. + * + * @author Ɓukasz Hanuszczak (hanuszczak@google.com) + */ +@RunWith(JUnit4.class) +public final class ASTHelpersFindSuperMethodsTest extends CompilerBasedAbstractTest { + + private FindSuperMethodsTestScanner scanner; + + @Before + public void prepareScanClassHierarchy() { + writeFile( + "Foo.java", + """ + abstract class Foo { + public abstract void foo(); + } + """); + writeFile( + "Bar.java", + """ + class Bar extends Foo { + @Override + public void foo() { + System.out.println("bar"); + } + } + """); + writeFile( + "Baz.java", + """ + class Baz extends Bar { + @Override + public void foo() { + System.out.println("baz"); + } + } + """); + writeFile( + "Quux.java", + """ + class Quux extends Baz { + public void foo(String string) { + System.out.println("I am not an override! " + string); + } + + public int bar(int x, int y) { + return x * y; + } + } + """); + writeFile( + "Norf.java", + """ + class Norf extends Quux { + @Override + public void foo() { + System.out.println("norf"); + } + + @Override + public int bar(int x, int y) { + return super.bar(x, y) + 42; + } + } + """); + + scanner = new FindSuperMethodsTestScanner(); + assertCompiles(scanner); + } + + @Test + public void findSuperMethods_findsSingleMethod() { + MethodSymbol barOfNorf = scanner.getMethod("Norf", "bar"); + MethodSymbol barOfQuux = scanner.getMethod("Quux", "bar"); + assertThat(findSuperMethods(barOfNorf)).containsExactly(barOfQuux); + } + + @Test + public void findSuperMethods_findsAllMethodsInTheHierarchy() { + MethodSymbol fooOfNorf = scanner.getMethod("Norf", "foo"); + MethodSymbol fooOfBaz = scanner.getMethod("Baz", "foo"); + MethodSymbol fooOfBar = scanner.getMethod("Bar", "foo"); + MethodSymbol fooOfQuux = scanner.getMethod("Foo", "foo"); + assertThat(findSuperMethods(fooOfNorf)) + .containsExactly(fooOfBaz, fooOfBar, fooOfQuux) + .inOrder(); + } + + @Test + public void findSuperMethod_findsNothingForAbstractMethod() { + MethodSymbol fooOfFoo = scanner.getMethod("Foo", "foo"); + assertThat(findSuperMethod(fooOfFoo)).isEqualTo(Optional.empty()); + } + + @Test + public void findSuperMethod_findsNothingForNewNonAbstractMethod() { + MethodSymbol barOfQuux = scanner.getMethod("Quux", "bar"); + assertThat(findSuperMethod(barOfQuux)).isEqualTo(Optional.empty()); + } + + @Test + public void findSuperMethod_findsAbstractSuperMethod() { + MethodSymbol fooOfFoo = scanner.getMethod("Foo", "foo"); + MethodSymbol fooOfBar = scanner.getMethod("Bar", "foo"); + assertThat(findSuperMethod(fooOfBar)).isEqualTo(Optional.of(fooOfFoo)); + } + + @Test + public void findSuperMethod_findsNormalSuperMethodForDirectSuperclass() { + MethodSymbol fooOfBar = scanner.getMethod("Bar", "foo"); + MethodSymbol fooOfBaz = scanner.getMethod("Baz", "foo"); + assertThat(findSuperMethod(fooOfBaz)).isEqualTo(Optional.of(fooOfBar)); + + MethodSymbol barOfQuux = scanner.getMethod("Quux", "bar"); + MethodSymbol barOfNorf = scanner.getMethod("Norf", "bar"); + assertThat(findSuperMethod(barOfNorf)).isEqualTo(Optional.of(barOfQuux)); + } + + @Test + public void findSuperMethod_findsNormalSuperMethodForNonDirectSuperclass() { + MethodSymbol fooOfBaz = scanner.getMethod("Baz", "foo"); + MethodSymbol fooOfNorf = scanner.getMethod("Norf", "foo"); + assertThat(findSuperMethod(fooOfNorf)).isEqualTo(Optional.of(fooOfBaz)); + } + + private ImmutableList findSuperMethods(MethodSymbol method) { + return ImmutableList.copyOf(ASTHelpers.findSuperMethods(method, getTypes())); + } + + private Optional 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 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 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 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 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 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 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 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()); + } +}