/* * 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.collectionincompatibletype; import static com.google.errorprone.BugPattern.Category.JDK; 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.errorprone.BugPattern; import com.google.errorprone.VisitorState; import com.google.errorprone.annotations.CompatibleWith; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; 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.VarSymbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Types; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.lang.model.element.Parameterizable; import javax.lang.model.element.TypeParameterElement; /** @author glorioso@google.com (Nick Glorioso) */ @BugPattern( name = "IncompatibleArgumentType", summary = "Passing argument to a generic method with an incompatible type.", category = JDK, severity = ERROR ) public class IncompatibleArgumentType extends BugChecker implements MethodInvocationTreeMatcher { // 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 { abstract @Nullable Type type(); static RequiredType create(Type type) { return new AutoValue_IncompatibleArgumentType_RequiredType(type); } } @Override public Description matchMethodInvocation( MethodInvocationTree methodInvocationTree, VisitorState state) { Type calledMethodType = ASTHelpers.getType(methodInvocationTree.getMethodSelect()); Type calledClazzType = ASTHelpers.getReceiverType(methodInvocationTree); List<? extends ExpressionTree> arguments = methodInvocationTree.getArguments(); MethodSymbol declaredMethod = ASTHelpers.getSymbol(methodInvocationTree); if (arguments.isEmpty() || declaredMethod == null) { return null; } List<RequiredType> 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<? extends ExpressionTree> arguments, List<RequiredType> requiredTypesAtCallSite, VisitorState state) { Types types = state.getTypes(); for (int i = 0; i < requiredTypesAtCallSite.size(); i++) { RequiredType requiredType = requiredTypesAtCallSite.get(i); if (requiredType == null) { continue; } ExpressionTree argument = arguments.get(i); Type argType = ASTHelpers.getType(argument); if (requiredType.type() != null && !types.isCastable( argType, types.erasure(ASTHelpers.getUpperBound(requiredType.type(), types)))) { // Report a violation for this type state.reportMatch(describeViolation(argument, argType, requiredType.type(), types)); } } } private Description describeViolation( ExpressionTree argument, Type argType, Type requiredType, Types types) { // 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.", argument, sourceType, targetType); return buildDescription(argument).setMessage(msg).build(); } @CheckReturnValue private boolean populateTypesToEnforce( MethodSymbol declaredMethod, Type calledMethodType, Type calledReceiverType, List<RequiredType> requiredTypesAtCallSite, VisitorState state) { // We'll only search the first method in the hierarchy with an annotation. boolean found = false; com.sun.tools.javac.util.List<VarSymbol> 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) { found = true; if (requiredTypesAtCallSite.size() <= i) { // varargs method with 0 args passed from the caller side // void foo(String...); foo(); break; } // 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()); requiredTypesAtCallSite.set(i, requiredType); } } return found; } @Nullable private 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; } private RequiredType resolveTypeFromGenericMethod( Type calledMethodType, MethodSymbol declaredMethod, String typeArgName) { int tyargIndex = findTypeArgInList(declaredMethod, typeArgName); if (tyargIndex != -1) { return RequiredType.create(getTypeFromTypeMapping(calledMethodType, typeArgName)); } return null; } @SuppressWarnings("unchecked") // Plumb through a type which is supposed to be a Types.Subst, then find the replacement // type that the compiler resolved. private static Type getTypeFromTypeMapping(Type m, String namedTypeArg) { try { // Reflectively extract the mapping from an enclosing instance of Types.Subst Field substField = m.getClass().getDeclaredField("this$0"); substField.setAccessible(true); Object subst = substField.get(m); Field fromField = subst.getClass().getDeclaredField("from"); Field toField = subst.getClass().getDeclaredField("to"); fromField.setAccessible(true); toField.setAccessible(true); // Search for named in from, and return the parallel instance in to. List<Type> types = (List<Type>) fromField.get(subst); List<Type> calledTypes = (List<Type>) toField.get(subst); for (int i = 0; i < types.size(); i++) { Type type = types.get(i); if (type.toString().equals(namedTypeArg)) { return calledTypes.get(i); } } } catch (ReflectiveOperationException ignored) { // Nothing we can do here. } return null; } // class Foo<X> { void something(@CW("X") Object x); } // new Foo<String>().something(123); @Nullable private 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<T> { // class Bar { // void something(@CW("T") Object o)); // } // } // new Foo<String>().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<String>().new Bar().something(123); // Yep // Foo<String>.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<? extends TypeParameterElement> typeParameters = hasTypeParams.getTypeParameters(); for (int i = 0; i < typeParameters.size(); i++) { if (typeParameters.get(i).getSimpleName().contentEquals(typeArgName)) { return i; } } return -1; } }