/*
* 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.bugpatterns;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.matchers.Matchers.toType;
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import static com.google.errorprone.util.ASTHelpers.getType;
import static com.google.errorprone.util.ASTHelpers.isSameType;
import com.google.common.primitives.Longs;
import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.Category;
import com.google.errorprone.BugPattern.SeverityLevel;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher;
import com.google.errorprone.fixes.Fix;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.CompoundAssignmentTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.LiteralTree;
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.source.util.TreeScanner;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symtab;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCMethodInvocation;
/** @author cushon@google.com (Liam Miller-Cushon) */
@BugPattern(
name = "BoxedPrimitiveConstructor",
category = Category.JDK,
summary = "valueOf or autoboxing provides better time and space performance",
severity = SeverityLevel.WARNING
)
public class BoxedPrimitiveConstructor extends BugChecker implements NewClassTreeMatcher {
private static final Matcher<Tree> TO_STRING =
toType(ExpressionTree.class, instanceMethod().anyClass().named("toString").withParameters());
private static final Matcher<Tree> HASH_CODE =
toType(ExpressionTree.class, instanceMethod().anyClass().named("hashCode").withParameters());
private static final Matcher<Tree> COMPARE_TO =
toType(
ExpressionTree.class,
instanceMethod().onDescendantOf("java.lang.Comparable").named("compareTo"));
@Override
public Description matchNewClass(NewClassTree tree, VisitorState state) {
Symbol sym = ASTHelpers.getSymbol(tree.getIdentifier());
if (sym == null) {
return NO_MATCH;
}
Types types = state.getTypes();
Symtab symtab = state.getSymtab();
// TODO(cushon): consider handling String also
if (sym.equals(types.boxedClass(symtab.byteType))
|| sym.equals(types.boxedClass(symtab.charType))
|| sym.equals(types.boxedClass(symtab.shortType))
|| sym.equals(types.boxedClass(symtab.intType))
|| sym.equals(types.boxedClass(symtab.longType))
|| sym.equals(types.boxedClass(symtab.doubleType))
|| sym.equals(types.boxedClass(symtab.floatType))
|| sym.equals(types.boxedClass(symtab.booleanType))) {
return describeMatch(tree, buildFix(tree, state));
}
return NO_MATCH;
}
private Fix buildFix(NewClassTree tree, VisitorState state) {
boolean autoboxFix = shouldAutoboxFix(state);
Types types = state.getTypes();
Type type = types.unboxedTypeOrType(getType(tree));
if (types.isSameType(type, state.getSymtab().booleanType)) {
Object value = literalValue(tree.getArguments().iterator().next());
if (value instanceof Boolean) {
return SuggestedFix.replace(tree, literalFix((boolean) value, autoboxFix));
} else if (value instanceof String) {
return SuggestedFix.replace(
tree, literalFix(Boolean.parseBoolean((String) value), autoboxFix));
}
}
// Primitive constructors are all unary
JCTree.JCExpression arg = (JCTree.JCExpression) getOnlyElement(tree.getArguments());
Type argType = getType(arg);
if (autoboxFix && argType.isPrimitive()) {
return SuggestedFix.builder()
.replace(
((JCTree) tree).getStartPosition(),
arg.getStartPosition(),
maybeCast(state, type, argType))
.replace(state.getEndPosition(arg), state.getEndPosition(tree), "")
.build();
}
JCTree parent = (JCTree) state.getPath().getParentPath().getParentPath().getLeaf();
if (TO_STRING.matches(parent, state)) {
// e.g. new Integer($A).toString() -> String.valueOf($A)
return SuggestedFix.builder()
.replace(parent.getStartPosition(), arg.getStartPosition(), "String.valueOf(")
.replace(state.getEndPosition(arg), state.getEndPosition(parent), ")")
.build();
}
String typeName = state.getSourceForNode(tree.getIdentifier());
if (HASH_CODE.matches(parent, state)) {
// e.g. new Integer($A).hashCode() -> Integer.hashCode($A)
SuggestedFix.Builder fix = SuggestedFix.builder();
String replacement;
if (types.isSameType(type, state.getSymtab().longType)) {
// TODO(b/29979605): Long.hashCode was added in JDK8
fix.addImport(Longs.class.getName());
replacement = "Longs.hashCode(";
} else {
replacement = String.format("%s.hashCode(", typeName);
}
return fix.replace(parent.getStartPosition(), arg.getStartPosition(), replacement)
.replace(state.getEndPosition(arg), state.getEndPosition(parent), ")")
.build();
}
DoubleAndFloatStatus doubleAndFloatStatus = doubleAndFloatStatus(state, type, argType);
if (COMPARE_TO.matches(parent, state)
&& ASTHelpers.getReceiver((ExpressionTree) parent).equals(tree)) {
JCMethodInvocation compareTo = (JCMethodInvocation) parent;
// e.g. new Integer($A).compareTo($B) -> Integer.compare($A, $B)
JCTree.JCExpression rhs = getOnlyElement(compareTo.getArguments());
String optionalCast = "";
String optionalSuffix = "";
switch (doubleAndFloatStatus) {
case PRIMITIVE_DOUBLE_INTO_FLOAT:
// new Float(double).compareTo($foo) => Float.compare((float) double, foo)
optionalCast = "(float) ";
break;
case BOXED_DOUBLE_INTO_FLOAT:
// new Float(Double).compareTo($foo) => Float.compare(Double.floatValue(), foo)
optionalSuffix = ".floatValue()";
break;
default:
break;
}
return SuggestedFix.builder()
.replace(
compareTo.getStartPosition(),
arg.getStartPosition(),
String.format("%s.compare(%s", typeName, optionalCast))
.replace(
state.getEndPosition(arg),
rhs.getStartPosition(),
String.format("%s, ", optionalSuffix))
.replace(state.getEndPosition(rhs), state.getEndPosition(compareTo), ")")
.build();
}
// Patch new Float(Double) => Float.valueOf(float) by downcasting the double, since
// neither valueOf(float) nor valueOf(String) match.
String prefixToArg;
String suffix = "";
switch (doubleAndFloatStatus) {
case PRIMITIVE_DOUBLE_INTO_FLOAT:
// new Float(double) => Float.valueOf((float) double)
prefixToArg = String.format("%s.valueOf(%s", typeName, "(float) ");
break;
case BOXED_DOUBLE_INTO_FLOAT:
// new Float(Double) => Double.floatValue()
prefixToArg = "";
suffix = ".floatValue(";
break;
default:
prefixToArg = String.format("%s.valueOf(", typeName);
break;
}
return SuggestedFix.builder()
.replace(((JCTree) tree).getStartPosition(), arg.getStartPosition(), prefixToArg)
.postfixWith(arg, suffix)
.build();
}
private String maybeCast(VisitorState state, Type type, Type argType) {
if (doubleAndFloatStatus(state, type, argType)
== DoubleAndFloatStatus.PRIMITIVE_DOUBLE_INTO_FLOAT) {
// e.g.: new Float(3.0d) => (float) 3.0d
return "(float) ";
}
// primitive widening conversions can't be combined with autoboxing, so add a
// explicit widening cast unless we're sure the expression doesn't get autoboxed
Tree parent = state.getPath().getParentPath().getLeaf();
// TODO(cushon): de-dupe with UnnecessaryCast
Type targetType =
parent.accept(
new TreeScanner<Type, Void>() {
@Override
public Type visitAssignment(AssignmentTree node, Void unused) {
return getType(node.getVariable());
}
@Override
public Type visitCompoundAssignment(CompoundAssignmentTree node, Void unused) {
return getType(node.getVariable());
}
@Override
public Type visitReturn(ReturnTree node, Void unused) {
return getType(state.findEnclosing(MethodTree.class).getReturnType());
}
@Override
public Type visitVariable(VariableTree node, Void unused) {
return getType(node.getType());
}
},
null);
if (!isSameType(type, argType, state) && !isSameType(targetType, type, state)) {
return String.format("(%s) ", type);
}
return "";
}
private enum DoubleAndFloatStatus {
NONE,
PRIMITIVE_DOUBLE_INTO_FLOAT,
BOXED_DOUBLE_INTO_FLOAT
}
private DoubleAndFloatStatus doubleAndFloatStatus(
VisitorState state, Type recieverType, Type argType) {
Types types = state.getTypes();
if (!types.isSameType(recieverType, state.getSymtab().floatType)) {
return DoubleAndFloatStatus.NONE;
}
if (types.isSameType(argType, types.boxedClass(state.getSymtab().doubleType).type)) {
return DoubleAndFloatStatus.BOXED_DOUBLE_INTO_FLOAT;
}
if (types.isSameType(argType, state.getSymtab().doubleType)) {
return DoubleAndFloatStatus.PRIMITIVE_DOUBLE_INTO_FLOAT;
}
return DoubleAndFloatStatus.NONE;
}
private boolean shouldAutoboxFix(VisitorState state) {
switch (state.getPath().getParentPath().getLeaf().getKind()) {
case METHOD_INVOCATION:
// autoboxing a method argument affects overload resolution
return false;
case MEMBER_SELECT:
// can't select members on primitives (e.g. `theInteger.toString()`)
return false;
default:
return true;
}
}
private String literalFix(boolean value, boolean autoboxFix) {
if (autoboxFix) {
return value ? "true" : "false";
}
return value ? "Boolean.TRUE" : "Boolean.FALSE";
}
private Object literalValue(Tree arg) {
if (!(arg instanceof LiteralTree)) {
return null;
}
return ((LiteralTree) arg).getValue();
}
}