blob: fc54c96891871b255c8819949e861940ea847ee0 [file] [log] [blame]
/*
* 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.base;
import static androidx.room.compiler.processing.XElementKt.isMethod;
import static androidx.room.compiler.processing.XElementKt.isTypeElement;
import static androidx.room.compiler.processing.XElementKt.isVariableElement;
import static androidx.room.compiler.processing.XTypeKt.isArray;
import static androidx.room.compiler.processing.compat.XConverters.toJavac;
import static androidx.room.compiler.processing.compat.XConverters.toXProcessing;
import static com.google.common.base.Preconditions.checkNotNull;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList;
import static dagger.internal.codegen.xprocessing.XAnnotationValues.getKindName;
import static dagger.internal.codegen.xprocessing.XElements.asEnumEntry;
import static dagger.internal.codegen.xprocessing.XElements.asExecutable;
import static dagger.internal.codegen.xprocessing.XElements.asMethod;
import static dagger.internal.codegen.xprocessing.XElements.asTypeElement;
import static dagger.internal.codegen.xprocessing.XElements.asTypeParameter;
import static dagger.internal.codegen.xprocessing.XElements.asVariable;
import static dagger.internal.codegen.xprocessing.XElements.getKindName;
import static dagger.internal.codegen.xprocessing.XElements.isEnumEntry;
import static dagger.internal.codegen.xprocessing.XElements.isExecutable;
import static dagger.internal.codegen.xprocessing.XElements.isTypeParameter;
import static dagger.internal.codegen.xprocessing.XExecutableTypes.asMethodType;
import static dagger.internal.codegen.xprocessing.XExecutableTypes.getKindName;
import static dagger.internal.codegen.xprocessing.XExecutableTypes.isMethodType;
import static dagger.internal.codegen.xprocessing.XTypes.asArray;
import static dagger.internal.codegen.xprocessing.XTypes.getKindName;
import static dagger.internal.codegen.xprocessing.XTypes.isDeclared;
import static dagger.internal.codegen.xprocessing.XTypes.isTypeOf;
import static dagger.internal.codegen.xprocessing.XTypes.isWildcard;
import androidx.room.compiler.processing.XAnnotation;
import androidx.room.compiler.processing.XAnnotationValue;
import androidx.room.compiler.processing.XElement;
import androidx.room.compiler.processing.XExecutableElement;
import androidx.room.compiler.processing.XExecutableType;
import androidx.room.compiler.processing.XProcessingEnv;
import androidx.room.compiler.processing.XProcessingEnv.Backend;
import androidx.room.compiler.processing.XType;
import androidx.room.compiler.processing.XTypeElement;
import androidx.room.compiler.processing.compat.XConverters;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.devtools.ksp.symbol.ClassKind;
import com.squareup.javapoet.ClassName;
import dagger.Reusable;
import dagger.internal.codegen.compileroption.CompilerOptions;
import dagger.internal.codegen.xprocessing.XAnnotationValues;
import dagger.internal.codegen.xprocessing.XAnnotations;
import dagger.internal.codegen.xprocessing.XElements;
import dagger.internal.codegen.xprocessing.XExecutableTypes;
import dagger.internal.codegen.xprocessing.XTypes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import javax.inject.Inject;
/**
* A fork of {@link com.google.auto.common.SuperficialValidation}.
*
* <p>This fork makes a couple changes from the original:
*
* <ul>
* <li>Throws {@link ValidationException} rather than returning {@code false} for invalid types.
* <li>Fixes a bug that incorrectly validates error types in annotations (b/213880825)
* <li>Exposes extra methods needed to validate various parts of an element rather than just the
* entire element.
* </ul>
*/
@Reusable
public final class DaggerSuperficialValidation {
/**
* Returns the type element with the given class name or throws {@link ValidationException} if it
* is not accessible in the current compilation.
*/
public static XTypeElement requireTypeElement(XProcessingEnv processingEnv, ClassName className) {
return requireTypeElement(processingEnv, className.canonicalName());
}
/**
* Returns the type element with the given class name or throws {@link ValidationException} if it
* is not accessible in the current compilation.
*/
public static XTypeElement requireTypeElement(XProcessingEnv processingEnv, String className) {
XTypeElement type = processingEnv.findTypeElement(className);
if (type == null) {
throw new ValidationException.KnownErrorType(className);
}
return type;
}
private final boolean isStrictValidationEnabled;
private final XProcessingEnv processingEnv;
@Inject
DaggerSuperficialValidation(XProcessingEnv processingEnv, CompilerOptions compilerOptions) {
this.processingEnv = processingEnv;
this.isStrictValidationEnabled = compilerOptions.strictSuperficialValidation();
}
/**
* Validates the {@link XElement#getType()} type of the given element.
*
* <p>Validating the type also validates any types it references, such as any type arguments or
* type bounds. For an {@link XExecutableType}, the parameter and return types must be fully
* defined, as must types declared in a {@code throws} clause or in the bounds of any type
* parameters.
*/
public void validateTypeOf(XElement element) {
try {
// In XProcessing, there is no generic way to get an element "asType" so we break this down
// differently for different element kinds.
if (isTypeElement(element)) {
XTypeElement typeElement = asTypeElement(element);
// TODO(b/247828057): Due to a bug in XProcessing, enum entry types are sometimes
// represented by XTypeElement rather than XEnumEntry in KSP which leads to failures later
// on. Thus, skip validation in these cases until this bug is fixed.
if (!(processingEnv.getBackend() == Backend.KSP
&& XConverters.toKS(typeElement).getClassKind() == ClassKind.ENUM_ENTRY)) {
validateType(Ascii.toLowerCase(getKindName(element)), typeElement.getType());
}
} else if (isVariableElement(element)) {
validateType(
Ascii.toLowerCase(getKindName(element)) + " type", asVariable(element).getType());
} else if (isExecutable(element)) {
validateExecutableType(asExecutable(element).getExecutableType());
} else if (isEnumEntry(element)) {
validateType(
Ascii.toLowerCase(getKindName(element)),
asEnumEntry(element).getEnumTypeElement().getType());
}
} catch (RuntimeException exception) {
throw ValidationException.from(exception).append(element);
}
}
/**
* Validates the {@link XElement#getSuperType()} type of the given element.
*
* <p>Validating the type also validates any types it references, such as any type arguments or
* type bounds.
*/
public void validateSuperTypeOf(XTypeElement element) {
try {
validateType("superclass", element.getSuperType());
} catch (RuntimeException exception) {
throw ValidationException.from(exception).append(element);
}
}
/**
* Validates the {@link XExecutableElement#getThrownTypes()} types of the given element.
*
* <p>Validating the type also validates any types it references, such as any type arguments or
* type bounds.
*/
public void validateThrownTypesOf(XExecutableElement element) {
try {
validateTypes("thrown type", element.getThrownTypes());
} catch (RuntimeException exception) {
throw ValidationException.from(exception).append(element);
}
}
/**
* Validates the annotation types of the given element.
*
* <p>Note: this method does not validate annotation values. This method is useful if you care
* about the annotation's annotations (e.g. to check for {@code Scope} or {@code Qualifier}). In
* such cases, we just need to validate the annotation's type.
*/
public void validateAnnotationTypesOf(XElement element) {
element
.getAllAnnotations()
.forEach(annotation -> validateAnnotationTypeOf(element, annotation));
}
/**
* Validates the type of the given annotation.
*
* <p>The annotation is assumed to be annotating the given element, but this is not checked. The
* element is only in the error message if a {@link ValidatationException} is thrown.
*
* <p>Note: this method does not validate annotation values. This method is useful if you care
* about the annotation's annotations (e.g. to check for {@code Scope} or {@code Qualifier}). In
* such cases, we just need to validate the annotation's type.
*/
// TODO(bcorso): See CL/427767370 for suggestions to make this API clearer.
public void validateAnnotationTypeOf(XElement element, XAnnotation annotation) {
try {
validateType("annotation type", annotation.getType());
} catch (RuntimeException exception) {
throw ValidationException.from(exception).append(annotation).append(element);
}
}
/** Validate the annotations of the given element. */
public void validateAnnotationsOf(XElement element) {
try {
validateAnnotations(element.getAllAnnotations());
} catch (RuntimeException exception) {
throw ValidationException.from(exception).append(element);
}
}
public void validateAnnotationOf(XElement element, XAnnotation annotation) {
try {
validateAnnotation(annotation);
} catch (RuntimeException exception) {
throw ValidationException.from(exception).append(element);
}
}
/**
* Validate the type hierarchy for the given type (with the given type description) within the
* given element.
*
* <p>Validation includes all superclasses, interfaces, and type parameters of those types.
*/
public void validateTypeHierarchyOf(String typeDescription, XElement element, XType type) {
try {
validateTypeHierarchy(typeDescription, type);
} catch (RuntimeException exception) {
throw ValidationException.from(exception).append(element);
}
}
private void validateTypeHierarchy(String desc, XType type) {
validateType(desc, type);
try {
type.getSuperTypes().forEach(supertype -> validateTypeHierarchy("supertype", supertype));
} catch (RuntimeException exception) {
throw ValidationException.from(exception).append(desc, type);
}
}
/**
* Returns true if all of the given elements return true from {@link #validateElement(XElement)}.
*/
private void validateElements(Collection<? extends XElement> elements) {
elements.forEach(this::validateElement);
}
/**
* Returns true if all types referenced by the given element are defined. The exact meaning of
* this depends on the kind of element. For packages, it means that all annotations on the package
* are fully defined. For other element kinds, it means that types referenced by the element,
* anything it contains, and any of its annotations element are all defined.
*/
public void validateElement(XElement element) {
checkNotNull(element);
// Validate the annotations first since these are common to all element kinds. We don't
// need to wrap these in try-catch because the *Of() methods are already wrapped.
validateAnnotationsOf(element);
// Validate enclosed elements based on the given element's kind.
try {
if (isTypeElement(element)) {
XTypeElement typeElement = asTypeElement(element);
validateElements(typeElement.getTypeParameters());
validateTypes("interface", typeElement.getSuperInterfaces());
if (typeElement.getSuperType() != null) {
validateType("superclass", typeElement.getSuperType());
}
validateElements(typeElement.getEnclosedElements());
} else if (isExecutable(element)) {
if (isMethod(element)) {
validateType("return type", asMethod(element).getReturnType());
}
XExecutableElement executableElement = asExecutable(element);
validateTypes("thrown type", executableElement.getThrownTypes());
validateElements(executableElement.getTypeParameters());
validateElements(executableElement.getParameters());
} else if (isTypeParameter(element)) {
validateTypes("bound type", asTypeParameter(element).getBounds());
}
} catch (RuntimeException exception) {
throw ValidationException.from(exception).append(element);
}
// Validate the type last. This allows errors on more specific elements to be caught above.
// E.g. errors on parameters will be attributed to the parameter elements rather than the method
// type, which generally leads to nicer error messages. We don't need to wrap these in try-catch
// because the *Of() methods are already wrapped.
validateTypeOf(element);
}
private void validateTypes(String desc, Collection<? extends XType> types) {
types.forEach(type -> validateType(desc, type));
}
/**
* Returns true if the given type is fully defined. This means that the type itself is defined, as
* are any types it references, such as any type arguments or type bounds.
*/
private void validateType(String desc, XType type) {
checkNotNull(type);
// TODO(b/242569252): Due to a bug in kotlinc, a TypeName may incorrectly contain a "$" instead
// of "." if the TypeName is requested before the type has been resolved. Furthermore,
// XProcessing will cache the incorrect TypeName so that further calls will still contain the
// "$" even after the type has been resolved. Thus, we try to resolve the type as early as
// possible to prevent using/caching the incorrect TypeName.
XTypes.resolveIfNeeded(type);
try {
if (isArray(type)) {
validateType("array component type", asArray(type).getComponentType());
} else if (isDeclared(type)) {
if (isStrictValidationEnabled) {
// There's a bug in TypeVisitor which will visit the visitDeclared() method rather than
// visitError() even when it's an ERROR kind. Thus, we check the kind directly here and
// fail validation if it's an ERROR kind (see b/213880825).
if (isErrorKind(type)) {
throw new ValidationException.KnownErrorType(type);
}
}
type.getTypeArguments().forEach(typeArg -> validateType("type argument", typeArg));
} else if (isWildcard(type)) {
if (type.extendsBound() != null) {
validateType("extends bound type", type.extendsBound());
}
} else if (isErrorKind(type)) {
throw new ValidationException.KnownErrorType(type);
}
} catch (RuntimeException e) {
throw ValidationException.from(e).append(desc, type);
}
}
// TODO(bcorso): Consider moving this over to XProcessing. There's some complication due to
// b/248552462 and the fact that XProcessing also uses the error.NonExistentClass type for invalid
// types in KSP, which we may want to keep as error kinds in KSP.
private boolean isErrorKind(XType type) {
// https://youtrack.jetbrains.com/issue/KT-34193/Kapt-CorrectErrorTypes-doesnt-work-for-generics
// XProcessing treats 'error.NonExistentClass' as an error type. However, due to the bug in KAPT
// (linked above), 'error.NonExistentClass' can still be referenced in the stub classes even
// when 'correctErrorTypes=true' is enabled. Thus, we can't treat 'error.NonExistentClass' as an
// actual error type, as that would completely prevent processing of stubs that exhibit this
// bug. This behavior also matches how things work in Javac, as 'error.NonExistentClass' is
// treated as a TypeKind.DECLARED rather than a TypeKind.ERROR since the type is a real class
// that exists on the classpath.
return type.isError()
&& !(processingEnv.getBackend() == Backend.JAVAC
&& type.getTypeName().toString().contentEquals("error.NonExistentClass"));
}
/**
* Returns true if the given type is fully defined. This means that the parameter and return types
* must be fully defined, as must types declared in a {@code throws} clause or in the bounds of
* any type parameters.
*/
private void validateExecutableType(XExecutableType type) {
try {
validateTypes("parameter type", type.getParameterTypes());
validateTypes("thrown type", type.getThrownTypes());
validateTypes("type variable", getTypeVariables(type));
if (isMethodType(type)) {
validateType("return type", asMethodType(type).getReturnType());
}
} catch (RuntimeException e) {
throw ValidationException.from(e).append(type);
}
}
private ImmutableList<XType> getTypeVariables(XExecutableType executableType) {
switch (processingEnv.getBackend()) {
case JAVAC:
return toJavac(executableType).getTypeVariables().stream()
.map(typeVariable -> toXProcessing(typeVariable, processingEnv))
.collect(toImmutableList());
case KSP:
// TODO(b/247851395): Add a way to get type variables as XTypes from XExecutableType --
// currently, we can only get TypeVariableNames from XMethodType. For now, just skip
// validating type variables of methods in KSP.
return ImmutableList.of();
}
throw new AssertionError("Unexpected backend: " + processingEnv.getBackend());
}
private void validateAnnotations(Collection<XAnnotation> annotations) {
annotations.forEach(this::validateAnnotation);
}
private void validateAnnotation(XAnnotation annotation) {
try {
validateType("annotation type", annotation.getType());
validateAnnotationValues(getDefaultValues(annotation));
validateAnnotationValues(annotation.getAnnotationValues());
} catch (RuntimeException exception) {
throw ValidationException.from(exception).append(annotation);
}
}
private ImmutableList<XAnnotationValue> getDefaultValues(XAnnotation annotation) {
switch (processingEnv.getBackend()) {
case JAVAC:
return annotation.getTypeElement().getDeclaredMethods().stream()
.map(XConverters::toJavac)
.filter(method -> method.getDefaultValue() != null)
.map(method -> toXProcessing(method.getDefaultValue(), method, processingEnv))
.collect(toImmutableList());
case KSP:
// TODO(b/231170716): Add a generic way to retrieve default values from XAnnotation
// For now, just ignore them in KSP when doing validation.
return ImmutableList.of();
}
throw new AssertionError("Unexpected backend: " + processingEnv.getBackend());
}
private void validateAnnotationValues(Collection<XAnnotationValue> values) {
values.forEach(this::validateAnnotationValue);
}
private void validateAnnotationValue(XAnnotationValue value) {
try {
XType expectedType = value.getValueType();
// TODO(b/249834057): In KSP error types in annotation values are just null, so check this
// first and throw KnownErrorType of "<error>" to match Javac for now.
if (processingEnv.getBackend() == Backend.KSP && value.getValue() == null) {
throw new ValidationException.KnownErrorType("<error>");
}
if (value.hasListValue()) {
validateAnnotationValues(value.asAnnotationValueList());
} else if (value.hasAnnotationValue()) {
validateIsEquivalentType(value.asAnnotation().getType(), expectedType);
validateAnnotation(value.asAnnotation());
} else if (value.hasEnumValue()) {
validateIsEquivalentType(value.asEnum().getEnumTypeElement().getType(), expectedType);
validateElement(value.asEnum());
} else if (value.hasTypeValue()) {
validateType("annotation value type", value.asType());
} else {
// Validates all other types, e.g. primitives and String values.
validateIsTypeOf(expectedType, ClassName.get(value.getValue().getClass()));
}
} catch (RuntimeException e) {
throw ValidationException.from(e).append(value);
}
}
private void validateIsTypeOf(XType expectedType, ClassName className) {
if (!isTypeOf(expectedType.boxed(), className)) {
throw new ValidationException.UnknownErrorType();
}
}
private void validateIsEquivalentType(XType type, XType expectedType) {
if (!XTypes.equivalence().equivalent(type, expectedType)) {
throw new ValidationException.KnownErrorType(type);
}
}
/**
* A runtime exception that can be used during superficial validation to collect information about
* unexpected exceptions during validation.
*/
public abstract static class ValidationException extends RuntimeException {
/** A {@link ValidationException} that originated from an unexpected exception. */
public static final class UnexpectedException extends ValidationException {
private UnexpectedException(Throwable throwable) {
super(throwable);
}
}
/** A {@link ValidationException} that originated from a known error type. */
public static final class KnownErrorType extends ValidationException {
private final String errorTypeName;
private KnownErrorType(XType errorType) {
this.errorTypeName = XTypes.toStableString(errorType);
}
private KnownErrorType(String errorTypeName) {
this.errorTypeName = errorTypeName;
}
public String getErrorTypeName() {
return errorTypeName;
}
}
/** A {@link ValidationException} that originated from an unknown error type. */
public static final class UnknownErrorType extends ValidationException {}
private static ValidationException from(Throwable throwable) {
if (throwable instanceof ValidationException) {
// We only ever create one instance of the ValidationException.
return (ValidationException) throwable;
} else if (throwable instanceof TypeNotPresentException) {
// XProcessing can throw TypeNotPresentException, so grab the error type from there if so.
return new KnownErrorType(((TypeNotPresentException) throwable).typeName());
}
return new UnexpectedException(throwable);
}
private Optional<XElement> lastReportedElement = Optional.empty();
private final List<String> messages = new ArrayList<>();
private ValidationException() {
super("");
}
private ValidationException(Throwable throwable) {
super("", throwable);
}
/**
* Appends a message for the given element and returns this instance of {@link
* ValidationException}
*/
private ValidationException append(XElement element) {
lastReportedElement = Optional.of(element);
return append(getMessageForElement(element));
}
/**
* Appends a message for the given type and returns this instance of {@link ValidationException}
*/
private ValidationException append(String desc, XType type) {
return append(
String.format(
"type (%s %s): %s",
getKindName(type),
desc,
XTypes.toStableString(type)));
}
/**
* Appends a message for the given executable type and returns this instance of {@link
* ValidationException}
*/
private ValidationException append(XExecutableType type) {
return append(
String.format(
"type (EXECUTABLE %s): %s",
Ascii.toLowerCase(getKindName(type)),
XExecutableTypes.toStableString(type)));
}
/**
* Appends a message for the given annotation and returns this instance of {@link
* ValidationException}
*/
private ValidationException append(XAnnotation annotation) {
// Note: Calling #toString() directly on the annotation throws NPE (b/216180336).
return append(String.format("annotation: %s", XAnnotations.toStableString(annotation)));
}
/** Appends the given message and returns this instance of {@link ValidationException} */
private ValidationException append(String message) {
messages.add(message);
return this;
}
/**
* Appends a message for the given annotation value and returns this instance of {@link
* ValidationException}
*/
private ValidationException append(XAnnotationValue value) {
return append(
String.format(
"annotation value (%s): %s=%s",
getKindName(value),
value.getName(), // SUPPRESS_GET_NAME_CHECK
XAnnotationValues.toStableString(value)));
}
@Override
public String getMessage() {
return String.format("\n Validation trace:\n => %s", getTrace());
}
public String getTrace() {
return String.join("\n => ", getMessageInternal().reverse());
}
private ImmutableList<String> getMessageInternal() {
if (!lastReportedElement.isPresent()) {
return ImmutableList.copyOf(messages);
}
// Append any enclosing element information if needed.
List<String> newMessages = new ArrayList<>(messages);
XElement element = lastReportedElement.get();
while (shouldAppendEnclosingElement(element)) {
element = element.getEnclosingElement();
newMessages.add(getMessageForElement(element));
}
return ImmutableList.copyOf(newMessages);
}
private static boolean shouldAppendEnclosingElement(XElement element) {
return element.getEnclosingElement() != null
// We don't report enclosing elements for types because the type name should contain any
// enclosing type and package information we need.
&& !isTypeElement(element)
&& (isExecutable(element.getEnclosingElement())
|| isTypeElement(element.getEnclosingElement()));
}
private String getMessageForElement(XElement element) {
return String.format(
"element (%s): %s",
Ascii.toUpperCase(getKindName(element)),
XElements.toStableString(element));
}
}
}