| /* |
| * Copyright (C) 2014 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.validation; |
| |
| import static com.google.common.collect.Iterables.getOnlyElement; |
| import static dagger.internal.codegen.base.Util.reentrantComputeIfAbsent; |
| import static dagger.internal.codegen.binding.AssistedInjectionAnnotations.assistedInjectedConstructors; |
| import static dagger.internal.codegen.binding.InjectionAnnotations.injectedConstructors; |
| import static dagger.internal.codegen.binding.SourceFiles.factoryNameForElement; |
| import static dagger.internal.codegen.binding.SourceFiles.membersInjectorNameForType; |
| import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList; |
| import static dagger.internal.codegen.xprocessing.XElements.closestEnclosingTypeElement; |
| import static dagger.internal.codegen.xprocessing.XElements.getAnyAnnotation; |
| import static dagger.internal.codegen.xprocessing.XMethodElements.hasTypeParameters; |
| import static dagger.internal.codegen.xprocessing.XTypes.isSubtype; |
| |
| import androidx.room.compiler.processing.XAnnotation; |
| import androidx.room.compiler.processing.XConstructorElement; |
| import androidx.room.compiler.processing.XElement; |
| import androidx.room.compiler.processing.XExecutableParameterElement; |
| import androidx.room.compiler.processing.XFieldElement; |
| import androidx.room.compiler.processing.XMethodElement; |
| import androidx.room.compiler.processing.XProcessingEnv; |
| import androidx.room.compiler.processing.XType; |
| import androidx.room.compiler.processing.XTypeElement; |
| import androidx.room.compiler.processing.XVariableElement; |
| import com.google.common.collect.ImmutableSet; |
| import com.squareup.javapoet.ClassName; |
| import com.squareup.javapoet.TypeName; |
| import dagger.internal.codegen.base.ClearableCache; |
| import dagger.internal.codegen.base.DaggerSuperficialValidation; |
| import dagger.internal.codegen.binding.InjectionAnnotations; |
| import dagger.internal.codegen.binding.MethodSignatureFormatter; |
| import dagger.internal.codegen.compileroption.CompilerOptions; |
| import dagger.internal.codegen.javapoet.TypeNames; |
| import dagger.internal.codegen.langmodel.Accessibility; |
| import dagger.internal.codegen.model.Scope; |
| import dagger.internal.codegen.xprocessing.XAnnotations; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Optional; |
| import javax.inject.Inject; |
| import javax.inject.Singleton; |
| import javax.tools.Diagnostic; |
| |
| /** |
| * A {@linkplain ValidationReport validator} for {@link Inject}-annotated elements and the types |
| * that contain them. |
| */ |
| @Singleton |
| public final class InjectValidator implements ClearableCache { |
| |
| private final XProcessingEnv processingEnv; |
| private final DependencyRequestValidator dependencyRequestValidator; |
| private final InjectionAnnotations injectionAnnotations; |
| private final DaggerSuperficialValidation superficialValidation; |
| private final MethodSignatureFormatter methodSignatureFormatter; |
| private final InternalValidator validator; |
| private final InternalValidator validatorWhenGeneratingCode; |
| |
| @Inject |
| InjectValidator( |
| XProcessingEnv processingEnv, |
| DependencyRequestValidator dependencyRequestValidator, |
| CompilerOptions compilerOptions, |
| InjectionAnnotations injectionAnnotations, |
| DaggerSuperficialValidation superficialValidation, |
| MethodSignatureFormatter methodSignatureFormatter) { |
| this.processingEnv = processingEnv; |
| this.dependencyRequestValidator = dependencyRequestValidator; |
| this.injectionAnnotations = injectionAnnotations; |
| this.superficialValidation = superficialValidation; |
| this.methodSignatureFormatter = methodSignatureFormatter; |
| |
| // When validating types that require a generated factory class we need to error on private and |
| // static inject members even if the compiler options are set to not error. |
| this.validatorWhenGeneratingCode = |
| new InternalValidator(Diagnostic.Kind.ERROR, Diagnostic.Kind.ERROR); |
| |
| // When validating types that might not require a generated factory we can take the user flags |
| // for private and static inject members into account, but try to reuse the existing one if the |
| // diagnostic kinds are the same. |
| this.validator = |
| (compilerOptions.privateMemberValidationKind() == Diagnostic.Kind.ERROR |
| && compilerOptions.staticMemberValidationKind() == Diagnostic.Kind.ERROR) |
| ? validatorWhenGeneratingCode |
| : new InternalValidator( |
| compilerOptions.privateMemberValidationKind(), |
| compilerOptions.staticMemberValidationKind()); |
| } |
| |
| @Override |
| public void clearCache() { |
| validator.clearCache(); |
| validatorWhenGeneratingCode.clearCache(); |
| } |
| |
| public ValidationReport validate(XTypeElement typeElement) { |
| return validator.validate(typeElement); |
| } |
| |
| public ValidationReport validateForMembersInjection(XTypeElement typeElement) { |
| return validator.validateForMembersInjection(typeElement); |
| } |
| |
| /** |
| * Validates {@code typeElement} that requires a factory to be generated. |
| * |
| * <p>In this case, the validator will have stricter validation for private and static injection |
| * since the generated factory doesn't support those types. |
| */ |
| public ValidationReport validateWhenGeneratingCode(XTypeElement typeElement) { |
| if (typeElement.getPackageName().startsWith("org.atinject.tck")) { |
| // The Technology Compatibility Kit (TCK) package is a special package for testing the JSR330 |
| // spec, which includes optional features like supporting static/private inject members. Even |
| // though Dagger doesn't support this, we allow it for this one case for the test coverage |
| // purposes. Use the normal validator which takes the user's compiler flags into account. |
| return validator.validate(typeElement); |
| } |
| return validatorWhenGeneratingCode.validate(typeElement); |
| } |
| |
| private final class InternalValidator { |
| private final Diagnostic.Kind privateMemberDiagnosticKind; |
| private final Diagnostic.Kind staticMemberDiagnosticKind; |
| private final Map<XTypeElement, ValidationReport> provisionReports = new HashMap<>(); |
| private final Map<XTypeElement, ValidationReport> membersInjectionReports = new HashMap<>(); |
| |
| InternalValidator( |
| Diagnostic.Kind privateMemberDiagnosticKind, Diagnostic.Kind staticMemberDiagnosticKind) { |
| this.privateMemberDiagnosticKind = privateMemberDiagnosticKind; |
| this.staticMemberDiagnosticKind = staticMemberDiagnosticKind; |
| } |
| |
| void clearCache() { |
| provisionReports.clear(); |
| membersInjectionReports.clear(); |
| } |
| |
| ValidationReport validate(XTypeElement typeElement) { |
| return reentrantComputeIfAbsent(provisionReports, typeElement, this::validateUncached); |
| } |
| |
| private ValidationReport validateUncached(XTypeElement typeElement) { |
| ValidationReport.Builder builder = ValidationReport.about(typeElement); |
| builder.addSubreport(validateForMembersInjectionInternal(typeElement)); |
| |
| ImmutableSet<XConstructorElement> injectConstructors = |
| ImmutableSet.<XConstructorElement>builder() |
| .addAll(injectedConstructors(typeElement)) |
| .addAll(assistedInjectedConstructors(typeElement)) |
| .build(); |
| |
| switch (injectConstructors.size()) { |
| case 0: |
| break; // Nothing to validate. |
| case 1: |
| builder.addSubreport(validateConstructor(getOnlyElement(injectConstructors))); |
| break; |
| default: |
| builder.addError( |
| String.format( |
| "Type %s may only contain one injected constructor. Found: %s", |
| typeElement.getQualifiedName(), |
| injectConstructors.stream() |
| .map(methodSignatureFormatter::format) |
| .collect(toImmutableList())), |
| typeElement); |
| } |
| |
| return builder.build(); |
| } |
| |
| private ValidationReport validateConstructor(XConstructorElement constructorElement) { |
| superficialValidation.validateTypeOf(constructorElement); |
| ValidationReport.Builder builder = |
| ValidationReport.about(constructorElement.getEnclosingElement()); |
| |
| if (InjectionAnnotations.hasInjectAnnotation(constructorElement) |
| && constructorElement.hasAnnotation(TypeNames.ASSISTED_INJECT)) { |
| builder.addError("Constructors cannot be annotated with both @Inject and @AssistedInject"); |
| } |
| |
| ClassName injectAnnotation = |
| getAnyAnnotation( |
| constructorElement, |
| TypeNames.INJECT, |
| TypeNames.INJECT_JAVAX, |
| TypeNames.ASSISTED_INJECT) |
| .map(XAnnotations::getClassName) |
| .get(); |
| |
| if (constructorElement.isPrivate()) { |
| builder.addError( |
| "Dagger does not support injection into private constructors", constructorElement); |
| } |
| |
| // If this type has already been processed in a previous round or compilation unit then there |
| // is no reason to recheck for invalid scope annotations since it's already been checked. |
| // This allows us to skip superficial validation of constructor annotations in subsequent |
| // compilations where the annotation types may no longer be on the classpath. |
| if (!processedInPreviousRoundOrCompilationUnit(constructorElement)) { |
| superficialValidation.validateAnnotationsOf(constructorElement); |
| for (XAnnotation qualifier : injectionAnnotations.getQualifiers(constructorElement)) { |
| builder.addError( |
| String.format( |
| "@Qualifier annotations are not allowed on @%s constructors", |
| injectAnnotation.simpleName()), |
| constructorElement, |
| qualifier); |
| } |
| |
| String scopeErrorMsg = |
| String.format( |
| "@Scope annotations are not allowed on @%s constructors", |
| injectAnnotation.simpleName()); |
| |
| if (injectAnnotation.equals(TypeNames.INJECT) |
| || injectAnnotation.equals(TypeNames.INJECT_JAVAX)) { |
| scopeErrorMsg += "; annotate the class instead"; |
| } |
| |
| for (Scope scope : injectionAnnotations.getScopes(constructorElement)) { |
| builder.addError( |
| scopeErrorMsg, constructorElement, scope.scopeAnnotation().xprocessing()); |
| } |
| } |
| |
| for (XExecutableParameterElement parameter : constructorElement.getParameters()) { |
| superficialValidation.validateTypeOf(parameter); |
| validateDependencyRequest(builder, parameter); |
| } |
| |
| if (throwsCheckedExceptions(constructorElement)) { |
| builder.addItem( |
| String.format( |
| "Dagger does not support checked exceptions on @%s constructors", |
| injectAnnotation.simpleName()), |
| privateMemberDiagnosticKind, |
| constructorElement); |
| } |
| |
| checkInjectIntoPrivateClass(constructorElement, builder); |
| |
| XTypeElement enclosingElement = constructorElement.getEnclosingElement(); |
| if (enclosingElement.isAbstract()) { |
| builder.addError( |
| String.format( |
| "@%s is nonsense on the constructor of an abstract class", |
| injectAnnotation.simpleName()), |
| constructorElement); |
| } |
| |
| if (enclosingElement.isNested() && !enclosingElement.isStatic()) { |
| builder.addError( |
| String.format( |
| "@%s constructors are invalid on inner classes. " |
| + "Did you mean to make the class static?", |
| injectAnnotation.simpleName()), |
| constructorElement); |
| } |
| |
| // Note: superficial validation of the annotations is done as part of getting the scopes. |
| ImmutableSet<Scope> scopes = |
| injectionAnnotations.getScopes(constructorElement.getEnclosingElement()); |
| if (injectAnnotation.equals(TypeNames.ASSISTED_INJECT)) { |
| for (Scope scope : scopes) { |
| builder.addError( |
| "A type with an @AssistedInject-annotated constructor cannot be scoped", |
| enclosingElement, |
| scope.scopeAnnotation().xprocessing()); |
| } |
| } else if (scopes.size() > 1) { |
| for (Scope scope : scopes) { |
| builder.addError( |
| "A single binding may not declare more than one @Scope", |
| enclosingElement, |
| scope.scopeAnnotation().xprocessing()); |
| } |
| } |
| |
| return builder.build(); |
| } |
| |
| private ValidationReport validateField(XFieldElement fieldElement) { |
| superficialValidation.validateTypeOf(fieldElement); |
| ValidationReport.Builder builder = ValidationReport.about(fieldElement); |
| if (fieldElement.isFinal()) { |
| builder.addError("@Inject fields may not be final", fieldElement); |
| } |
| |
| if (fieldElement.isPrivate()) { |
| builder.addItem( |
| "Dagger does not support injection into private fields", |
| privateMemberDiagnosticKind, |
| fieldElement); |
| } |
| |
| if (fieldElement.isStatic()) { |
| builder.addItem( |
| "Dagger does not support injection into static fields", |
| staticMemberDiagnosticKind, |
| fieldElement); |
| } |
| |
| if (fieldElement.isProtected() |
| && fieldElement.getEnclosingElement().isFromKotlin() |
| ) { |
| builder.addError( |
| "Dagger injector does not have access to kotlin protected fields", fieldElement); |
| } |
| |
| validateDependencyRequest(builder, fieldElement); |
| |
| return builder.build(); |
| } |
| |
| private ValidationReport validateMethod(XMethodElement methodElement) { |
| superficialValidation.validateTypeOf(methodElement); |
| ValidationReport.Builder builder = ValidationReport.about(methodElement); |
| if (methodElement.isAbstract()) { |
| builder.addError("Methods with @Inject may not be abstract", methodElement); |
| } |
| |
| if (methodElement.isPrivate()) { |
| builder.addItem( |
| "Dagger does not support injection into private methods", |
| privateMemberDiagnosticKind, |
| methodElement); |
| } |
| |
| if (methodElement.isStatic()) { |
| builder.addItem( |
| "Dagger does not support injection into static methods", |
| staticMemberDiagnosticKind, |
| methodElement); |
| } |
| |
| // No need to resolve type parameters since we're only checking existence. |
| if (hasTypeParameters(methodElement)) { |
| builder.addError("Methods with @Inject may not declare type parameters", methodElement); |
| } |
| |
| // No need to resolve thrown types since we're only checking existence. |
| if (!methodElement.getThrownTypes().isEmpty()) { |
| builder.addError( |
| "Methods with @Inject may not throw checked exceptions. " |
| + "Please wrap your exceptions in a RuntimeException instead.", |
| methodElement); |
| } |
| |
| for (XExecutableParameterElement parameter : methodElement.getParameters()) { |
| superficialValidation.validateTypeOf(parameter); |
| validateDependencyRequest(builder, parameter); |
| } |
| |
| return builder.build(); |
| } |
| |
| private void validateDependencyRequest( |
| ValidationReport.Builder builder, XVariableElement parameter) { |
| dependencyRequestValidator.validateDependencyRequest(builder, parameter, parameter.getType()); |
| dependencyRequestValidator.checkNotProducer(builder, parameter); |
| } |
| |
| public ValidationReport validateForMembersInjection(XTypeElement typeElement) { |
| return !processedInPreviousRoundOrCompilationUnit(typeElement) |
| ? validate(typeElement) // validate everything |
| : validateForMembersInjectionInternal(typeElement); // validate only inject members |
| } |
| |
| private ValidationReport validateForMembersInjectionInternal(XTypeElement typeElement) { |
| return reentrantComputeIfAbsent( |
| membersInjectionReports, typeElement, this::validateForMembersInjectionInternalUncached); |
| } |
| |
| private ValidationReport validateForMembersInjectionInternalUncached(XTypeElement typeElement) { |
| superficialValidation.validateTypeOf(typeElement); |
| // TODO(beder): This element might not be currently compiled, so this error message could be |
| // left in limbo. Find an appropriate way to display the error message in that case. |
| ValidationReport.Builder builder = ValidationReport.about(typeElement); |
| boolean hasInjectedMembers = false; |
| for (XFieldElement field : typeElement.getDeclaredFields()) { |
| if (InjectionAnnotations.hasInjectAnnotation(field)) { |
| hasInjectedMembers = true; |
| ValidationReport report = validateField(field); |
| if (!report.isClean()) { |
| builder.addSubreport(report); |
| } |
| } |
| } |
| for (XMethodElement method : typeElement.getDeclaredMethods()) { |
| if (InjectionAnnotations.hasInjectAnnotation(method)) { |
| hasInjectedMembers = true; |
| ValidationReport report = validateMethod(method); |
| if (!report.isClean()) { |
| builder.addSubreport(report); |
| } |
| } |
| } |
| |
| if (hasInjectedMembers) { |
| checkInjectIntoPrivateClass(typeElement, builder); |
| checkInjectIntoKotlinObject(typeElement, builder); |
| } |
| |
| Optional.ofNullable(typeElement.getSuperType()) |
| .filter(supertype -> !supertype.getTypeName().equals(TypeName.OBJECT)) |
| .ifPresent( |
| supertype -> { |
| superficialValidation.validateSuperTypeOf(typeElement); |
| ValidationReport report = validateForMembersInjection(supertype.getTypeElement()); |
| if (!report.isClean()) { |
| builder.addSubreport(report); |
| } |
| }); |
| |
| return builder.build(); |
| } |
| |
| /** Returns true if the given method element declares a checked exception. */ |
| private boolean throwsCheckedExceptions(XConstructorElement constructorElement) { |
| XType runtimeException = processingEnv.findType(TypeNames.RUNTIME_EXCEPTION); |
| XType error = processingEnv.findType(TypeNames.ERROR); |
| superficialValidation.validateThrownTypesOf(constructorElement); |
| return !constructorElement.getThrownTypes().stream() |
| .allMatch(type -> isSubtype(type, runtimeException) || isSubtype(type, error)); |
| } |
| |
| private void checkInjectIntoPrivateClass(XElement element, ValidationReport.Builder builder) { |
| if (!Accessibility.isElementAccessibleFromOwnPackage(closestEnclosingTypeElement(element))) { |
| builder.addItem( |
| "Dagger does not support injection into private classes", |
| privateMemberDiagnosticKind, |
| element); |
| } |
| } |
| |
| private void checkInjectIntoKotlinObject( |
| XTypeElement element, ValidationReport.Builder builder) { |
| if (element.isKotlinObject() || element.isCompanionObject()) { |
| builder.addError("Dagger does not support injection into Kotlin objects", element); |
| } |
| } |
| |
| private boolean processedInPreviousRoundOrCompilationUnit( |
| XConstructorElement injectConstructor) { |
| return processingEnv.findTypeElement(factoryNameForElement(injectConstructor)) != null; |
| } |
| |
| private boolean processedInPreviousRoundOrCompilationUnit(XTypeElement membersInjectedType) { |
| return processingEnv.findTypeElement(membersInjectorNameForType(membersInjectedType)) != null; |
| } |
| } |
| } |