/*
* Copyright 2016 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.fixes;
import static com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH;
import static com.google.errorprone.BugPattern.Category.JDK;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
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.LiteralTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.ReturnTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.doctree.LinkTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.DocTreePath;
import com.sun.source.util.DocTreePathScanner;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.tree.DCTree;
import com.sun.tools.javac.tree.JCTree;
import java.io.IOException;
import java.lang.annotation.Retention;
import javax.lang.model.element.Modifier;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** @author cushon@google.com (Liam Miller-Cushon) */
@RunWith(JUnit4.class)
public class SuggestedFixesTest {
@Retention(RUNTIME)
public @interface EditModifiers {
String value() default "";
EditKind kind() default EditKind.ADD;
enum EditKind {
ADD,
REMOVE
}
}
@BugPattern(
name = "EditModifiers",
category = Category.ONE_OFF,
summary = "Edits modifiers",
severity = SeverityLevel.ERROR
)
public static class EditModifiersChecker extends BugChecker
implements VariableTreeMatcher, MethodTreeMatcher {
static final ImmutableMap<String, Modifier> MODIFIERS_BY_NAME = createModifiersByName();
private static ImmutableMap<String, Modifier> createModifiersByName() {
ImmutableMap.Builder<String, Modifier> builder = ImmutableMap.builder();
for (Modifier mod : Modifier.values()) {
builder.put(mod.toString(), mod);
}
return builder.build();
}
@Override
public Description matchVariable(VariableTree tree, VisitorState state) {
return editModifiers(tree, state);
}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
return editModifiers(tree, state);
}
private Description editModifiers(Tree tree, VisitorState state) {
EditModifiers editModifiers =
ASTHelpers.getAnnotation(
ASTHelpers.findEnclosingNode(state.getPath(), ClassTree.class), EditModifiers.class);
Modifier mod = MODIFIERS_BY_NAME.get(editModifiers.value());
Verify.verifyNotNull(mod, editModifiers.value());
Fix fix;
switch (editModifiers.kind()) {
case ADD:
fix = SuggestedFixes.addModifiers(tree, state, mod);
break;
case REMOVE:
fix = SuggestedFixes.removeModifiers(tree, state, mod);
break;
default:
throw new AssertionError(editModifiers.kind());
}
return describeMatch(tree, fix);
}
}
@Test
public void addAtBeginningOfLine() throws IOException {
BugCheckerRefactoringTestHelper.newInstance(new EditModifiersChecker(), getClass())
.addInputLines(
"in/Test.java",
"import javax.annotation.Nullable;",
String.format("import %s;", EditModifiers.class.getCanonicalName()),
"@EditModifiers(value=\"final\", kind=EditModifiers.EditKind.ADD)",
"class Test {",
" @Nullable",
" int foo() {",
" return 10;",
" }",
"}")
.addOutputLines(
"out/Test.java",
"import javax.annotation.Nullable;",
String.format("import %s;", EditModifiers.class.getCanonicalName()),
"@EditModifiers(value=\"final\", kind=EditModifiers.EditKind.ADD)",
"class Test {",
" @Nullable",
" final int foo() {",
" return 10;",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
@Test
public void addModifiers() {
CompilationTestHelper.newInstance(EditModifiersChecker.class, getClass())
.addSourceLines(
"Test.java",
String.format("import %s;", EditModifiers.class.getCanonicalName()),
"import javax.annotation.Nullable;",
"@EditModifiers(value=\"final\", kind=EditModifiers.EditKind.ADD)",
"class Test {",
" // BUG: Diagnostic contains: final Object one",
" Object one;",
" // BUG: Diagnostic contains: @Nullable final Object two",
" @Nullable Object two;",
" // BUG: Diagnostic contains: @Nullable public final Object three",
" @Nullable public Object three;",
" // BUG: Diagnostic contains: public final Object four",
" public Object four;",
"}")
.doTest();
}
@Test
public void addModifiersComment() {
CompilationTestHelper.newInstance(EditModifiersChecker.class, getClass())
.addSourceLines(
"Test.java",
String.format("import %s;", EditModifiers.class.getCanonicalName()),
"import javax.annotation.Nullable;",
"@EditModifiers(value=\"final\", kind=EditModifiers.EditKind.ADD)",
"class Test {",
" // BUG: Diagnostic contains:"
+ " private @Deprecated /*comment*/ final volatile Object one;",
" private @Deprecated /*comment*/ volatile Object one;",
" // BUG: Diagnostic contains:"
+ " private @Deprecated /*comment*/ static final Object two = null;",
" private @Deprecated /*comment*/ static Object two = null;",
"}")
.doTest();
}
@Test
public void addModifiersFirst() {
CompilationTestHelper.newInstance(EditModifiersChecker.class, getClass())
.addSourceLines(
"Test.java",
String.format("import %s;", EditModifiers.class.getCanonicalName()),
"import javax.annotation.Nullable;",
"@EditModifiers(value=\"public\", kind=EditModifiers.EditKind.ADD)",
"class Test {",
" // BUG: Diagnostic contains: public static final transient Object one",
" static final transient Object one = null;",
"}")
.doTest();
}
@Test
public void removeModifiers() {
CompilationTestHelper.newInstance(EditModifiersChecker.class, getClass())
.addSourceLines(
"Test.java",
String.format("import %s;", EditModifiers.class.getCanonicalName()),
"import javax.annotation.Nullable;",
"@EditModifiers(value=\"final\", kind=EditModifiers.EditKind.REMOVE)",
"class Test {",
" // BUG: Diagnostic contains: Object one",
" final Object one = null;",
" // BUG: Diagnostic contains: @Nullable Object two",
" @Nullable final Object two = null;",
" // BUG: Diagnostic contains: @Nullable public Object three",
" @Nullable public final Object three = null;",
" // BUG: Diagnostic contains: public Object four",
" public final Object four = null;",
"}")
.doTest();
}
@BugPattern(
category = Category.ONE_OFF,
name = "CastReturn",
severity = SeverityLevel.ERROR,
summary = "Adds casts to returned expressions"
)
public static class CastReturn extends BugChecker implements ReturnTreeMatcher {
@Override
public Description matchReturn(ReturnTree tree, VisitorState state) {
if (tree.getExpression() == null) {
return Description.NO_MATCH;
}
Type type =
ASTHelpers.getSymbol(ASTHelpers.findEnclosingNode(state.getPath(), MethodTree.class))
.getReturnType();
SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
String qualifiedTargetType = SuggestedFixes.qualifyType(state, fixBuilder, type.tsym);
fixBuilder.prefixWith(tree.getExpression(), String.format("(%s) ", qualifiedTargetType));
return describeMatch(tree, fixBuilder.build());
}
}
@BugPattern(
category = Category.ONE_OFF,
name = "CastReturn",
severity = SeverityLevel.ERROR,
summary = "Adds casts to returned expressions"
)
public static class CastReturnFullType extends BugChecker implements ReturnTreeMatcher {
@Override
public Description matchReturn(ReturnTree tree, VisitorState state) {
if (tree.getExpression() == null) {
return Description.NO_MATCH;
}
Type type =
ASTHelpers.getSymbol(ASTHelpers.findEnclosingNode(state.getPath(), MethodTree.class))
.getReturnType();
SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
String qualifiedTargetType = SuggestedFixes.qualifyType(state, fixBuilder, type);
fixBuilder.prefixWith(tree.getExpression(), String.format("(%s) ", qualifiedTargetType));
return describeMatch(tree, fixBuilder.build());
}
}
@Test
public void qualifiedName_Object() {
CompilationTestHelper.newInstance(CastReturn.class, getClass())
.addSourceLines(
"Test.java",
"class Test {",
" Object f() {",
" // BUG: Diagnostic contains: return (Object) null;",
" return null;",
" }",
"}")
.doTest();
}
@Test
public void qualifiedName_imported() {
CompilationTestHelper.newInstance(CastReturn.class, getClass())
.addSourceLines(
"Test.java",
"import java.util.Map.Entry;",
"class Test {",
" java.util.Map.Entry<String, Integer> f() {",
" // BUG: Diagnostic contains: return (Entry) null;",
" return null;",
" }",
"}")
.doTest();
}
@Test
public void qualifiedName_notImported() {
CompilationTestHelper.newInstance(CastReturn.class, getClass())
.addSourceLines(
"Test.java",
"class Test {",
" java.util.Map.Entry<String, Integer> f() {",
" // BUG: Diagnostic contains: return (Map.Entry) null;",
" return null;",
" }",
"}")
.doTest();
}
@Test
public void qualifiedName_typeVariable() {
CompilationTestHelper.newInstance(CastReturn.class, getClass())
.addSourceLines(
"Test.java",
"class Test<T> {",
" T f() {",
" // BUG: Diagnostic contains: return (T) null;",
" return null;",
" }",
"}")
.doTest();
}
@Test
public void fullQualifiedName_Object() {
CompilationTestHelper.newInstance(CastReturnFullType.class, getClass())
.addSourceLines(
"Test.java",
"class Test {",
" Object f() {",
" // BUG: Diagnostic contains: return (Object) null;",
" return null;",
" }",
"}")
.doTest();
}
@Test
public void fullQualifiedName_imported() {
CompilationTestHelper.newInstance(CastReturnFullType.class, getClass())
.addSourceLines(
"Test.java",
"import java.util.Map.Entry;",
"class Test {",
" java.util.Map.Entry<String, Integer> f() {",
" // BUG: Diagnostic contains: return (Entry<String,Integer>) null;",
" return null;",
" }",
"}")
.doTest();
}
@Test
public void fullQualifiedName_notImported() {
CompilationTestHelper.newInstance(CastReturnFullType.class, getClass())
.addSourceLines(
"Test.java",
"class Test {",
" java.util.Map.Entry<String, Integer> f() {",
" // BUG: Diagnostic contains: return (Map.Entry<String,Integer>) null;",
" return null;",
" }",
"}")
.doTest();
}
@Test
public void fullQualifiedName_typeVariable() {
CompilationTestHelper.newInstance(CastReturnFullType.class, getClass())
.addSourceLines(
"Test.java",
"class Test<T> {",
" T f() {",
" // BUG: Diagnostic contains: return (T) null;",
" return null;",
" }",
"}")
.doTest();
}
/** A test check that qualifies javadoc link. */
@BugPattern(
name = "JavadocQualifier",
category = BugPattern.Category.JDK,
summary = "all javadoc links should be qualified",
severity = ERROR
)
public static class JavadocQualifier extends BugChecker implements BugChecker.ClassTreeMatcher {
@Override
public Description matchClass(ClassTree tree, final VisitorState state) {
final DCTree.DCDocComment comment =
((JCTree.JCCompilationUnit) state.getPath().getCompilationUnit())
.docComments.getCommentTree((JCTree) tree);
if (comment == null) {
return Description.NO_MATCH;
}
final SuggestedFix.Builder fix = SuggestedFix.builder();
new DocTreePathScanner<Void, Void>() {
@Override
public Void visitLink(LinkTree node, Void aVoid) {
SuggestedFixes.qualifyDocReference(
fix, new DocTreePath(getCurrentPath(), node.getReference()), state);
return null;
}
}.scan(new DocTreePath(state.getPath(), comment), null);
if (fix.isEmpty()) {
return Description.NO_MATCH;
}
return describeMatch(tree, fix.build());
}
}
@Test
public void qualifyJavadocTest() throws Exception {
BugCheckerRefactoringTestHelper.newInstance(new JavadocQualifier(), getClass())
.addInputLines(
"in/Test.java", //
"import java.util.List;",
"import java.util.Map;",
"/** foo {@link List} bar {@link Map#containsKey(Object)} baz {@link #foo} */",
"class Test {",
" void foo() {}",
"}")
.addOutputLines(
"out/Test.java", //
"import java.util.List;",
"import java.util.Map;",
"/** foo {@link java.util.List} bar {@link java.util.Map#containsKey(Object)} baz"
+ " {@link Test#foo} */",
"class Test {",
" void foo() {}",
"}")
.doTest(TEXT_MATCH);
}
@BugPattern(name = "SuppressMe", category = Category.ONE_OFF, summary = "", severity = ERROR)
static final class SuppressMe extends BugChecker implements LiteralTreeMatcher {
@Override
public Description matchLiteral(LiteralTree tree, VisitorState state) {
if (tree.getValue().equals(42)) {
Fix potentialFix = SuggestedFixes.addSuppressWarnings(state, "SuppressMe");
if (potentialFix == null) {
return describeMatch(tree);
}
return describeMatch(tree, potentialFix);
}
return Description.NO_MATCH;
}
}
@Test
public void testSuppressWarningsFix() throws IOException {
BugCheckerRefactoringTestHelper refactorTestHelper =
BugCheckerRefactoringTestHelper.newInstance(new SuppressMe(), getClass());
refactorTestHelper
.addInputLines(
"in/Test.java",
"public class Test {",
" static final int BEST_NUMBER = 42;",
" static { int i = 42; }",
" @SuppressWarnings(\"one\")",
" public void doIt() {",
" System.out.println(\"\" + 42);",
" }",
"}")
.addOutputLines(
"out/Test.java",
"public class Test {",
" @SuppressWarnings(\"SuppressMe\") static final int BEST_NUMBER = 42;",
" static { @SuppressWarnings(\"SuppressMe\") int i = 42; }",
" @SuppressWarnings({\"one\", \"SuppressMe\"})",
" public void doIt() {",
" System.out.println(\"\" + 42);",
" }",
"}")
.doTest();
}
/** A test bugchecker that deletes any field whose removal doesn't break the compilation. */
@BugPattern(name = "CompilesWithFixChecker", category = JDK, summary = "", severity = ERROR)
public static class CompilesWithFixChecker extends BugChecker implements VariableTreeMatcher {
@Override
public Description matchVariable(VariableTree tree, VisitorState state) {
Fix fix = SuggestedFix.delete(tree);
return SuggestedFixes.compilesWithFix(fix, state)
? describeMatch(tree, fix)
: Description.NO_MATCH;
}
}
@Test
public void compilesWithFixTest() throws IOException {
BugCheckerRefactoringTestHelper.newInstance(new CompilesWithFixChecker(), getClass())
.addInputLines(
"in/Test.java",
"class Test {",
" void f() {",
" int x = 0;",
" int y = 1;",
" System.err.println(y);",
" }",
"}")
.addOutputLines(
"out/Test.java",
"class Test {",
" void f() {",
" int y = 1;",
" System.err.println(y);",
" }",
"}")
.doTest();
}
}