421 lines
16 KiB
Java
421 lines
16 KiB
Java
/*
|
|
* 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.auto.common.MoreElements.asType;
|
|
import static com.google.auto.common.MoreElements.isAnnotationPresent;
|
|
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 javax.lang.model.element.Modifier.ABSTRACT;
|
|
import static javax.lang.model.element.Modifier.FINAL;
|
|
import static javax.lang.model.element.Modifier.PRIVATE;
|
|
import static javax.lang.model.element.Modifier.STATIC;
|
|
import static javax.lang.model.type.TypeKind.DECLARED;
|
|
|
|
import com.google.auto.common.MoreElements;
|
|
import com.google.auto.common.MoreTypes;
|
|
import com.google.common.collect.ImmutableSet;
|
|
import dagger.assisted.AssistedInject;
|
|
import dagger.internal.codegen.base.ClearableCache;
|
|
import dagger.internal.codegen.binding.InjectionAnnotations;
|
|
import dagger.internal.codegen.compileroption.CompilerOptions;
|
|
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.model.Scope;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
import javax.inject.Inject;
|
|
import javax.inject.Singleton;
|
|
import javax.lang.model.element.AnnotationMirror;
|
|
import javax.lang.model.element.Element;
|
|
import javax.lang.model.element.ExecutableElement;
|
|
import javax.lang.model.element.Modifier;
|
|
import javax.lang.model.element.TypeElement;
|
|
import javax.lang.model.element.VariableElement;
|
|
import javax.lang.model.type.TypeKind;
|
|
import javax.lang.model.type.TypeMirror;
|
|
import javax.lang.model.util.ElementFilter;
|
|
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 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<ExecutableElement, ValidationReport<TypeElement>> reports = new HashMap<>();
|
|
|
|
@Inject
|
|
InjectValidator(
|
|
DaggerTypes types,
|
|
DaggerElements elements,
|
|
DependencyRequestValidator dependencyRequestValidator,
|
|
CompilerOptions compilerOptions,
|
|
InjectionAnnotations injectionAnnotations,
|
|
KotlinMetadataUtil metadataUtil) {
|
|
this(
|
|
types,
|
|
elements,
|
|
compilerOptions,
|
|
dependencyRequestValidator,
|
|
Optional.empty(),
|
|
injectionAnnotations,
|
|
metadataUtil);
|
|
}
|
|
|
|
private InjectValidator(
|
|
DaggerTypes types,
|
|
DaggerElements elements,
|
|
CompilerOptions compilerOptions,
|
|
DependencyRequestValidator dependencyRequestValidator,
|
|
Optional<Kind> privateAndStaticInjectionDiagnosticKind,
|
|
InjectionAnnotations injectionAnnotations,
|
|
KotlinMetadataUtil metadataUtil) {
|
|
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() {
|
|
reports.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(
|
|
types,
|
|
elements,
|
|
compilerOptions,
|
|
dependencyRequestValidator,
|
|
Optional.of(Diagnostic.Kind.ERROR),
|
|
injectionAnnotations,
|
|
metadataUtil);
|
|
}
|
|
|
|
public ValidationReport<TypeElement> validateConstructor(ExecutableElement constructorElement) {
|
|
return reentrantComputeIfAbsent(reports, constructorElement, this::validateConstructorUncached);
|
|
}
|
|
|
|
private ValidationReport<TypeElement> validateConstructorUncached(
|
|
ExecutableElement constructorElement) {
|
|
ValidationReport.Builder<TypeElement> builder =
|
|
ValidationReport.about(asType(constructorElement.getEnclosingElement()));
|
|
|
|
if (isAnnotationPresent(constructorElement, Inject.class)
|
|
&& isAnnotationPresent(constructorElement, AssistedInject.class)) {
|
|
builder.addError("Constructors cannot be annotated with both @Inject and @AssistedInject");
|
|
}
|
|
|
|
Class<?> injectAnnotation =
|
|
isAnnotationPresent(constructorElement, Inject.class) ? Inject.class : AssistedInject.class;
|
|
|
|
if (constructorElement.getModifiers().contains(PRIVATE)) {
|
|
builder.addError(
|
|
"Dagger does not support injection into private constructors", constructorElement);
|
|
}
|
|
|
|
for (AnnotationMirror qualifier : injectionAnnotations.getQualifiers(constructorElement)) {
|
|
builder.addError(
|
|
String.format(
|
|
"@Qualifier annotations are not allowed on @%s constructors",
|
|
injectAnnotation.getSimpleName()),
|
|
constructorElement,
|
|
qualifier);
|
|
}
|
|
|
|
String scopeErrorMsg =
|
|
String.format(
|
|
"@Scope annotations are not allowed on @%s constructors",
|
|
injectAnnotation.getSimpleName());
|
|
|
|
if (injectAnnotation == Inject.class) {
|
|
scopeErrorMsg += "; annotate the class instead";
|
|
}
|
|
|
|
for (Scope scope : scopesOf(constructorElement)) {
|
|
builder.addError(scopeErrorMsg, constructorElement, scope.scopeAnnotation());
|
|
}
|
|
|
|
for (VariableElement parameter : constructorElement.getParameters()) {
|
|
validateDependencyRequest(builder, parameter);
|
|
}
|
|
|
|
if (throwsCheckedExceptions(constructorElement)) {
|
|
builder.addItem(
|
|
String.format(
|
|
"Dagger does not support checked exceptions on @%s constructors",
|
|
injectAnnotation.getSimpleName()),
|
|
privateMemberDiagnosticKind(),
|
|
constructorElement);
|
|
}
|
|
|
|
checkInjectIntoPrivateClass(constructorElement, builder);
|
|
|
|
TypeElement enclosingElement =
|
|
MoreElements.asType(constructorElement.getEnclosingElement());
|
|
|
|
Set<Modifier> typeModifiers = enclosingElement.getModifiers();
|
|
if (typeModifiers.contains(ABSTRACT)) {
|
|
builder.addError(
|
|
String.format(
|
|
"@%s is nonsense on the constructor of an abstract class",
|
|
injectAnnotation.getSimpleName()),
|
|
constructorElement);
|
|
}
|
|
|
|
if (enclosingElement.getNestingKind().isNested()
|
|
&& !typeModifiers.contains(STATIC)) {
|
|
builder.addError(
|
|
String.format(
|
|
"@%s constructors are invalid on inner classes. "
|
|
+ "Did you mean to make the class static?",
|
|
injectAnnotation.getSimpleName()),
|
|
constructorElement);
|
|
}
|
|
|
|
// This is computationally expensive, but probably preferable to a giant index
|
|
ImmutableSet<ExecutableElement> injectConstructors =
|
|
ImmutableSet.<ExecutableElement>builder()
|
|
.addAll(injectedConstructors(enclosingElement))
|
|
.addAll(assistedInjectedConstructors(enclosingElement))
|
|
.build();
|
|
|
|
if (injectConstructors.size() > 1) {
|
|
builder.addError("Types may only contain one injected constructor", constructorElement);
|
|
}
|
|
|
|
ImmutableSet<Scope> scopes = scopesOf(enclosingElement);
|
|
if (injectAnnotation == AssistedInject.class) {
|
|
for (Scope scope : scopes) {
|
|
builder.addError(
|
|
"A type with an @AssistedInject-annotated constructor cannot be scoped",
|
|
enclosingElement,
|
|
scope.scopeAnnotation());
|
|
}
|
|
} else if (scopes.size() > 1) {
|
|
for (Scope scope : scopes) {
|
|
builder.addError(
|
|
"A single binding may not declare more than one @Scope",
|
|
enclosingElement,
|
|
scope.scopeAnnotation());
|
|
}
|
|
}
|
|
|
|
return builder.build();
|
|
}
|
|
|
|
private ValidationReport<VariableElement> validateField(VariableElement fieldElement) {
|
|
ValidationReport.Builder<VariableElement> builder = ValidationReport.about(fieldElement);
|
|
Set<Modifier> modifiers = fieldElement.getModifiers();
|
|
if (modifiers.contains(FINAL)) {
|
|
builder.addError("@Inject fields may not be final", fieldElement);
|
|
}
|
|
|
|
if (modifiers.contains(PRIVATE)) {
|
|
builder.addItem(
|
|
"Dagger does not support injection into private fields",
|
|
privateMemberDiagnosticKind(),
|
|
fieldElement);
|
|
}
|
|
|
|
if (modifiers.contains(STATIC)) {
|
|
builder.addItem(
|
|
"Dagger does not support injection into static fields",
|
|
staticMemberDiagnosticKind(),
|
|
fieldElement);
|
|
}
|
|
|
|
validateDependencyRequest(builder, fieldElement);
|
|
|
|
return builder.build();
|
|
}
|
|
|
|
private ValidationReport<ExecutableElement> validateMethod(ExecutableElement methodElement) {
|
|
ValidationReport.Builder<ExecutableElement> builder = ValidationReport.about(methodElement);
|
|
Set<Modifier> modifiers = methodElement.getModifiers();
|
|
if (modifiers.contains(ABSTRACT)) {
|
|
builder.addError("Methods with @Inject may not be abstract", methodElement);
|
|
}
|
|
|
|
if (modifiers.contains(PRIVATE)) {
|
|
builder.addItem(
|
|
"Dagger does not support injection into private methods",
|
|
privateMemberDiagnosticKind(),
|
|
methodElement);
|
|
}
|
|
|
|
if (modifiers.contains(STATIC)) {
|
|
builder.addItem(
|
|
"Dagger does not support injection into static methods",
|
|
staticMemberDiagnosticKind(),
|
|
methodElement);
|
|
}
|
|
|
|
if (!methodElement.getTypeParameters().isEmpty()) {
|
|
builder.addError("Methods with @Inject may not declare type parameters", methodElement);
|
|
}
|
|
|
|
if (!methodElement.getThrownTypes().isEmpty()) {
|
|
builder.addError("Methods with @Inject may not throw checked exceptions. "
|
|
+ "Please wrap your exceptions in a RuntimeException instead.", methodElement);
|
|
}
|
|
|
|
for (VariableElement parameter : methodElement.getParameters()) {
|
|
validateDependencyRequest(builder, parameter);
|
|
}
|
|
|
|
return builder.build();
|
|
}
|
|
|
|
private void validateDependencyRequest(
|
|
ValidationReport.Builder<?> builder, VariableElement parameter) {
|
|
dependencyRequestValidator.validateDependencyRequest(builder, parameter, parameter.asType());
|
|
dependencyRequestValidator.checkNotProducer(builder, parameter);
|
|
}
|
|
|
|
public ValidationReport<TypeElement> validateMembersInjectionType(TypeElement 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<TypeElement> builder = ValidationReport.about(typeElement);
|
|
boolean hasInjectedMembers = false;
|
|
for (VariableElement element : ElementFilter.fieldsIn(typeElement.getEnclosedElements())) {
|
|
if (MoreElements.isAnnotationPresent(element, Inject.class)) {
|
|
hasInjectedMembers = true;
|
|
ValidationReport<VariableElement> report = validateField(element);
|
|
if (!report.isClean()) {
|
|
builder.addSubreport(report);
|
|
}
|
|
}
|
|
}
|
|
for (ExecutableElement element : ElementFilter.methodsIn(typeElement.getEnclosedElements())) {
|
|
if (MoreElements.isAnnotationPresent(element, Inject.class)) {
|
|
hasInjectedMembers = true;
|
|
ValidationReport<ExecutableElement> report = validateMethod(element);
|
|
if (!report.isClean()) {
|
|
builder.addSubreport(report);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasInjectedMembers) {
|
|
checkInjectIntoPrivateClass(typeElement, builder);
|
|
checkInjectIntoKotlinObject(typeElement, builder);
|
|
}
|
|
TypeMirror superclass = typeElement.getSuperclass();
|
|
if (!superclass.getKind().equals(TypeKind.NONE)) {
|
|
ValidationReport<TypeElement> report = validateType(MoreTypes.asTypeElement(superclass));
|
|
if (!report.isClean()) {
|
|
builder.addSubreport(report);
|
|
}
|
|
}
|
|
return builder.build();
|
|
}
|
|
|
|
public ValidationReport<TypeElement> validateType(TypeElement typeElement) {
|
|
ValidationReport.Builder<TypeElement> builder = ValidationReport.about(typeElement);
|
|
ValidationReport<TypeElement> membersInjectionReport =
|
|
validateMembersInjectionType(typeElement);
|
|
if (!membersInjectionReport.isClean()) {
|
|
builder.addSubreport(membersInjectionReport);
|
|
}
|
|
for (ExecutableElement element :
|
|
ElementFilter.constructorsIn(typeElement.getEnclosedElements())) {
|
|
if (isAnnotationPresent(element, Inject.class)
|
|
|| isAnnotationPresent(element, AssistedInject.class)) {
|
|
ValidationReport<TypeElement> report = validateConstructor(element);
|
|
if (!report.isClean()) {
|
|
builder.addSubreport(report);
|
|
}
|
|
}
|
|
}
|
|
return builder.build();
|
|
}
|
|
|
|
public boolean isValidType(TypeMirror type) {
|
|
if (!type.getKind().equals(DECLARED)) {
|
|
return true;
|
|
}
|
|
return validateType(MoreTypes.asTypeElement(type)).isClean();
|
|
}
|
|
|
|
/** Returns true if the given method element declares a checked exception. */
|
|
private boolean throwsCheckedExceptions(ExecutableElement methodElement) {
|
|
TypeMirror runtimeExceptionType = elements.getTypeElement(RuntimeException.class).asType();
|
|
TypeMirror errorType = elements.getTypeElement(Error.class).asType();
|
|
for (TypeMirror thrownType : methodElement.getThrownTypes()) {
|
|
if (!types.isSubtype(thrownType, runtimeExceptionType)
|
|
&& !types.isSubtype(thrownType, errorType)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void checkInjectIntoPrivateClass(
|
|
Element element, ValidationReport.Builder<TypeElement> builder) {
|
|
if (!Accessibility.isElementAccessibleFromOwnPackage(
|
|
DaggerElements.closestEnclosingTypeElement(element))) {
|
|
builder.addItem(
|
|
"Dagger does not support injection into private classes",
|
|
privateMemberDiagnosticKind(),
|
|
element);
|
|
}
|
|
}
|
|
|
|
private void checkInjectIntoKotlinObject(
|
|
TypeElement element, ValidationReport.Builder<TypeElement> builder) {
|
|
if (metadataUtil.isObjectClass(element) || metadataUtil.isCompanionObjectClass(element)) {
|
|
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());
|
|
}
|
|
}
|