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

dagger.internal.codegen.xprocessing.XTypes Maven / Gradle / Ivy

/*
 * Copyright (C) 2021 The Dagger 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 dagger.internal.codegen.xprocessing;

import static androidx.room.compiler.processing.XTypeKt.isArray;
import static androidx.room.compiler.processing.XTypeKt.isVoid;
import static androidx.room.compiler.processing.compat.XConverters.getProcessingEnv;
import static androidx.room.compiler.processing.compat.XConverters.toJavac;
import static androidx.room.compiler.processing.compat.XConverters.toXProcessing;
import static com.google.auto.common.MoreTypes.asDeclared;
import static com.google.common.base.CaseFormat.LOWER_CAMEL;
import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Iterables.getOnlyElement;
import static dagger.internal.codegen.extension.DaggerCollectors.toOptional;
import static dagger.internal.codegen.xprocessing.XTypes.asArray;
import static dagger.internal.codegen.xprocessing.XTypes.isDeclared;
import static dagger.internal.codegen.xprocessing.XTypes.isNoType;
import static java.util.stream.Collectors.joining;

import androidx.room.compiler.processing.XArrayType;
import androidx.room.compiler.processing.XConstructorType;
import androidx.room.compiler.processing.XExecutableType;
import androidx.room.compiler.processing.XMethodType;
import androidx.room.compiler.processing.XProcessingEnv;
import androidx.room.compiler.processing.XType;
import androidx.room.compiler.processing.XTypeElement;
import androidx.room.compiler.processing.XTypeVariableType;
import com.google.auto.common.MoreElements;
import com.google.common.base.Equivalence;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeVariableName;
import com.squareup.javapoet.WildcardTypeName;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import javax.lang.model.element.Element;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ErrorType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.WildcardType;
import javax.lang.model.util.SimpleTypeVisitor8;

// TODO(bcorso): Consider moving these methods into XProcessing library.
/** A utility class for {@link XType} helper methods. */
public final class XTypes {
  private static class XTypeEquivalence extends Equivalence {
    private final boolean ignoreVariance;

    XTypeEquivalence(boolean ignoreVariance) {
      this.ignoreVariance = ignoreVariance;
    }

    @Override
    protected boolean doEquivalent(XType left, XType right) {
      return getTypeName(left).equals(getTypeName(right));
    }

    @Override
    protected int doHash(XType type) {
      return getTypeName(type).hashCode();
    }

    @Override
    public String toString() {
      return "XTypes.equivalence()";
    }

    private TypeName getTypeName(XType type) {
      return ignoreVariance ? stripVariances(type.getTypeName()) : type.getTypeName();
    }
  }

  public static TypeName stripVariances(TypeName typeName) {
    if (typeName instanceof WildcardTypeName) {
      WildcardTypeName wildcardTypeName = (WildcardTypeName) typeName;
      if (!wildcardTypeName.lowerBounds.isEmpty()) {
        return stripVariances(getOnlyElement(wildcardTypeName.lowerBounds));
      } else if (!wildcardTypeName.upperBounds.isEmpty()) {
        return stripVariances(getOnlyElement(wildcardTypeName.upperBounds));
      }
    } else if (typeName instanceof ArrayTypeName) {
      ArrayTypeName arrayTypeName = (ArrayTypeName) typeName;
      return ArrayTypeName.of(stripVariances(arrayTypeName.componentType));
    } else if (typeName instanceof ParameterizedTypeName) {
      ParameterizedTypeName parameterizedTypeName = (ParameterizedTypeName) typeName;
      if (parameterizedTypeName.typeArguments.isEmpty()) {
        return parameterizedTypeName;
      } else {
        return ParameterizedTypeName.get(
            parameterizedTypeName.rawType,
            parameterizedTypeName.typeArguments.stream()
                .map(XTypes::stripVariances)
                .toArray(TypeName[]::new));
      }
    }
    return typeName;
  }

  private static final Equivalence XTYPE_EQUIVALENCE_IGNORING_VARIANCE =
      new XTypeEquivalence(/* ignoreVariance= */ true);

  /**
   * Returns an {@link Equivalence} for {@link XType} based on the {@link TypeName} with variances
   * ignored (e.g. {@code Foo} would be equivalent to {@code Foo}).
   *
   * 

Currently, this equivalence does not take into account nullability, as it just relies on * JavaPoet's {@link TypeName}. Thus, two types with the same type name but different nullability * are equal with this equivalence. */ public static Equivalence equivalenceIgnoringVariance() { return XTYPE_EQUIVALENCE_IGNORING_VARIANCE; } private static final Equivalence XTYPE_EQUIVALENCE = new XTypeEquivalence(/* ignoreVariance= */ false); /** * Returns an {@link Equivalence} for {@link XType} based on the {@link TypeName}. * *

Currently, this equivalence does not take into account nullability, as it just relies on * JavaPoet's {@link TypeName}. Thus, two types with the same type name but different nullability * are equal with this equivalence. */ public static Equivalence equivalence() { return XTYPE_EQUIVALENCE; } // TODO(bcorso): Support XType.getEnclosingType() properly in XProcessing. @SuppressWarnings("ReturnMissingNullable") public static XType getEnclosingType(XType type) { checkArgument(isDeclared(type)); XProcessingEnv.Backend backend = getProcessingEnv(type).getBackend(); switch (backend) { case JAVAC: return toXProcessing(asDeclared(toJavac(type)).getEnclosingType(), getProcessingEnv(type)); case KSP: // For now, just return the enclosing type of the XTypeElement, which for most cases is good // enough. This may be incorrect in some rare cases (not tested), e.g. if Outer.Inner // inherits its type parameter from Outer then the enclosing type of Outer.Inner // should be Outer rather than Outer, as we would get from the code below. XTypeElement enclosingTypeElement = type.getTypeElement().getEnclosingTypeElement(); return enclosingTypeElement == null ? null : enclosingTypeElement.getType(); } throw new AssertionError("Unexpected backend: " + backend); } /** Returns {@code true} if and only if the {@code type1} is assignable to {@code type2}. */ public static boolean isAssignableTo(XType type1, XType type2) { return type2.isAssignableFrom(type1); } /** Returns {@code true} if {@code type1} is a subtype of {@code type2}. */ public static boolean isSubtype(XType type1, XType type2) { XProcessingEnv processingEnv = getProcessingEnv(type1); switch (processingEnv.getBackend()) { case JAVAC: // The implementation used for KSP should technically also work in Javac but we avoid it to // avoid any possible regressions in Javac. return toJavac(processingEnv) .getTypeUtils() // ALLOW_TYPES_ELEMENTS .isSubtype(toJavac(type1), toJavac(type2)); case KSP: if (isPrimitive(type1) || isPrimitive(type2)) { // For primitive types we can't just check isAssignableTo since auto-boxing means boxed // types are assignable to primitive (and vice versa) though neither are subtypes. return type1.isSameType(type2); } return isAssignableTo(type1, type2); } throw new AssertionError("Unexpected backend: " + processingEnv.getBackend()); } /** Returns the erasure of the given {@link TypeName}. */ public static TypeName erasedTypeName(XType type) { XProcessingEnv processingEnv = getProcessingEnv(type); switch (processingEnv.getBackend()) { case JAVAC: // The implementation used for KSP should technically also work in Javac but we avoid it to // avoid any possible regressions in Javac. return toXProcessing( toJavac(processingEnv).getTypeUtils() // ALLOW_TYPES_ELEMENTS .erasure(toJavac(type)), processingEnv) .getTypeName(); case KSP: // In KSP, we have to derive the erased TypeName ourselves. return erasedTypeName(type.getTypeName()); } throw new AssertionError("Unexpected backend: " + processingEnv.getBackend()); } private static TypeName erasedTypeName(TypeName typeName) { // See https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.6 if (typeName instanceof ArrayTypeName) { // Erasure of 'C[]' is '|C|[]' return ArrayTypeName.of(erasedTypeName(((ArrayTypeName) typeName).componentType)); } else if (typeName instanceof ParameterizedTypeName) { // Erasure of 'C' is '|C|' // Erasure of nested type T.C is |T|.C // Nested types, e.g. Foo.Bar, are also represented as ParameterizedTypeName and // calling ParameterizedTypeName.rawType gives the correct result, e.g. Foo.Bar. return ((ParameterizedTypeName) typeName).rawType; } else if (typeName instanceof TypeVariableName) { // Erasure of type variable is the erasure of its left-most bound return erasedTypeName(((TypeVariableName) typeName).bounds.get(0)); } // For every other type, the erasure is the type itself. return typeName; } /** * Throws {@link TypeNotPresentException} if {@code type} is an {@link * javax.lang.model.type.ErrorType}. */ public static void checkTypePresent(XType type) { if (isArray(type)) { checkTypePresent(asArray(type).getComponentType()); } else if (isDeclared(type)) { type.getTypeArguments().forEach(XTypes::checkTypePresent); } else if (type.isError()) { throw new TypeNotPresentException(type.toString(), null); } } /** Returns {@code true} if the given type is a raw type of a parameterized type. */ public static boolean isRawParameterizedType(XType type) { XProcessingEnv processingEnv = getProcessingEnv(type); switch (processingEnv.getBackend()) { case JAVAC: return isDeclared(type) && type.getTypeArguments().isEmpty() && !type.getTypeElement().getType().getTypeArguments().isEmpty(); case KSP: return isDeclared(type) // TODO(b/245619245): Due to the bug in XProcessing, the logic used for Javac won't work // since XType#getTypeArguments() does not return an empty list for java raw types. // However, the type name seems to get it correct, so we compare the typename to the raw // typename until this bug is fixed. && type.getRawType() != null && type.getTypeName().equals(type.getRawType().getTypeName()) && !type.getTypeElement().getType().getTypeArguments().isEmpty(); } throw new AssertionError("Unexpected backend: " + processingEnv.getBackend()); } /** Returns the given {@code type} as an {@link XArrayType}. */ public static XArrayType asArray(XType type) { return (XArrayType) type; } /** Returns the given {@code type} as an {@link XTypeVariableType}. */ public static XTypeVariableType asTypeVariable(XType type) { return (XTypeVariableType) type; } /** Returns {@code true} if the raw type of {@code type} is equal to {@code className}. */ public static boolean isTypeOf(XType type, ClassName className) { return isDeclared(type) && type.getTypeElement().getClassName().equals(className); } /** Returns {@code true} if the given type represents the {@code null} type. */ public static boolean isNullType(XType type) { XProcessingEnv.Backend backend = getProcessingEnv(type).getBackend(); switch (backend) { case JAVAC: return toJavac(type).getKind().equals(TypeKind.NULL); // AFAICT, there's no way to actually get a "null" type in KSP's model case KSP: return false; } throw new AssertionError("Unexpected backend: " + backend); } /** Returns {@code true} if the given type has no actual type. */ public static boolean isNoType(XType type) { return type.isNone() || isVoid(type); } /** Returns {@code true} if the given type is a declared type. */ public static boolean isWildcard(XType type) { XProcessingEnv.Backend backend = getProcessingEnv(type).getBackend(); switch (backend) { case JAVAC: // In Javac, check the TypeKind directly. This also avoids a Javac bug (b/242569252) where // calling XType.getTypeName() too early caches an incorrect type name. return toJavac(type).getKind().equals(TypeKind.WILDCARD); case KSP: // TODO(bcorso): Consider representing this as an actual type in XProcessing. return type.getTypeName() instanceof WildcardTypeName; } throw new AssertionError("Unexpected backend: " + backend); } /** Returns {@code true} if the given type is a declared type. */ public static boolean isDeclared(XType type) { // TODO(b/241477426): Due to a bug in XProcessing, array types accidentally get assigned an // invalid XTypeElement, so we check explicitly until this is fixed. // TODO(b/242918001): Due to a bug in XProcessing, wildcard types accidentally get assigned an // invalid XTypeElement, so we check explicitly until this is fixed. return !isWildcard(type) && !isArray(type) && type.getTypeElement() != null; } /** Returns {@code true} if the given type is a type variable. */ public static boolean isTypeVariable(XType type) { // TODO(bcorso): Consider representing this as an actual type in XProcessing. return type.getTypeName() instanceof TypeVariableName; } /** Returns {@code true} if {@code type1} is equivalent to {@code type2}. */ public static boolean areEquivalentTypes(XType type1, XType type2) { return type1.getTypeName().equals(type2.getTypeName()); } /** Returns {@code true} if the given type is a primitive type. */ public static boolean isPrimitive(XType type) { // TODO(bcorso): Consider representing this as an actual type in XProcessing. return type.getTypeName().isPrimitive(); } /** Returns {@code true} if the given type has type parameters. */ public static boolean hasTypeParameters(XType type) { return !type.getTypeArguments().isEmpty(); } public static boolean isMethod(XExecutableType type) { return type instanceof XMethodType; } public static boolean isConstructor(XExecutableType type) { return type instanceof XConstructorType; } public static boolean isFloat(XType type) { return type.getTypeName().equals(TypeName.FLOAT) || type.getTypeName().equals(KnownTypeNames.BOXED_FLOAT); } public static boolean isShort(XType type) { return type.getTypeName().equals(TypeName.SHORT) || type.getTypeName().equals(KnownTypeNames.BOXED_SHORT); } public static boolean isChar(XType type) { return type.getTypeName().equals(TypeName.CHAR) || type.getTypeName().equals(KnownTypeNames.BOXED_CHAR); } public static boolean isDouble(XType type) { return type.getTypeName().equals(TypeName.DOUBLE) || type.getTypeName().equals(KnownTypeNames.BOXED_DOUBLE); } public static boolean isBoolean(XType type) { return type.getTypeName().equals(TypeName.BOOLEAN) || type.getTypeName().equals(KnownTypeNames.BOXED_BOOLEAN); } private static class KnownTypeNames { static final TypeName BOXED_SHORT = TypeName.SHORT.box(); static final TypeName BOXED_DOUBLE = TypeName.DOUBLE.box(); static final TypeName BOXED_FLOAT = TypeName.FLOAT.box(); static final TypeName BOXED_CHAR = TypeName.CHAR.box(); static final TypeName BOXED_BOOLEAN = TypeName.BOOLEAN.box(); } /** * Returns the non-{@link Object} superclass of the type with the proper type parameters. An empty * {@link Optional} is returned if there is no non-{@link Object} superclass. */ public static Optional nonObjectSuperclass(XType type) { if (!isDeclared(type)) { return Optional.empty(); } // We compare elements (rather than TypeName) here because its more efficient on the heap. XTypeElement objectElement = objectElement(getProcessingEnv(type)); XTypeElement typeElement = type.getTypeElement(); if (!typeElement.isClass() || typeElement.equals(objectElement)) { return Optional.empty(); } XType superClass = typeElement.getSuperClass(); if (!isDeclared(superClass)) { return Optional.empty(); } XTypeElement superClassElement = superClass.getTypeElement(); if (!superClassElement.isClass() || superClassElement.equals(objectElement)) { return Optional.empty(); } // TODO(b/310954522): XType#getSuperTypes() is less efficient (especially on the heap) as it // requires creating XType for not just superclass but all super interfaces as well, so we go // through a bit of effort here to avoid that call unless its absolutely necessary since // nonObjectSuperclass is called quite a bit via InjectionSiteFactory. However, we should // eventually optimize this on the XProcessing side instead, e.g. maybe separating // XType#getSuperClass() into a separate method. return superClass.getTypeArguments().isEmpty() ? Optional.of(superClass) : type.getSuperTypes().stream() .filter(XTypes::isDeclared) .filter(supertype -> supertype.getTypeElement().isClass()) .filter(supertype -> !supertype.getTypeElement().equals(objectElement)) .collect(toOptional()); } private static XTypeElement objectElement(XProcessingEnv processingEnv) { switch (processingEnv.getBackend()) { case JAVAC: return processingEnv.requireTypeElement(TypeName.OBJECT); case KSP: return processingEnv.requireTypeElement("kotlin.Any"); } throw new AssertionError("Unexpected backend: " + processingEnv.getBackend()); } /** * Returns {@code type}'s single type argument. * *

For example, if {@code type} is {@code List} this will return {@code Number}. * * @throws IllegalArgumentException if {@code type} is not a declared type or has zero or more * than one type arguments. */ public static XType unwrapType(XType type) { XType unwrapped = unwrapTypeOrDefault(type, null); checkArgument(unwrapped != null, "%s is a raw type", type); return unwrapped; } private static XType unwrapTypeOrDefault(XType type, XType defaultType) { // Check the type parameters of the element's XType since the input XType could be raw. checkArgument(isDeclared(type)); XTypeElement typeElement = type.getTypeElement(); checkArgument( typeElement.getType().getTypeArguments().size() == 1, "%s does not have exactly 1 type parameter. Found: %s", typeElement.getQualifiedName(), typeElement.getType().getTypeArguments()); return getOnlyElement(type.getTypeArguments(), defaultType); } /** * Returns {@code type}'s single type argument wrapped in {@code wrappingClass}. * *

For example, if {@code type} is {@code List} and {@code wrappingClass} is {@code * Set.class}, this will return {@code Set}. * *

If {@code type} has no type parameters, returns a {@link XType} for {@code wrappingClass} as * a raw type. * * @throws IllegalArgumentException if {@code} has more than one type argument. */ public static XType rewrapType(XType type, ClassName wrappingClassName) { XProcessingEnv processingEnv = getProcessingEnv(type); XTypeElement wrappingType = processingEnv.requireTypeElement(wrappingClassName.canonicalName()); switch (type.getTypeArguments().size()) { case 0: return processingEnv.getDeclaredType(wrappingType); case 1: return processingEnv.getDeclaredType(wrappingType, getOnlyElement(type.getTypeArguments())); default: throw new IllegalArgumentException(type + " has more than 1 type argument"); } } /** * Returns a string representation of {@link XType} that is independent of the backend * (javac/ksp). */ // TODO(b/241141586): Replace this with TypeName.toString(). Technically, TypeName.toString() // should already be independent of the backend but we supply our own custom implementation to // remain backwards compatible with the previous implementation, which used TypeMirror#toString(). public static String toStableString(XType type) { try { return toStableString(type.getTypeName()); } catch (TypeNotPresentException e) { return e.typeName(); } } private static String toStableString(TypeName typeName) { if (typeName instanceof ClassName) { return ((ClassName) typeName).canonicalName(); } else if (typeName instanceof ArrayTypeName) { return String.format( "%s[]", toStableString(((ArrayTypeName) typeName).componentType)); } else if (typeName instanceof ParameterizedTypeName) { ParameterizedTypeName parameterizedTypeName = (ParameterizedTypeName) typeName; return String.format( "%s<%s>", parameterizedTypeName.rawType, parameterizedTypeName.typeArguments.stream() .map(XTypes::toStableString) // We purposely don't use a space after the comma to for backwards compatibility with // usages that depended on the previous TypeMirror#toString() implementation. .collect(joining(","))); } else if (typeName instanceof WildcardTypeName) { WildcardTypeName wildcardTypeName = (WildcardTypeName) typeName; // Wildcard types have exactly 1 upper bound. TypeName upperBound = getOnlyElement(wildcardTypeName.upperBounds); if (!upperBound.equals(TypeName.OBJECT)) { // Wildcards with non-Object upper bounds can't have lower bounds. checkState(wildcardTypeName.lowerBounds.isEmpty()); return String.format("? extends %s", toStableString(upperBound)); } if (!wildcardTypeName.lowerBounds.isEmpty()) { // Wildcard types can have at most 1 lower bound. TypeName lowerBound = getOnlyElement(wildcardTypeName.lowerBounds); return String.format("? super %s", toStableString(lowerBound)); } // If the upper bound is Object and there is no lower bound then just use "?". return "?"; } else if (typeName instanceof TypeVariableName) { return ((TypeVariableName) typeName).name; } else { // For all other types (e.g. primitive types) just use the TypeName's toString() return typeName.toString(); } } public static String getKindName(XType type) { if (isArray(type)) { return "ARRAY"; } else if (isWildcard(type)) { return "WILDCARD"; } else if (isTypeVariable(type)) { return "TYPEVAR"; } else if (isVoid(type)) { return "VOID"; } else if (isNullType(type)) { return "NULL"; } else if (isNoType(type)) { return "NONE"; } else if (isPrimitive(type)) { return LOWER_CAMEL.to(UPPER_UNDERSCORE, type.getTypeName().toString()); } else if (type.isError()) { // TODO(b/249801446): For now, we must call XType.isError() after the other checks because // some types in KSP (e.g. Wildcard) are not disjoint from error types and may return true // until this bug is fixed. // Note: Most of these types are disjoint, so ordering doesn't matter. However, error type is // a subtype of declared type so make sure we check isError() before isDeclared() so that // error types are reported as ERROR rather than DECLARED. return "ERROR"; } else if (isDeclared(type)) { return "DECLARED"; } else { return "UNKNOWN"; } } /** * Iterates through the various types referenced within the given {@code type} and resolves it * if needed. */ public static void resolveIfNeeded(XType type) { if (getProcessingEnv(type).getBackend() == XProcessingEnv.Backend.JAVAC) { // TODO(b/242569252): Due to a bug in javac, a TypeName may incorrectly contain a "$" instead // of "." if the TypeName is requested before the type has been resolved. Thus, we try to // resolve the type by calling Element#getKind() to force the correct TypeName. toJavac(type).accept(TypeResolutionVisitor.INSTANCE, new HashSet<>()); } } private static final class TypeResolutionVisitor extends SimpleTypeVisitor8> { static final TypeResolutionVisitor INSTANCE = new TypeResolutionVisitor(); @Override public Void visitDeclared(DeclaredType t, Set visited) { if (!visited.add(t.asElement())) { return null; } if (MoreElements.asType(t.asElement()).getQualifiedName().toString().contains("$")) { // Force symbol completion/resolution on the type by calling Element#getKind(). t.asElement().getKind(); } t.getTypeArguments().forEach(arg -> arg.accept(this, visited)); return null; } @Override public Void visitError(ErrorType t, Set visited) { visitDeclared(t, visited); return null; } @Override public Void visitArray(ArrayType t, Set visited) { t.getComponentType().accept(this, visited); return null; } @Override public Void visitWildcard(WildcardType t, Set visited) { if (t.getExtendsBound() != null) { t.getExtendsBound().accept(this, visited); } if (t.getSuperBound() != null) { t.getSuperBound().accept(this, visited); } return null; } @Override protected Void defaultAction(TypeMirror e, Set visited) { return null; } } private XTypes() {} }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy