/*
* Copyright 2011 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.bugpatterns;
import static com.google.errorprone.BugPattern.Category.GUAVA;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.argument;
import static com.google.errorprone.matchers.Matchers.staticMethod;
import com.google.common.base.Joiner;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.fixes.Fix;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matchers;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.BinaryTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCIdent;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Checks that the 1st argument to Preconditions.checkNotNull() isn't a primitive. The primitive
* would be autoboxed to a non-null boxed type, and the check would trivially pass.
*
* <p>In our experience, most of these errors are from copied-and-pasted code and should simply be
* removed.
*
* @author sjnickerson@google.com (Simon Nickerson)
* @author eaftan@google.com (Eddie Aftandilian)
*/
@BugPattern(
name = "PreconditionsCheckNotNullPrimitive",
summary =
"First argument to `Preconditions.checkNotNull()` is a primitive rather "
+ "than an object reference",
explanation =
"`Preconditions.checkNotNull()` takes as an argument a reference that should be "
+ "non-null. Often a primitive is passed as the argument to check. The primitive "
+ "will be [autoboxed]"
+ "(http://docs.oracle.com/javase/7/docs/technotes/guides/language/autoboxing.html) "
+ "into a boxed object, which is non-null, causing the check to "
+ "always pass without the condition being evaluated.\n\n"
+ "If the intent was to ensure that the primitive met some criterion (e.g., a boolean "
+ "that should be non-null), please use `Preconditions.checkState()` or "
+ "`Preconditions.checkArgument()` instead.",
category = GUAVA,
severity = ERROR
)
public class PreconditionsCheckNotNullPrimitive extends BugChecker
implements MethodInvocationTreeMatcher {
@Override
public Description matchMethodInvocation(MethodInvocationTree methodInvocationTree, VisitorState state) {
if (allOf(
staticMethod().onClass("com.google.common.base.Preconditions").named("checkNotNull"),
argument(0, Matchers.<ExpressionTree>isPrimitiveType()))
.matches(methodInvocationTree, state)) {
return describe(methodInvocationTree, state);
}
return Description.NO_MATCH;
}
/**
* If the call to Preconditions.checkNotNull is part of an expression (assignment, return, etc.),
* we substitute the argument for the method call. E.g.:
* {@code bar = Preconditions.checkNotNull(foo); ==> bar = foo;}
*
* <p>If the argument to Preconditions.checkNotNull is a comparison using == or != and one of the
* operands is null, we call checkNotNull on the non-null operand. E.g.:
* {@code checkNotNull(a == null); ==> checkNotNull(a);}
*
* <p>If the argument is a method call or binary tree and its return type is boolean, change it to a
* checkArgument/checkState. E.g.:
* {@code Preconditions.checkNotNull(foo.hasFoo()) ==> Preconditions.checkArgument(foo.hasFoo())}
*
* <p>Otherwise, delete the checkNotNull call. E.g.:
* {@code Preconditions.checkNotNull(foo); ==> [delete the line]}
*/
public Description describe(MethodInvocationTree methodInvocationTree, VisitorState state) {
ExpressionTree arg1 = methodInvocationTree.getArguments().get(0);
Tree parent = state.getPath().getParentPath().getLeaf();
// Assignment, return, etc.
if (parent.getKind() != Kind.EXPRESSION_STATEMENT) {
return describeMatch(arg1,
SuggestedFix.replace(methodInvocationTree, arg1.toString()));
}
// Comparison to null
if (arg1.getKind() == Kind.EQUAL_TO || arg1.getKind() == Kind.NOT_EQUAL_TO) {
BinaryTree binaryExpr = (BinaryTree) arg1;
if (binaryExpr.getLeftOperand().getKind() == Kind.NULL_LITERAL) {
return describeMatch(arg1,
SuggestedFix.replace(arg1, binaryExpr.getRightOperand().toString()));
}
if (binaryExpr.getRightOperand().getKind() == Kind.NULL_LITERAL) {
return describeMatch(arg1,
SuggestedFix.replace(arg1, binaryExpr.getLeftOperand().toString()));
}
}
if ((arg1 instanceof BinaryTree || arg1.getKind() == Kind.METHOD_INVOCATION ||
arg1.getKind() == Kind.LOGICAL_COMPLEMENT) &&
((JCExpression) arg1).type == state.getSymtab().booleanType) {
return describeMatch(arg1,
createCheckArgumentOrStateCall(methodInvocationTree, state, arg1));
}
return describeMatch(arg1, SuggestedFix.delete(parent));
}
/**
* Creates a SuggestedFix that replaces the checkNotNull call with a checkArgument or checkState
* call.
*/
private Fix createCheckArgumentOrStateCall(MethodInvocationTree methodInvocationTree,
VisitorState state, ExpressionTree arg1) {
SuggestedFix.Builder fix = SuggestedFix.builder();
String replacementMethod = "checkState";
if (hasMethodParameter(state.getPath(), arg1)) {
replacementMethod = "checkArgument";
}
StringBuilder replacement = new StringBuilder();
// Was the original call to Preconditions.checkNotNull a static import or not?
if (methodInvocationTree.getMethodSelect().getKind() == Kind.IDENTIFIER) {
replacement.append(replacementMethod + "(");
fix.addStaticImport("com.google.common.base.Preconditions." + replacementMethod);
} else {
replacement.append("Preconditions." + replacementMethod + "(");
}
Joiner.on(", ").appendTo(replacement, methodInvocationTree.getArguments());
replacement.append(")");
fix.replace(methodInvocationTree, replacement.toString());
return fix.build();
}
/**
* Determines whether the expression contains a reference to one of the
* enclosing method's parameters.
*
* TODO(eaftan): Extract this to ASTHelpers.
*
* @param path the path to the current tree node
* @param tree the node to compare against the parameters
* @return whether the argument is a parameter to the enclosing method
*/
private static boolean hasMethodParameter(TreePath path, ExpressionTree tree) {
Set<Symbol> symbols = new HashSet<>();
for (IdentifierTree ident : getVariableUses(tree)) {
Symbol sym = ASTHelpers.getSymbol(ident);
if (sym.isLocal()) {
symbols.add(sym);
}
}
// Find enclosing method declaration.
while (path != null && !(path.getLeaf() instanceof MethodTree)) {
path = path.getParentPath();
}
if (path == null) {
throw new IllegalStateException("Should have an enclosing method declaration");
}
MethodTree methodDecl = (MethodTree) path.getLeaf();
for (VariableTree param : methodDecl.getParameters()) {
if (symbols.contains(ASTHelpers.getSymbol(param))) {
return true;
}
}
return false;
}
/**
* Find the root variable identifiers from an arbitrary expression.
*
* Examples:
* a.trim().intern() ==> {a}
* a.b.trim().intern() ==> {a}
* this.intValue.foo() ==> {this}
* this.foo() ==> {this}
* intern() ==> {}
* String.format() ==> {}
* java.lang.String.format() ==> {}
* x.y.z(s.t) ==> {x,s}
*/
static List<IdentifierTree> getVariableUses(ExpressionTree tree) {
final List<IdentifierTree> freeVars = new ArrayList<>();
new TreeScanner<Void, Void>() {
@Override
public Void visitIdentifier(IdentifierTree node, Void v) {
if (((JCIdent) node).sym instanceof VarSymbol) {
freeVars.add(node);
}
return super.visitIdentifier(node, v);
}
}.scan(tree, null);
return freeVars;
}
}