com.google.errorprone.bugpatterns.collectionincompatibletype.IncompatibleArgumentType Maven / Gradle / Ivy
/*
* Copyright 2016 The Error Prone Authors.
*
* 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.collectionincompatibletype;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.bugpatterns.collectionincompatibletype.AbstractCollectionIncompatibleTypeMatcher.extractTypeArgAsMemberOfSupertype;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableListMultimap;
import com.google.errorprone.BugPattern;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.CompatibleWith;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.bugpatterns.TypeCompatibilityUtils;
import com.google.errorprone.bugpatterns.TypeCompatibilityUtils.TypeCompatibilityReport;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.google.errorprone.util.Signatures;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.TypeVariableSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import javax.lang.model.element.Parameterizable;
import javax.lang.model.element.TypeParameterElement;
/**
* @author [email protected] (Nick Glorioso)
*/
@BugPattern(
summary = "Passing argument to a generic method with an incompatible type.",
severity = ERROR)
public class IncompatibleArgumentType extends BugChecker implements MethodInvocationTreeMatcher {
private final TypeCompatibilityUtils typeCompatibilityUtils;
public IncompatibleArgumentType(ErrorProneFlags flags) {
this.typeCompatibilityUtils = TypeCompatibilityUtils.fromFlags(flags);
}
// Nonnull requiredType: The type I need is bound, in requiredType
// null requiredType: I found the type variable, but I can't bind it to any type
@AutoValue
abstract static class RequiredType {
@Nullable
abstract Type type();
static RequiredType create(Type type) {
return new AutoValue_IncompatibleArgumentType_RequiredType(type);
}
}
@Override
public Description matchMethodInvocation(
MethodInvocationTree methodInvocationTree, VisitorState state) {
// example:
// class Foo {
// void bar(@CompatibleWith("A") Object o, @CompatibleWith("B") Object o2) {}
// }
// new Foo().bar(1, "a');
// A Type substitution capturing on Foo and on bar(Object,Object);
Type calledMethodType = ASTHelpers.getType(methodInvocationTree.getMethodSelect());
// A Type substitution capturing on Foo
Type calledClazzType = ASTHelpers.getReceiverType(methodInvocationTree);
List arguments = methodInvocationTree.getArguments();
// The unbound MethodSymbol for bar(), with type parameters and
MethodSymbol declaredMethod = ASTHelpers.getSymbol(methodInvocationTree);
if (arguments.isEmpty()) {
return Description.NO_MATCH;
}
List requiredTypesAtCallSite =
new ArrayList<>(Collections.nCopies(arguments.size(), null));
Types types = state.getTypes();
if (!populateTypesToEnforce(
declaredMethod, calledMethodType, calledClazzType, requiredTypesAtCallSite, state)) {
// No annotations on this method, try the supers;
for (MethodSymbol method : ASTHelpers.findSuperMethods(declaredMethod, types)) {
if (populateTypesToEnforce(
method, calledMethodType, calledClazzType, requiredTypesAtCallSite, state)) {
break;
}
}
}
reportAnyViolations(arguments, requiredTypesAtCallSite, state);
// We manually report ourselves, so we don't pass any errors up the chain.
return Description.NO_MATCH;
}
private void reportAnyViolations(
List arguments,
List requiredArgumentTypes,
VisitorState state) {
Types types = state.getTypes();
for (int i = 0; i < requiredArgumentTypes.size(); i++) {
RequiredType requiredType = requiredArgumentTypes.get(i);
if (requiredType == null) {
continue;
}
ExpressionTree argument = arguments.get(i);
Type argType = ASTHelpers.getType(argument);
if (requiredType.type() != null) {
// Report a violation for this type
TypeCompatibilityReport report =
typeCompatibilityUtils.compatibilityOfTypes(requiredType.type(), argType, state);
if (!report.isCompatible()) {
state.reportMatch(
describeViolation(argument, argType, requiredType.type(), types, state));
}
}
}
}
private Description describeViolation(
ExpressionTree argument, Type argType, Type requiredType, Types types, VisitorState state) {
// For the error message, use simple names instead of fully qualified names unless they are
// identical.
String sourceType = Signatures.prettyType(argType);
String targetType = Signatures.prettyType(ASTHelpers.getUpperBound(requiredType, types));
if (sourceType.equals(targetType)) {
sourceType = argType.toString();
targetType = requiredType.toString();
}
String msg =
String.format(
"Argument '%s' should not be passed to this method. Its type %s is not"
+ " compatible with the required type: %s.",
state.getSourceForNode(argument), sourceType, targetType);
return buildDescription(argument).setMessage(msg).build();
}
// Return whether this method contains any @CompatibleWith annotations. If there are none, the
// caller should explore super-methods.
@CheckReturnValue
private static boolean populateTypesToEnforce(
MethodSymbol declaredMethod,
Type calledMethodType,
Type calledReceiverType,
List argumentTypeRequirements,
VisitorState state) {
boolean foundAnyTypeToEnforce = false;
List params = declaredMethod.params();
for (int i = 0; i < params.size(); i++) {
VarSymbol varSymbol = params.get(i);
CompatibleWith anno = ASTHelpers.getAnnotation(varSymbol, CompatibleWith.class);
if (anno != null) {
foundAnyTypeToEnforce = true;
// Now we try and resolve the generic type argument in the annotation against the current
// method call's projection of this generic type.
RequiredType requiredType =
resolveRequiredTypeForThisCall(
state, calledMethodType, calledReceiverType, declaredMethod, anno.value());
// @CW is on the varags parameter
if (declaredMethod.isVarArgs() && i == params.size() - 1) {
if (i >= argumentTypeRequirements.size()) {
// varargs method with 0 args passed from the caller side, no arguments to enforce
// void foo(String...); foo();
break;
} else {
// Set this required type for all of the arguments in the varargs position.
for (int j = i; j < argumentTypeRequirements.size(); j++) {
argumentTypeRequirements.set(j, requiredType);
}
}
} else {
argumentTypeRequirements.set(i, requiredType);
}
}
}
return foundAnyTypeToEnforce;
}
@Nullable
@CheckReturnValue
// From calledReceiverType
private static RequiredType resolveRequiredTypeForThisCall(
VisitorState state,
Type calledMethodType,
Type calledReceiverType,
MethodSymbol declaredMethod,
String typeArgName) {
RequiredType requiredType =
resolveTypeFromGenericMethod(calledMethodType, declaredMethod, typeArgName);
if (requiredType == null) {
requiredType =
resolveTypeFromClass(
calledReceiverType, (ClassSymbol) declaredMethod.owner, typeArgName, state);
}
return requiredType;
}
@Nullable
private static RequiredType resolveTypeFromGenericMethod(
Type calledMethodType, MethodSymbol declaredMethod, String typeArgName) {
int tyargIndex = findTypeArgInList(declaredMethod, typeArgName);
return tyargIndex == -1
? null
: RequiredType.create(
getTypeFromTypeMapping(calledMethodType, declaredMethod, typeArgName));
}
// Plumb through a type which is supposed to be a Types.Subst, then find the replacement
// type that the compiler resolved.
@Nullable
private static Type getTypeFromTypeMapping(
Type m, MethodSymbol declaredMethod, String namedTypeArg) {
ImmutableListMultimap substitutions =
ASTHelpers.getTypeSubstitution(m, declaredMethod);
for (Map.Entry e : substitutions.entries()) {
if (e.getKey().getSimpleName().contentEquals(namedTypeArg)) {
return e.getValue();
}
}
return null;
}
// class Foo { void something(@CW("X") Object x); }
// new Foo().something(123);
@Nullable
private static RequiredType resolveTypeFromClass(
Type calledType, ClassSymbol clazzSymbol, String typeArgName, VisitorState state) {
// Try on the class
int tyargIndex = findTypeArgInList(clazzSymbol, typeArgName);
if (tyargIndex != -1) {
return RequiredType.create(
extractTypeArgAsMemberOfSupertype(calledType, clazzSymbol, tyargIndex, state.getTypes()));
}
while (clazzSymbol.isInner()) {
// class Foo {
// class Bar {
// void something(@CW("T") Object o));
// }
// }
// new Foo().new Bar().something(123); // should fail, 123 needs to match String
ClassSymbol encloser = clazzSymbol.owner.enclClass();
calledType = calledType.getEnclosingType();
tyargIndex = findTypeArgInList(encloser, typeArgName);
if (tyargIndex != -1) {
if (calledType.getTypeArguments().isEmpty()) {
// If the receiver is held in a reference without the enclosing class's type arguments, we
// can't determine the required type:
// new Foo().new Bar().something(123); // Yep
// Foo.Bar bar = ...;
// bar.something(123); // Yep
// Foo.Bar bar = ...;
// bar.something(123); // Nope (this call would be unchecked if arg was T)
return null;
}
return RequiredType.create(calledType.getTypeArguments().get(tyargIndex));
}
clazzSymbol = encloser;
}
return null;
}
private static int findTypeArgInList(Parameterizable hasTypeParams, String typeArgName) {
List typeParameters = hasTypeParams.getTypeParameters();
for (int i = 0; i < typeParameters.size(); i++) {
if (typeParameters.get(i).getSimpleName().contentEquals(typeArgName)) {
return i;
}
}
return -1;
}
}