/*
* 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.errorprone.BugPattern.Category.JDK;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
import com.google.common.base.Optional;
import com.google.common.primitives.Ints;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.Matchers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.UnaryTree;
import java.math.BigDecimal;
import java.math.BigInteger;
/** @author endobson@google.com (Eric Dobson) */
@BugPattern(
name = "BigDecimalLiteralDouble",
summary =
"BigDecimal(double) and BigDecimal.valueOf(double) may lose precision, "
+ "prefer BigDecimal(String) or BigDecimal(long)",
category = JDK,
severity = WARNING
)
public class BigDecimalLiteralDouble extends BugChecker
implements MethodInvocationTreeMatcher, NewClassTreeMatcher {
private static final BigInteger LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE);
private static final BigInteger LONG_MIN = BigInteger.valueOf(Long.MIN_VALUE);
private static final String BIG_DECIMAL = BigDecimal.class.getName();
private static final Matcher<ExpressionTree> valueOfMethod =
staticMethod().onClass(BIG_DECIMAL).named("valueOf").withParameters("double");
private static final Matcher<ExpressionTree> constructor =
Matchers.constructor().forClass(BIG_DECIMAL).withParameters("double");
// Matches literals and unary +/- and a literal, since most people conceptually think of -1.0 as a
// literal. Doesn't handle nested unary operators as new BigDecimal(String) doesn't accept
// multiple unary prefixes.
private static final Matcher<ExpressionTree> literalArgument = new Matcher<ExpressionTree>() {
@Override
public boolean matches(ExpressionTree tree, VisitorState state) {
if (tree.getKind() == Kind.UNARY_PLUS || tree.getKind() == Kind.UNARY_MINUS) {
tree = ((UnaryTree) tree).getExpression();
}
return tree.getKind() == Kind.DOUBLE_LITERAL;
}
};
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!valueOfMethod.matches(tree, state)) {
return Description.NO_MATCH;
}
ExpressionTree arg = tree.getArguments().get(0);
if (!literalArgument.matches(arg, state)) {
return Description.NO_MATCH;
}
// Don't suggest an integral replacement in this case as it may change the scale of the
// resulting BigDecimal.
return createDescription(tree, arg, state, false);
}
@Override
public Description matchNewClass(NewClassTree tree, VisitorState state) {
if (!constructor.matches(tree, state)) {
return Description.NO_MATCH;
}
ExpressionTree arg = tree.getArguments().get(0);
if (!literalArgument.matches(arg, state)) {
return Description.NO_MATCH;
}
return createDescription(tree, arg, state, true);
}
public Description createDescription(
ExpressionTree tree, ExpressionTree arg, VisitorState state, boolean suggestIntegral) {
String literal = state.getSourceForNode(arg);
if (literal == null) {
return describeMatch(tree);
}
// BigDecimal doesn't seem to support underscores or terminal Ds in its string parsing
literal = literal.replaceAll("[_dD]", "");
BigDecimal intendedValue = new BigDecimal(literal);
Optional<BigInteger> integralValue = asBigInteger(intendedValue);
Description.Builder description = buildDescription(tree);
if (suggestIntegral && integralValue.isPresent() && isWithinLongRange(integralValue.get())) {
long longValue = integralValue.get().longValue();
String suggestedString;
switch (Ints.saturatedCast(longValue)) {
case 0:
suggestedString = "BigDecimal.ZERO";
break;
case 1:
suggestedString = "BigDecimal.ONE";
break;
case 10:
suggestedString = "BigDecimal.TEN";
break;
default:
suggestedString = "new BigDecimal(" + longValue + "L)";
}
description.addFix(
SuggestedFix.builder()
.addImport("java.math.BigDecimal")
.replace(tree, suggestedString)
.build());
}
description.addFix(
SuggestedFix.builder()
.addImport("java.math.BigDecimal")
.replace(tree, "new BigDecimal(\"" + literal + "\")")
.build());
return description.build();
}
public static Optional<BigInteger> asBigInteger(BigDecimal v) {
try {
return Optional.of(v.toBigIntegerExact());
} catch (ArithmeticException e) {
return Optional.absent();
}
}
private boolean isWithinLongRange(BigInteger v) {
return LONG_MIN.compareTo(v) <= 0 && v.compareTo(LONG_MAX) <= 0;
}
}