/*
* Copyright 2014 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.Maps.newHashMap;
import static com.google.errorprone.BugPattern.Category.JDK;
import static com.google.errorprone.BugPattern.MaturityLevel.EXPERIMENTAL;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.fixes.SuggestedFix.replace;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.matchers.Matchers.hasIdentifier;
import static com.google.errorprone.matchers.MultiMatcher.MatchType.ANY;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.getType;
import static com.sun.source.tree.Tree.Kind.IDENTIFIER;
import static java.util.Collections.unmodifiableList;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Type;
import java.util.List;
import java.util.Map;
/**
* Checks, if two constructors in a class both accept {@code Foo foo} and one calls the other, that
* the caller passes {@code foo} as a parameter. The goal is to catch copy-paste errors:<pre>
* MissileLauncher(Location target, boolean askForConfirmation) {
* ...
* }
* MissileLauncher(Location target) {
* this(target, false);
* }
* MissileLauncher(boolean askForConfirmation) {
* this(TEST_TARGET, <b>false</b>); // should be askForConfirmation
* }</pre>
*
* @author cpovirk@google.com (Chris Povirk)
*/
@BugPattern(name = "ChainingConstructorIgnoresParameter",
maturity = EXPERIMENTAL, category = JDK, severity = WARNING,
explanation = "A constructor parameter might not be being used as expected",
summary = "The called constructor accepts a parameter with the same name and type as one of "
+ "its caller's parameters, but its caller doesn't pass that parameter to it. It's likely "
+ "that it was intended to.")
public final class ChainingConstructorIgnoresParameter extends BugChecker
implements CompilationUnitTreeMatcher, MethodInvocationTreeMatcher, MethodTreeMatcher {
private final Map<MethodSymbol, List<VariableTree>> paramTypesForMethod = newHashMap();
private final Multimap<MethodSymbol, Caller> callersToEvaluate = ArrayListMultimap.create();
@Override
public Description matchCompilationUnit(List<? extends AnnotationTree> packageAnnotations,
ExpressionTree packageName, List<? extends ImportTree> imports, VisitorState state) {
/*
* Clear the collections to save memory. (I wonder if it also helps to handle weird cases when a
* class has multiple definitions. But I would expect for multiple definitions within the same
* compiler invocation to cause deeper problems.)
*/
paramTypesForMethod.clear();
callersToEvaluate.clear(); // should have already been cleared
return NO_MATCH;
}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
MethodSymbol symbol = getSymbol(tree);
// TODO(cpovirk): determine whether anyone might be calling Foo.this()
if (!isIdentifierWithName(tree.getMethodSelect(), "this")) {
return NO_MATCH;
}
callersToEvaluate.put(symbol, new Caller(tree, state));
return evaluateCallers(symbol);
}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
MethodSymbol symbol = getSymbol(tree);
if (!symbol.isConstructor()) {
return NO_MATCH;
}
paramTypesForMethod.put(symbol, unmodifiableList(tree.getParameters()));
return evaluateCallers(symbol);
}
private Description evaluateCallers(MethodSymbol symbol) {
List<VariableTree> paramTypes = paramTypesForMethod.get(symbol);
if (paramTypes == null) {
// We haven't seen the declaration yet. We'll evaluate the call when we do.
return NO_MATCH;
}
for (Caller caller : callersToEvaluate.removeAll(symbol)) {
VisitorState state = caller.state;
MethodInvocationTree invocation = caller.tree;
MethodTree callerConstructor = state.findEnclosing(MethodTree.class);
if (callerConstructor == null) {
continue; // impossible, at least in compilable code?
}
Map<String, Type> availableParams = indexTypeByName(callerConstructor.getParameters());
/*
* TODO(cpovirk): Better handling of varargs: If the last parameter type is varargs and it is
* called as varargs (rather than by passing an array), then rewrite the parameter types to
* (p0, p1, ..., p[n-2], p[n-1] = element type of varargs parameter if an argument is
* supplied, p[n] = ditto, etc.). For now, we settle for not crashing in the face of a
* mismatch between the number of parameters declared and the number supplied.
*
* (Use MethodSymbol.isVarArgs.)
*/
for (int i = 0; i < paramTypes.size() && i < invocation.getArguments().size(); i++) {
VariableTree formalParam = paramTypes.get(i);
String formalParamName = formalParam.getName().toString();
Type formalParamType = getType(formalParam.getType());
Type availableParamType = availableParams.get(formalParamName);
ExpressionTree actualParam = invocation.getArguments().get(i);
if (
/*
* The caller has no param of this type. (Or if it did, we couldn't determine the type.
* Does that ever happen?) If the param doesn't exist, the caller can't be failing to
* pass it.
*/
availableParamType == null
/*
* We couldn't determine the type of the formal parameter. (Does this ever happen?)
*/
|| formalParamType == null
/*
* The caller is passing the expected parameter (or "ImmutableList.copyOf(parameter),"
* "new File(parameter)," etc.).
*/
|| referencesIdentifierWithName(formalParamName, actualParam, state)) {
continue;
}
if (state.getTypes().isAssignable(availableParamType, formalParamType)) {
reportMatch(invocation, state, actualParam, formalParamName);
}
/*
* If formal parameter is of an incompatible type, the caller might in theory still intend
* to pass a dervied expression. For example, "Foo(String file)" might intend to call
* "Foo(File file)" by passing "new File(file)." If this comes up in practice, we could
* provide the dummy suggested fix "someExpression(formalParamName)." However, my research
* suggests that this will rarely if ever be what the user wants.
*/
}
}
// All matches are reported through reportMatch calls instead of return values.
return NO_MATCH;
}
private static Map<String, Type> indexTypeByName(List<? extends VariableTree> parameters) {
Map<String, Type> result = newHashMap();
for (VariableTree parameter : parameters) {
result.put(parameter.getName().toString(), getType(parameter.getType()));
}
return result;
}
private void reportMatch(
Tree diagnosticPosition, VisitorState state, Tree toReplace, String replaceWith) {
report(diagnosticPosition,
describeMatch(diagnosticPosition, replace(toReplace, replaceWith)),
state);
}
// Cloned from Scanner. This also appears in ThreadSafe. See the TODO there.
private void report(Tree diagnosticPosition, Description description, VisitorState state) {
state.getMatchListener().onMatch(diagnosticPosition);
state.getDescriptionListener().onDescribed(description);
}
private static boolean referencesIdentifierWithName(
final String name, ExpressionTree tree, VisitorState state) {
Matcher<IdentifierTree> identifierMatcher = new Matcher<IdentifierTree>() {
@Override
public boolean matches(IdentifierTree tree, VisitorState state) {
return isIdentifierWithName(tree, name);
}
};
return hasIdentifier(ANY, identifierMatcher).matches(tree, state);
}
private static boolean isIdentifierWithName(ExpressionTree tree, String name) {
return tree.getKind() == IDENTIFIER && ((IdentifierTree) tree).getName().contentEquals(name);
}
private static final class Caller {
final MethodInvocationTree tree;
final VisitorState state;
Caller(MethodInvocationTree tree, VisitorState state) {
this.tree = tree;
this.state = state;
}
}
}