/* * 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.XElementKt.isConstructor; import static androidx.room.compiler.processing.XElementKt.isField; import static androidx.room.compiler.processing.XElementKt.isMethod; import static androidx.room.compiler.processing.XElementKt.isMethodParameter; import static androidx.room.compiler.processing.XElementKt.isTypeElement; import static androidx.room.compiler.processing.XElementKt.isVariableElement; 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.toKS; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; import static dagger.internal.codegen.xprocessing.XElements.getSimpleName; import static java.util.stream.Collectors.joining; import androidx.room.compiler.processing.XAnnotated; import androidx.room.compiler.processing.XAnnotation; import androidx.room.compiler.processing.XConstructorElement; import androidx.room.compiler.processing.XElement; import androidx.room.compiler.processing.XEnumEntry; import androidx.room.compiler.processing.XEnumTypeElement; import androidx.room.compiler.processing.XExecutableElement; import androidx.room.compiler.processing.XExecutableParameterElement; import androidx.room.compiler.processing.XFieldElement; import androidx.room.compiler.processing.XHasModifiers; import androidx.room.compiler.processing.XMemberContainer; import androidx.room.compiler.processing.XMethodElement; import androidx.room.compiler.processing.XProcessingEnv; import androidx.room.compiler.processing.XTypeElement; import androidx.room.compiler.processing.XTypeParameterElement; import androidx.room.compiler.processing.XVariableElement; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.devtools.ksp.symbol.KSAnnotated; import com.squareup.javapoet.ClassName; import java.util.Collection; import java.util.Optional; import javax.annotation.Nullable; import javax.lang.model.element.ElementKind; import javax.lang.model.element.Modifier; // TODO(bcorso): Consider moving these methods into XProcessing library. /** A utility class for {@link XElement} helper methods. */ public final class XElements { // TODO(bcorso): Replace usages with getJvmName() once it exists. /** Returns the simple name of the member container. */ public static String getSimpleName(XMemberContainer memberContainer) { return memberContainer.getClassName().simpleName(); } /** Returns the simple name of the element. */ public static String getSimpleName(XElement element) { if (isTypeElement(element)) { return asTypeElement(element) .getName(); // SUPPRESS_GET_NAME_CHECK: This uses java simple name implementation under // the hood. } else if (isVariableElement(element)) { return asVariable(element).getName(); // SUPPRESS_GET_NAME_CHECK } else if (isEnumEntry(element)) { return asEnumEntry(element).getName(); // SUPPRESS_GET_NAME_CHECK } else if (isMethod(element)) { // Note: We use "jvm name" here rather than "simple name" because simple name is not reliable // in KAPT. In particular, XProcessing relies on matching the method to its descriptor found // in the Kotlin @Metadata to get the simple name. However, this doesn't work for method // descriptors that contain generated types because the stub and @Metadata will disagree due // to the following bug: // https://youtrack.jetbrains.com/issue/KT-35124/KAPT-not-correcting-error-types-in-Kotlin-Metadata-information-produced-for-the-stubs. // In any case, always using the jvm name should be safe; however, it will unfortunately // contain any obfuscation added by kotlinc, e.g. for "internal" methods, which can make the // "simple name" not as nice/short when used for things like error messages or class names. return asMethod(element).getJvmName(); } else if (isConstructor(element)) { return ""; } else if (isTypeParameter(element)) { return asTypeParameter(element).getName(); // SUPPRESS_GET_NAME_CHECK } throw new AssertionError("No simple name for: " + element); } private static boolean isSyntheticElement(XElement element) { if (isMethodParameter(element)) { XExecutableParameterElement executableParam = asMethodParameter(element); return executableParam.isContinuationParam() || executableParam.isReceiverParam() || executableParam.isKotlinPropertyParam(); } if (isMethod(element)) { return asMethod(element).isKotlinPropertyMethod(); } return false; } @Nullable public static KSAnnotated toKSAnnotated(XElement element) { if (isSyntheticElement(element)) { return toKS(element); } if (isExecutable(element)) { return toKS(asExecutable(element)); } if (isTypeElement(element)) { return toKS(asTypeElement(element)); } if (isField(element)) { return toKS(asField(element)); } if (isMethodParameter(element)) { return toKS(asMethodParameter(element)); } throw new IllegalStateException( "Returning KSAnnotated declaration for " + element + " is not supported."); } /** * Returns the closest enclosing element that is a {@link XTypeElement} or throws an {@link * IllegalStateException} if one doesn't exist. */ public static XTypeElement closestEnclosingTypeElement(XElement element) { return optionalClosestEnclosingTypeElement(element) .orElseThrow(() -> new IllegalStateException("No enclosing TypeElement for: " + element)); } /** * Returns {@code true} if {@code encloser} is equal to or transitively encloses {@code enclosed}. */ public static boolean transitivelyEncloses(XElement encloser, XElement enclosed) { XElement current = enclosed; while (current != null) { if (current.equals(encloser)) { return true; } current = current.getEnclosingElement(); } return false; } private static Optional optionalClosestEnclosingTypeElement(XElement element) { if (isTypeElement(element)) { return Optional.of(asTypeElement(element)); } else if (isConstructor(element)) { return Optional.of(asConstructor(element).getEnclosingElement()); } else if (isMethod(element)) { return optionalClosestEnclosingTypeElement(asMethod(element).getEnclosingElement()); } else if (isField(element)) { return optionalClosestEnclosingTypeElement(asField(element).getEnclosingElement()); } else if (isMethodParameter(element)) { return optionalClosestEnclosingTypeElement(asMethodParameter(element).getEnclosingElement()); } return Optional.empty(); } public static boolean isAbstract(XElement element) { return asHasModifiers(element).isAbstract(); } public static boolean isPublic(XElement element) { return asHasModifiers(element).isPublic(); } public static boolean isPrivate(XElement element) { return asHasModifiers(element).isPrivate(); } public static boolean isStatic(XElement element) { return asHasModifiers(element).isStatic(); } // TODO(bcorso): Ideally we would modify XElement to extend XHasModifiers to prevent possible // runtime exceptions if the element does not extend XHasModifiers. However, for Dagger's purpose // all usages should be on elements that do extend XHasModifiers, so generalizing this for // XProcessing is probably overkill for now. private static XHasModifiers asHasModifiers(XElement element) { // In javac, Element implements HasModifiers but in XProcessing XElement does not. // Currently, the elements that do not extend XHasModifiers are XMemberContainer, XEnumEntry, // XVariableElement. Though most instances of XMemberContainer will extend XHasModifiers through // XTypeElement instead. checkArgument(element instanceof XHasModifiers, "Element %s does not have modifiers", element); return (XHasModifiers) element; } // Note: This method always returns `false` but I'd rather not remove it from our codebase since // if XProcessing adds package elements to their model I'd like to catch it here and fail early. public static boolean isPackage(XElement element) { // Currently, XProcessing doesn't represent package elements so this method always returns // false, but we check the state in Javac just to be sure. There's nothing to check in KSP since // there is no concept of package elements in KSP. if (getProcessingEnv(element).getBackend() == XProcessingEnv.Backend.JAVAC) { checkState(toJavac(element).getKind() != ElementKind.PACKAGE); } return false; } public static boolean isTypeParameter(XElement element) { return element instanceof XTypeParameterElement; } public static XTypeParameterElement asTypeParameter(XElement element) { return (XTypeParameterElement) element; } public static boolean isEnumEntry(XElement element) { return element instanceof XEnumEntry; } public static boolean isEnum(XElement element) { return element instanceof XEnumTypeElement; } public static boolean isExecutable(XElement element) { return isConstructor(element) || isMethod(element); } public static XExecutableElement asExecutable(XElement element) { checkState(isExecutable(element)); return (XExecutableElement) element; } public static XTypeElement asTypeElement(XElement element) { checkState(isTypeElement(element)); return (XTypeElement) element; } // TODO(bcorso): Rename this and the XElementKt.isMethodParameter to isExecutableParameter. public static XExecutableParameterElement asMethodParameter(XElement element) { checkState(isMethodParameter(element)); return (XExecutableParameterElement) element; } public static XFieldElement asField(XElement element) { checkState(isField(element)); return (XFieldElement) element; } public static XEnumEntry asEnumEntry(XElement element) { return (XEnumEntry) element; } public static XVariableElement asVariable(XElement element) { checkState(isVariableElement(element)); return (XVariableElement) element; } public static XConstructorElement asConstructor(XElement element) { checkState(isConstructor(element)); return (XConstructorElement) element; } public static XMethodElement asMethod(XElement element) { checkState(isMethod(element)); return (XMethodElement) element; } public static ImmutableSet getAnnotatedAnnotations( XAnnotated annotated, ClassName annotationName) { return annotated.getAllAnnotations().stream() .filter(annotation -> annotation.getType().getTypeElement().hasAnnotation(annotationName)) .collect(toImmutableSet()); } /** Returns {@code true} if {@code annotated} is annotated with any of the given annotations. */ public static boolean hasAnyAnnotation(XAnnotated annotated, ClassName... annotations) { return hasAnyAnnotation(annotated, ImmutableSet.copyOf(annotations)); } /** Returns {@code true} if {@code annotated} is annotated with any of the given annotations. */ public static boolean hasAnyAnnotation(XAnnotated annotated, Collection annotations) { return annotations.stream().anyMatch(annotated::hasAnnotation); } /** * Returns any annotation from {@code annotations} that annotates {@code annotated} or else {@code * Optional.empty()}. */ public static Optional getAnyAnnotation( XAnnotated annotated, ClassName... annotations) { return getAnyAnnotation(annotated, ImmutableSet.copyOf(annotations)); } /** * Returns any annotation from {@code annotations} that annotates {@code annotated} or else {@code * Optional.empty()}. */ public static Optional getAnyAnnotation( XAnnotated annotated, Collection annotations) { return annotations.stream() .filter(annotated::hasAnnotation) .map(annotated::getAnnotation) .findFirst(); } /** Returns all annotations from {@code annotations} that annotate {@code annotated}. */ public static ImmutableSet getAllAnnotations( XAnnotated annotated, Collection annotations) { return annotations.stream() .filter(annotated::hasAnnotation) .map(annotated::getAnnotation) .collect(toImmutableSet()); } /** * Returns a string representation of {@link XElement} that is independent of the backend * (javac/ksp). */ public static String toStableString(XElement element) { if (element == null) { return ""; } try { if (isTypeElement(element)) { return asTypeElement(element).getQualifiedName(); } else if (isExecutable(element)) { XExecutableElement executable = asExecutable(element); return String.format( "%s(%s)", getSimpleName( isConstructor(element) ? asConstructor(element).getEnclosingElement() : executable), executable.getParameters().stream() .map(XExecutableParameterElement::getType) .map(XTypes::toStableString) .collect(joining(","))); } else if (isEnumEntry(element) || isField(element) || isMethodParameter(element) || isTypeParameter(element)) { return getSimpleName(element); } return element.toString(); } catch (TypeNotPresentException e) { return e.typeName(); } } // XElement#kindName() exists, but doesn't give consistent results between JAVAC and KSP (e.g. // METHOD vs FUNCTION) so this custom implementation is meant to provide that consistency. public static String getKindName(XElement element) { if (isTypeElement(element)) { XTypeElement typeElement = asTypeElement(element); if (typeElement.isClass()) { return "CLASS"; } else if (typeElement.isInterface()) { return "INTERFACE"; } else if (typeElement.isAnnotationClass()) { return "ANNOTATION_TYPE"; } } else if (isEnum(element)) { return "ENUM"; } else if (isEnumEntry(element)) { return "ENUM_CONSTANT"; } else if (isConstructor(element)) { return "CONSTRUCTOR"; } else if (isMethod(element)) { return "METHOD"; } else if (isField(element)) { return "FIELD"; } else if (isMethodParameter(element)) { return "PARAMETER"; } else if (isTypeParameter(element)) { return "TYPE_PARAMETER"; } return element.kindName(); } public static String packageName(XElement element) { return element.getClosestMemberContainer().asClassName().getPackageName(); } public static boolean isFinal(XExecutableElement element) { if (element.isFinal()) { return true; } if (getProcessingEnv(element).getBackend() == XProcessingEnv.Backend.KSP) { if (toKS(element).getModifiers().contains(com.google.devtools.ksp.symbol.Modifier.FINAL)) { return true; } } return false; } public static ImmutableList getModifiers(XExecutableElement element) { ImmutableList.Builder builder = ImmutableList.builder(); if (isFinal(element)) { builder.add(Modifier.FINAL); } else if (element.isAbstract()) { builder.add(Modifier.ABSTRACT); } if (element.isStatic()) { builder.add(Modifier.STATIC); } if (element.isPublic()) { builder.add(Modifier.PUBLIC); } else if (element.isPrivate()) { builder.add(Modifier.PRIVATE); } else if (element.isProtected()) { builder.add(Modifier.PROTECTED); } return builder.build(); } private XElements() {} }