All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.errorprone.bugpatterns.TypeCompatibilityUtils Maven / Gradle / Ivy

/*
 * Copyright 2020 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;

import static com.google.common.collect.Iterables.isEmpty;
import static com.google.errorprone.util.ASTHelpers.enclosingPackage;
import static com.google.errorprone.util.ASTHelpers.findMatchingMethods;
import static com.google.errorprone.util.ASTHelpers.getUpperBound;
import static com.google.errorprone.util.ASTHelpers.isCastable;
import static com.google.errorprone.util.ASTHelpers.isSameType;
import static com.google.errorprone.util.ASTHelpers.isSubtype;
import static com.google.errorprone.util.ASTHelpers.scope;

import com.google.auto.value.AutoValue;
import com.google.common.collect.Streams;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.suppliers.Supplier;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.TypeTag;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Names;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.Nullable;
import javax.lang.model.type.TypeKind;

/**
 * Logical utility methods to answer the question: Are these two types "compatible" with each other,
 * in the context of an equality check.
 *
 * 

i.e.: It is possible that an object of one type could be equal to an object of the other type. */ public final class TypeCompatibilityUtils { private static final String WITHOUT_EQUALS_REASON = ". Though these types are the same, the type doesn't implement equals."; private final boolean treatBuildersAsIncomparable; public static TypeCompatibilityUtils fromFlags(ErrorProneFlags flags) { return new TypeCompatibilityUtils( flags.getBoolean("TypeCompatibility:TreatBuildersAsIncomparable").orElse(true)); } public static TypeCompatibilityUtils allOn() { return new TypeCompatibilityUtils(/* treatBuildersAsIncomparable= */ true); } private TypeCompatibilityUtils(boolean treatBuildersAsIncomparable) { this.treatBuildersAsIncomparable = treatBuildersAsIncomparable; } public TypeCompatibilityReport compatibilityOfTypes( Type receiverType, Type argumentType, VisitorState state) { return compatibilityOfTypes(receiverType, argumentType, typeSet(state), typeSet(state), state); } private TypeCompatibilityReport compatibilityOfTypes( Type leftType, Type rightType, Set previouslySeenComponentsOfLeftType, Set previouslySeenComponentsOfRightType, VisitorState state) { if (leftType == null || rightType == null || leftType.getKind() == TypeKind.NULL || rightType.getKind() == TypeKind.NULL) { return TypeCompatibilityReport.compatible(); } Types types = state.getTypes(); Type leftUpperBound = getUpperBound(leftType, types); Type rightUpperBound = getUpperBound(rightType, types); if (types.isSameType(leftUpperBound, rightUpperBound)) { // As a special case, consider Builder classes without an equals implementation to not be // comparable to themselves. It would be reasonable to mistake Builders as having value // semantics, which may be misleading. if (treatBuildersAsIncomparable && !leftUpperBound.tsym.isEnum() && leftUpperBound.isFinal() && leftUpperBound.tsym.name.toString().endsWith("Builder")) { Names names = state.getNames(); MethodSymbol equals = (MethodSymbol) state.getSymtab().objectType.tsym.members().findFirst(state.getNames().equals); return isEmpty( scope(types.membersClosure(leftUpperBound, /* skipInterface= */ false)) .getSymbolsByName( names.toString, m -> m != equals && m.overrides( equals, leftUpperBound.tsym, types, /* checkResult= */ false))) ? TypeCompatibilityReport.incompatible(leftType, rightType, WITHOUT_EQUALS_REASON) : TypeCompatibilityReport.compatible(); } return TypeCompatibilityReport.compatible(); } if (leftType.isPrimitive() && rightType.isPrimitive() && !isSameType(leftType, rightType, state)) { return TypeCompatibilityReport.incompatible(leftType, rightType); } if (!isFeasiblyCompatible(leftType, rightType, state)) { return TypeCompatibilityReport.incompatible(leftType, rightType); } // Now, see if we can find a generic superclass between the two types, and if so, check the // generic parameters for cast-compatibility: // class Super (with an equals() override) // class Bar extends Super // class Foo extends Super // Bar and Foo would least-upper-bound to Super, and we compare String and Integer to each-other Type erasedLeftType = types.erasure(leftType); Type erasedRightType = types.erasure(rightType); Type commonSupertype = types.lub(erasedRightType, erasedLeftType); // primitives, etc. can't have a common superclass. if (commonSupertype.getTag().equals(TypeTag.BOT) || commonSupertype.getTag().equals(TypeTag.ERROR)) { return TypeCompatibilityReport.compatible(); } // Detect a potential generics mismatch - if there are no generics, this will return a // compatible report. TypeCompatibilityReport compatibilityReport = checkForGenericsMismatch( leftType, rightType, commonSupertype, previouslySeenComponentsOfLeftType, previouslySeenComponentsOfRightType, state); if (!compatibilityReport.isCompatible()) { return compatibilityReport; } // At this point, we're pretty sure these types are compatible with each other. However, there // are certain scenarios where the normal processing believes that these two types are // compatible, but due to the way that certain classes' equals methods are constructed, they // deceive the normal processing into thinking they're compatible, but they are not. return areTypesIncompatibleCollections(leftType, rightType, commonSupertype, state) || areIncompatibleProtoTypes(erasedLeftType, erasedRightType, commonSupertype, state) ? TypeCompatibilityReport.incompatible(leftType, rightType) : TypeCompatibilityReport.compatible(); } private static boolean isFeasiblyCompatible(Type leftType, Type rightType, VisitorState state) { // If one type can be cast into the other, they are potentially equal to each other. // Note: we do this precisely in this order to allow primitive values to be checked pre-1.7: // 1.6: java.lang.Object can't be cast to primitives // 1.7: java.lang.Object can be cast to primitives (implicitly through the boxed primitive type) if (isCastable(rightType, leftType, state)) { return true; } // Otherwise, we collect all overrides of java.lang.Object.equals() that the left type has. // If one of those overrides is inherited by the right type, then they share a common supertype // that defines its own equals method. Types types = state.getTypes(); Symbol rightClass = getUpperBound(rightType, state.getTypes()).tsym; return findMatchingMethods( EQUALS.get(state), m -> customEqualsMethod(m, state), leftType, types) .stream() .anyMatch(method -> rightClass.isSubClass(method.enclClass(), types)); } /** * Returns if the method represents an override of equals(Object) that is not on `Object` or * `Enum`. * *

This would represent an equals method that could specify equality semantics aside from * object identity. */ private static boolean customEqualsMethod(MethodSymbol methodSymbol, VisitorState state) { ClassSymbol owningClass = methodSymbol.enclClass(); return !methodSymbol.isStatic() && ((methodSymbol.flags() & Flags.SYNTHETIC) == 0) && state.getTypes().isSameType(methodSymbol.getReturnType(), state.getSymtab().booleanType) && methodSymbol.getParameters().size() == 1 && state .getTypes() .isSameType(methodSymbol.getParameters().get(0).type, state.getSymtab().objectType) && !owningClass.equals(state.getSymtab().objectType.tsym) && !owningClass.equals(state.getSymtab().enumSym); } /** * Detects the situation where two distinct subtypes of Collection (e.g.: List or Set) are * compared. * *

Since they share a common interface with an equals() declaration ({@code Collection}), * normal processing will consider them compatible with each other, but each subtype of Collection * overrides equals() in such a way as to be incompatible with each other. */ private static boolean areTypesIncompatibleCollections( Type leftType, Type rightType, Type nearestCommonSupertype, VisitorState state) { // We want to disallow equality between these collection sub-interfaces, but *do* want to // allow compatibility between Collection and List. Type collectionType = JAVA_UTIL_COLLECTION.get(state); return isSameType(nearestCommonSupertype, collectionType, state) && !isSameType(leftType, collectionType, state) && !isSameType(rightType, collectionType, state); } private boolean areIncompatibleProtoTypes( Type leftType, Type rightType, Type nearestCommonSupertype, VisitorState state) { // See discussion in b/152428396 - Proto equality is defined as having the "same message type", // with the same corresponding field values. However - there are 3 flavors of Java Proto API // that could represent the same message (proto1, mutable proto2, and immutable proto2 [as well // as immutable proto3, but those look like proto2 classes without "hazzer" method]). // // Protos share a common super-interface that defines an equals() method, but since every proto // message shares that supertype, we shouldn't let that shared equals() definition override our // attempts to find mismatched equals() between "really" unrelated objects. // // We do our best here to identify circumstances where the programmer likely got protos that are // unrelated (requiring special treatment above and beyond the normal logic in // compatibilityOfTypes), but ignore cases where the protos are *actually* equal to each other // according to its definition. // proto1: io.ProtocolMessage < p.AbstractMutableMessage < p.MutableMessage < p.Message // proto2-mutable: p.GeneratedMutableMessage < p.AbstractMutableMessage < ... as proto1 // proto2-immutable: p.GeneratedMessage < p.AbstractMessage < p.Message // DynamicMessage is comparable to all other proto types. Type dynamicMessage = COM_GOOGLE_PROTOBUF_DYNAMICMESSAGE.get(state); if (isSameType(leftType, dynamicMessage, state) || isSameType(rightType, dynamicMessage, state)) { return false; } Type protoBase = COM_GOOGLE_PROTOBUF_MESSAGE.get(state); if (isSameType(nearestCommonSupertype, protoBase, state) && !isSameType(leftType, protoBase, state) && !isSameType(rightType, protoBase, state)) { // In this situation, there's a mix of (immutable proto2, others) messages. Here, we want to // figure out if the other is a proto2-mutable representing the same message/type as the // immutable proto. // While proto2 can be compared to proto1, this is somewhat of a historical accident. We don't // want to endorse this, and want to ban it. Protos of the same version and same descriptor // should be comparable, hence proto2-immutable and proto2-mutable can be. // See b/152428396#comment10, but basically, we inspect the names of the classes to guess // whether or not those types are actually representing the same type. String leftClassPart = classNamePart(leftType); String rightClassPart = classNamePart(rightType); return !classesAreMutableAndImmutableOfSameType(leftClassPart, rightClassPart) && !classesAreMutableAndImmutableOfSameType(rightClassPart, leftClassPart); } // Otherwise, if these two types are *concrete* proto classes, but not the same message, then // consider them incompatible with each other. Type messageLite = COM_GOOGLE_PROTOBUF_MESSAGELITE.get(state); return isSubtype(nearestCommonSupertype, messageLite, state) && isConcrete(leftType, state.getTypes()) && isConcrete(rightType, state.getTypes()); } private static boolean isConcrete(Type type, Types types) { Type toEvaluate = getUpperBound(type, types); return (toEvaluate.tsym.flags() & (Flags.ABSTRACT | Flags.INTERFACE)) == 0; } private static boolean classesAreMutableAndImmutableOfSameType(String l, String r) { return l.startsWith("Mutable") && l.substring("Mutable".length()).equals(r); } private static String classNamePart(Type type) { String fullClassname = type.asElement().getQualifiedName().toString(); String packageName = enclosingPackage(type.asElement()).fullname.toString(); String prefix = fullClassname.substring(packageName.length()); return prefix.startsWith(".") ? prefix.substring(1) : prefix; } /** * Given a {@code leftType} and {@code rightType} (of the shared supertype {@code superType}), * compare the generic types of those two types (as projected against the {@code superType}) * against each other. * *

If there are no generic types, or the generic types are compatible with each other, returns * a {@link TypeCompatibilityReport#isCompatible} report. Otherwise, returns a compatibility * report showing that a specific generic type in the projection of {@code leftType} is * incompatible with the specific generic type in the projection of {@code rightType}. */ private TypeCompatibilityReport checkForGenericsMismatch( Type leftType, Type rightType, Type superType, Set previousLeftTypes, Set previousRightTypes, VisitorState state) { List leftGenericTypes = typeArgsAsSuper(leftType, superType, state); List rightGenericTypes = typeArgsAsSuper(rightType, superType, state); return Streams.zip(leftGenericTypes.stream(), rightGenericTypes.stream(), TypePair::new) // If we encounter an f-bound, skip that index's type when comparing the compatibility of // types to avoid infinite recursion: // interface Super, B> // class Foo extends Super // class Bar extends Super .filter( tp -> !(previousLeftTypes.contains(tp.left) || isSameType(tp.left, leftType, state) || previousRightTypes.contains(tp.right) || isSameType(tp.right, rightType, state))) .map( types -> { Set nextLeftTypes = typeSet(state); nextLeftTypes.addAll(previousLeftTypes); nextLeftTypes.add(leftType); Set nextRightTypes = typeSet(state); nextRightTypes.addAll(previousRightTypes); nextRightTypes.add(rightType); return compatibilityOfTypes( types.left, types.right, nextLeftTypes, nextRightTypes, state); }) .filter(tcr -> !tcr.isCompatible()) .findFirst() .orElse(TypeCompatibilityReport.compatible()); } private static List typeArgsAsSuper(Type baseType, Type superType, VisitorState state) { Type projectedType = state.getTypes().asSuper(baseType, superType.tsym); if (projectedType != null) { return projectedType.getTypeArguments(); } return new ArrayList<>(); } private static TreeSet typeSet(VisitorState state) { return new TreeSet<>( (t1, t2) -> state.getTypes().isSameType(t1, t2) ? 0 : t1.toString().compareTo(t2.toString())); } @AutoValue public abstract static class TypeCompatibilityReport { private static final TypeCompatibilityReport COMPATIBLE = new AutoValue_TypeCompatibilityUtils_TypeCompatibilityReport(true, null, null, null); public abstract boolean isCompatible(); @Nullable public abstract Type lhs(); @Nullable public abstract Type rhs(); @Nullable public abstract String extraReason(); static TypeCompatibilityReport compatible() { return COMPATIBLE; } static TypeCompatibilityReport incompatible(Type lhs, Type rhs) { return incompatible(lhs, rhs, ""); } static TypeCompatibilityReport incompatible(Type lhs, Type rhs, String extraReason) { return new AutoValue_TypeCompatibilityUtils_TypeCompatibilityReport( false, lhs, rhs, extraReason); } } private static final class TypePair { final Type left; final Type right; TypePair(Type left, Type right) { this.left = left; this.right = right; } } private static final Supplier EQUALS = VisitorState.memoize(state -> state.getName("equals")); private static final Supplier COM_GOOGLE_PROTOBUF_DYNAMICMESSAGE = VisitorState.memoize(state -> state.getTypeFromString("com.google.protobuf.DynamicMessage")); private static final Supplier COM_GOOGLE_PROTOBUF_MESSAGE = VisitorState.memoize(state -> state.getTypeFromString("com.google.protobuf.Message")); private static final Supplier COM_GOOGLE_PROTOBUF_MESSAGELITE = VisitorState.memoize(state -> state.getTypeFromString("com.google.protobuf.MessageLite")); private static final Supplier JAVA_UTIL_COLLECTION = VisitorState.memoize(state -> state.getTypeFromString("java.util.Collection")); }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy