blob: f347ad3000e4b28c4c8dbf39ee1e687c241b4d71 [file] [log] [blame]
/*
* 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 androidx.room.compiler.processing.compat.XConverters.toJavac;
import static androidx.room.compiler.processing.compat.XConverters.toXProcessing;
import static com.google.common.collect.Iterables.getOnlyElement;
import static dagger.internal.codegen.base.Scopes.scopesOf;
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.xprocessing.XElements.getAnyAnnotation;
import static dagger.internal.codegen.xprocessing.XMethodElements.hasTypeParameters;
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 dagger.internal.codegen.base.ClearableCache;
import dagger.internal.codegen.binding.InjectionAnnotations;
import dagger.internal.codegen.compileroption.CompilerOptions;
import dagger.internal.codegen.javapoet.TypeNames;
import dagger.internal.codegen.kotlin.KotlinMetadataUtil;
import dagger.internal.codegen.langmodel.Accessibility;
import dagger.internal.codegen.langmodel.DaggerElements;
import dagger.internal.codegen.langmodel.DaggerTypes;
import dagger.internal.codegen.xprocessing.XAnnotations;
import dagger.spi.model.Scope;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.tools.Diagnostic;
import javax.tools.Diagnostic.Kind;
/**
* 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 DaggerTypes types;
private final DaggerElements elements;
private final CompilerOptions compilerOptions;
private final DependencyRequestValidator dependencyRequestValidator;
private final Optional<Diagnostic.Kind> privateAndStaticInjectionDiagnosticKind;
private final InjectionAnnotations injectionAnnotations;
private final KotlinMetadataUtil metadataUtil;
private final Map<XTypeElement, ValidationReport> provisionReports = new HashMap<>();
private final Map<XTypeElement, ValidationReport> membersInjectionReports = new HashMap<>();
@Inject
InjectValidator(
XProcessingEnv processingEnv,
DaggerTypes types,
DaggerElements elements,
DependencyRequestValidator dependencyRequestValidator,
CompilerOptions compilerOptions,
InjectionAnnotations injectionAnnotations,
KotlinMetadataUtil metadataUtil) {
this(
processingEnv,
types,
elements,
compilerOptions,
dependencyRequestValidator,
Optional.empty(),
injectionAnnotations,
metadataUtil);
}
private InjectValidator(
XProcessingEnv processingEnv,
DaggerTypes types,
DaggerElements elements,
CompilerOptions compilerOptions,
DependencyRequestValidator dependencyRequestValidator,
Optional<Kind> privateAndStaticInjectionDiagnosticKind,
InjectionAnnotations injectionAnnotations,
KotlinMetadataUtil metadataUtil) {
this.processingEnv = processingEnv;
this.types = types;
this.elements = elements;
this.compilerOptions = compilerOptions;
this.dependencyRequestValidator = dependencyRequestValidator;
this.privateAndStaticInjectionDiagnosticKind = privateAndStaticInjectionDiagnosticKind;
this.injectionAnnotations = injectionAnnotations;
this.metadataUtil = metadataUtil;
}
@Override
public void clearCache() {
provisionReports.clear();
membersInjectionReports.clear();
}
/**
* Returns a new validator that performs the same validation as this one, but is strict about
* rejecting optionally-specified JSR 330 behavior that Dagger doesn't support (unless {@code
* -Adagger.ignorePrivateAndStaticInjectionForComponent=enabled} was set in the javac options).
*/
public InjectValidator whenGeneratingCode() {
return compilerOptions.ignorePrivateAndStaticInjectionForComponent()
? this
: new InjectValidator(
processingEnv,
types,
elements,
compilerOptions,
dependencyRequestValidator,
Optional.of(Diagnostic.Kind.ERROR),
injectionAnnotations,
metadataUtil);
}
public ValidationReport validate(XTypeElement typeElement) {
return reentrantComputeIfAbsent(provisionReports, typeElement, this::validateUncached);
}
private ValidationReport validateUncached(XTypeElement typeElement) {
ValidationReport.Builder builder = ValidationReport.about(typeElement);
builder.addSubreport(validateForMembersInjection(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("Types may only contain one injected constructor", typeElement);
}
return builder.build();
}
private ValidationReport validateConstructor(XConstructorElement constructorElement) {
DaggerSuperficialValidation.validateElement(constructorElement);
ValidationReport.Builder builder =
ValidationReport.about(constructorElement.getEnclosingElement());
if (constructorElement.hasAnnotation(TypeNames.INJECT)
&& constructorElement.hasAnnotation(TypeNames.ASSISTED_INJECT)) {
builder.addError("Constructors cannot be annotated with both @Inject and @AssistedInject");
}
ClassName injectAnnotation =
getAnyAnnotation(constructorElement, TypeNames.INJECT, TypeNames.ASSISTED_INJECT)
.map(XAnnotations::getClassName)
.get();
if (constructorElement.isPrivate()) {
builder.addError(
"Dagger does not support injection into private constructors", 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)) {
scopeErrorMsg += "; annotate the class instead";
}
for (Scope scope : scopesOf(constructorElement)) {
builder.addError(
scopeErrorMsg,
constructorElement,
toXProcessing(scope.scopeAnnotation().java(), processingEnv));
}
for (XExecutableParameterElement parameter : constructorElement.getParameters()) {
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);
}
DaggerSuperficialValidation.validateAnnotationsOf(enclosingElement);
ImmutableSet<Scope> scopes = scopesOf(enclosingElement);
if (injectAnnotation.equals(TypeNames.ASSISTED_INJECT)) {
for (Scope scope : scopes) {
builder.addError(
"A type with an @AssistedInject-annotated constructor cannot be scoped",
enclosingElement,
toXProcessing(scope.scopeAnnotation().java(), processingEnv));
}
} else if (scopes.size() > 1) {
for (Scope scope : scopes) {
builder.addError(
"A single binding may not declare more than one @Scope",
enclosingElement,
toXProcessing(scope.scopeAnnotation().java(), processingEnv));
}
}
return builder.build();
}
private ValidationReport validateField(XFieldElement fieldElement) {
DaggerSuperficialValidation.validateElement(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);
}
validateDependencyRequest(builder, fieldElement);
return builder.build();
}
private ValidationReport validateMethod(XMethodElement methodElement) {
DaggerSuperficialValidation.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()) {
DaggerSuperficialValidation.validateElement(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) {
// TODO(bcorso): validate the entire type if a factory hasn't been generated yet so that we
// we can report any errors on the constructor etc. that may have been missed.
return reentrantComputeIfAbsent(
membersInjectionReports, typeElement, this::validateForMembersInjectionUncached);
}
private ValidationReport validateForMembersInjectionUncached(XTypeElement typeElement) {
DaggerSuperficialValidation.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 (field.hasAnnotation(TypeNames.INJECT)) {
hasInjectedMembers = true;
ValidationReport report = validateField(field);
if (!report.isClean()) {
builder.addSubreport(report);
}
}
}
for (XMethodElement method : typeElement.getDeclaredMethods()) {
if (method.hasAnnotation(TypeNames.INJECT)) {
hasInjectedMembers = true;
ValidationReport report = validateMethod(method);
if (!report.isClean()) {
builder.addSubreport(report);
}
}
}
if (hasInjectedMembers) {
checkInjectIntoPrivateClass(typeElement, builder);
checkInjectIntoKotlinObject(typeElement, builder);
}
if (typeElement.getSuperType() != null) {
DaggerSuperficialValidation.validateSuperTypeOf(typeElement);
ValidationReport report =
validateForMembersInjection(typeElement.getSuperType().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);
DaggerSuperficialValidation.validateThrownTypesOf(constructorElement);
return !constructorElement.getThrownTypes().stream()
.allMatch(type -> types.isSubtype(type, runtimeException) || types.isSubtype(type, error));
}
private void checkInjectIntoPrivateClass(XElement element, ValidationReport.Builder builder) {
if (!Accessibility.isElementAccessibleFromOwnPackage(
DaggerElements.closestEnclosingTypeElement(toJavac(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 Diagnostic.Kind privateMemberDiagnosticKind() {
return privateAndStaticInjectionDiagnosticKind.orElse(
compilerOptions.privateMemberValidationKind());
}
private Diagnostic.Kind staticMemberDiagnosticKind() {
return privateAndStaticInjectionDiagnosticKind.orElse(
compilerOptions.staticMemberValidationKind());
}
}