/*
* Copyright 2013 Google Inc. All Rights Reserved.
*
* 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 static com.google.errorprone.BugPattern.Category.JDK;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import com.google.common.base.Joiner;
import com.google.common.base.Verify;
import com.google.common.collect.Iterables;
import com.google.common.io.ByteStreams;
import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.Category;
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.VariableTreeMatcher;
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.scanner.Scanner;
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.LiteralTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Type.TypeVar;
import com.sun.tools.javac.parser.Tokens.Comment;
import com.sun.tools.javac.tree.JCTree.JCLiteral;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
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<TestScanner> tests = new ArrayList<>();
@After
public void tearDown() {
for (TestScanner test : tests) {
test.verifyAssertionsComplete();
}
}
@Test
public void testGetStartPositionUnix() {
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 testGetStartPositionWindows() {
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 testGetStartPositionWithWhitespaceUnix() {
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 testGetStartPositionWithWhitespaceWindows() {
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<LiteralTree> literalHasStartPosition(final int startPosition) {
return new Matcher<LiteralTree>() {
@Override
public boolean matches(LiteralTree tree, VisitorState state) {
JCLiteral literal = (JCLiteral) tree;
return literal.getStartPosition() == startPosition;
}
};
}
private Scanner literalExpressionMatches(final Matcher<LiteralTree> 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 testGetReceiver() {
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<ExpressionTree> expressionHasReceiverAndType(
final String expectedReceiver, final String expectedType) {
return Matchers.allOf(
new Matcher<ExpressionTree>() {
@Override
public boolean matches(ExpressionTree t, VisitorState state) {
ExpressionTree receiver = ASTHelpers.getReceiver(t);
return expectedReceiver != null
? receiver.toString().equals(expectedReceiver)
: receiver == null;
}
},
new Matcher<ExpressionTree>() {
@Override
public boolean matches(ExpressionTree t, VisitorState state) {
Type type = ASTHelpers.getReceiverType(t);
return state.getTypeFromString(expectedType).equals(type);
}
});
}
private Scanner expressionStatementMatches(final String expectedExpression,
final Matcher<ExpressionTree> 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 testAnnotationHelpers() {
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<ClassTree>() {
@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);
}
// verify that hasAnnotation(Symbol, String, VisitorState) uses binary names for inner classes
@Test
public void testInnerAnnotationType() {
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<ClassTree>() {
@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 testGetTypeOnNestedAnnotationType() {
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();
assertEquals("B.MyAnnotation", ASTHelpers.getType(tree.getAnnotationType()).toString());
return super.visitAnnotation(tree, state);
}
};
tests.add(scanner);
assertCompiles(scanner);
}
@Test
public void testGetTypeOnNestedClassType() {
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();
assertEquals("B.C", ASTHelpers.getType(tree.getType()).toString());
return super.visitVariable(tree, state);
}
};
tests.add(scanner);
assertCompiles(scanner);
}
@Test
public void testGetTypeOnParameterizedType() {
writeFile(
"Pair.java",
"public class Pair<A, B> { ",
" public A first;",
" public B second;",
"}");
writeFile(
"Test.java",
"public class Test {",
" public Integer doSomething(Pair<Integer, String> 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#getUpperBound */
private TestScanner getUpperBoundScanner(final 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 testGetUpperBoundConcreteType() {
writeFile(
"A.java",
"import java.lang.Number;",
"import java.util.List;",
"public class A {",
" public List<Number> myList;",
"}");
TestScanner scanner = getUpperBoundScanner("java.lang.Number");
tests.add(scanner);
assertCompiles(scanner);
}
@Test
public void testGetUpperBoundUpperBoundedWildcard() {
writeFile(
"A.java",
"import java.lang.Number;",
"import java.util.List;",
"public class A {",
" public List<? extends Number> myList;",
"}");
TestScanner scanner = getUpperBoundScanner("java.lang.Number");
tests.add(scanner);
assertCompiles(scanner);
}
@Test
public void testGetUpperBoundUnboundedWildcard() {
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 testGetUpperBoundLowerBoundedWildcard() {
writeFile(
"A.java",
"import java.lang.Number;",
"import java.util.List;",
"public class A {",
" public List<? super Number> myList;",
"}");
TestScanner scanner = getUpperBoundScanner("java.lang.Object");
tests.add(scanner);
assertCompiles(scanner);
}
@Test
public void testGetUpperBoundTypeVariable() {
writeFile(
"A.java",
"import java.util.List;",
"public class A<T> {",
" public List<T> myList;",
"}");
TestScanner scanner = getUpperBoundScanner("java.lang.Object");
tests.add(scanner);
assertCompiles(scanner);
}
@Test
public void testGetUpperBoundCapturedTypeVariable() {
writeFile(
"A.java",
"import java.lang.Number;",
"import java.util.List;",
"public class A {",
" public void doSomething(List<? extends Number> list) {",
" list.get(0);",
" }",
"}");
TestScanner scanner =
new TestScanner() {
@Override
public Void visitMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!"super()".equals(tree.toString())) { // ignore synthetic super call
setAssertionsComplete();
Type type = ASTHelpers.getType(tree);
assertThat(type instanceof TypeVar).isTrue();
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 testCommentTokens() {
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<String> comments = new ArrayList<>();
for (ErrorProneToken t : state.getTokensForNode(tree)) {
if (!t.comments().isEmpty()) {
for (Comment c : t.comments()) {
Verify.verify(c.getSourcePos(0) >= 0);
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 testHasDirectAnnotationWithSimpleName() {
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);
}
@BugPattern(
name = "HasDirectAnnotationWithSimpleNameChecker",
category = Category.ONE_OFF,
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 class CustomCRVTest {
/** A custom @CRV annotation. */
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckReturnValue {}
@CheckReturnValue
public static String hello() {
return "Hello!";
}
}
@Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
static void addClassToJar(JarOutputStream jos, Class<?> clazz) throws IOException {
String entryPath = clazz.getName().replace('.', '/') + ".class";
try (InputStream is = clazz.getClassLoader().getResourceAsStream(entryPath)) {
jos.putNextEntry(new JarEntry(entryPath));
ByteStreams.copy(is, jos);
}
}
@Test
public void testHasDirectAnnotationWithSimpleNameWithoutAnnotationOnClasspath()
throws IOException {
File libJar = tempFolder.newFile("lib.jar");
try (FileOutputStream fis = new FileOutputStream(libJar);
JarOutputStream jos = new JarOutputStream(fis)) {
addClassToJar(jos, CustomCRVTest.class);
addClassToJar(jos, ASTHelpersTest.class);
addClassToJar(jos, CompilerBasedAbstractTest.class);
}
CompilationTestHelper.newInstance(HasDirectAnnotationWithSimpleNameChecker.class, getClass())
.addSourceLines(
"Test.java",
"class Test {",
" void m() {",
" // BUG: Diagnostic contains:",
" com.google.errorprone.util.ASTHelpersTest.CustomCRVTest.hello();",
" }",
"}")
.setArgs(Arrays.asList("-cp", libJar.toString()))
.doTest();
}
/* Test infrastructure */
private static abstract 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;
}
<T extends Tree> void assertMatch(T node, VisitorState visitorState,
Matcher<T> matcher) {
VisitorState state = visitorState.withPath(getCurrentPath());
assertTrue(matcher.matches(node, state));
}
public void verifyAssertionsComplete() {
assertTrue("Expected the visitor to call setAssertionsComplete().", assertionsComplete);
}
}
/** A checker that reports the constant value of fields. */
@BugPattern(name = "ConstChecker", category = JDK, 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(
name = "PrintResultTypeOfFirstArgument",
category = Category.ONE_OFF,
severity = SeverityLevel.ERROR,
summary = "Prints the type of the first argument in method calls"
)
public static class PrintResultTypeOfFirstArgument extends BugChecker
implements MethodInvocationTreeMatcher {
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
List<? extends ExpressionTree> arguments = tree.getArguments();
if (arguments.isEmpty()) {
return Description.NO_MATCH;
}
return buildDescription(tree)
.setMessage(ASTHelpers.getResultType(Iterables.getFirst(arguments, null)).toString())
.build();
}
}
@Test
public void getResultType_findsConcreteType_withGenericMethodCall() {
CompilationTestHelper.newInstance(PrintResultTypeOfFirstArgument.class, getClass())
.addSourceLines(
"Test.java",
"abstract class Test {",
" abstract <T> 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: <nulltype>",
" target(null);",
" }",
"}")
.doTest();
}
@Test
public void getResultType_findsConcreteType_withGenericConstructorCall() {
CompilationTestHelper.newInstance(PrintResultTypeOfFirstArgument.class, getClass())
.addSourceLines(
"Test.java",
"class GenericTest<T> {}",
"abstract class Test {",
" abstract void target(Object param);",
" private void test() {",
" // BUG: Diagnostic contains: GenericTest<java.lang.String>",
" target(new GenericTest<String>());",
" }",
"}")
.doTest();
}
}