dagger.internal.codegen.validation.BindingElementValidator Maven / Gradle / Ivy
/*
* Copyright (C) 2016 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.XTypeKt.isArray;
import static androidx.room.compiler.processing.XTypeKt.isVoid;
import static com.google.common.base.Verify.verifyNotNull;
import static dagger.internal.codegen.base.Util.reentrantComputeIfAbsent;
import static dagger.internal.codegen.binding.AssistedInjectionAnnotations.isAssistedFactoryType;
import static dagger.internal.codegen.binding.AssistedInjectionAnnotations.isAssistedInjectionType;
import static dagger.internal.codegen.binding.MapKeys.getMapKeys;
import static dagger.internal.codegen.xprocessing.XTypes.isDeclared;
import static dagger.internal.codegen.xprocessing.XTypes.isPrimitive;
import static dagger.internal.codegen.xprocessing.XTypes.isTypeVariable;
import androidx.room.compiler.processing.XAnnotation;
import androidx.room.compiler.processing.XElement;
import androidx.room.compiler.processing.XType;
import androidx.room.compiler.processing.XTypeElement;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.FormatMethod;
import com.squareup.javapoet.ClassName;
import dagger.internal.codegen.base.ContributionType;
import dagger.internal.codegen.base.FrameworkTypes;
import dagger.internal.codegen.base.SetType;
import dagger.internal.codegen.binding.InjectionAnnotations;
import dagger.internal.codegen.javapoet.TypeNames;
import dagger.internal.codegen.model.Key;
import dagger.internal.codegen.model.Scope;
import dagger.internal.codegen.xprocessing.XElements;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.inject.Qualifier;
/** A validator for elements that represent binding declarations. */
public abstract class BindingElementValidator {
private static final ImmutableSet MULTIBINDING_ANNOTATIONS =
ImmutableSet.of(TypeNames.INTO_SET, TypeNames.ELEMENTS_INTO_SET, TypeNames.INTO_MAP);
private final AllowsMultibindings allowsMultibindings;
private final AllowsScoping allowsScoping;
private final Map cache = new HashMap<>();
private final InjectionAnnotations injectionAnnotations;
/** Creates a validator object. */
// TODO(bcorso): Consider reworking BindingElementValidator and all subclasses to use composition
// rather than inheritance. The web of inheritance makes it difficult to track what implementation
// of a method is actually being used.
protected BindingElementValidator(
AllowsMultibindings allowsMultibindings,
AllowsScoping allowsScoping,
InjectionAnnotations injectionAnnotations) {
this.allowsMultibindings = allowsMultibindings;
this.allowsScoping = allowsScoping;
this.injectionAnnotations = injectionAnnotations;
}
/** Returns a {@link ValidationReport} for {@code element}. */
public final ValidationReport validate(E element) {
return reentrantComputeIfAbsent(cache, element, this::validateUncached);
}
private ValidationReport validateUncached(E element) {
return elementValidator(element).validate();
}
/**
* Returns an error message of the form "<{@link #bindingElements()}> rule", where
* rule comes from calling {@link String#format(String, Object...)} on {@code ruleFormat}
* and the other arguments.
*/
@FormatMethod
protected final String bindingElements(String ruleFormat, Object... args) {
return new Formatter().format("%s ", bindingElements()).format(ruleFormat, args).toString();
}
/**
* The kind of elements that this validator validates. Should be plural. Used for error reporting.
*/
protected abstract String bindingElements();
/** The verb describing the {@link ElementValidator#bindingElementType()} in error messages. */
// TODO(ronshapiro,dpb): improve the name of this method and it's documentation.
protected abstract String bindingElementTypeVerb();
/** The error message when a binding element has a bad type. */
protected String badTypeMessage() {
return bindingElements(
"must %s a primitive, an array, a type variable, or a declared type",
bindingElementTypeVerb());
}
/**
* The error message when a the type for a binding element with {@link
* dagger.multibindings.ElementsIntoSet @ElementsIntoSet} or {@code SET_VALUES} is a not set type.
*/
protected String elementsIntoSetNotASetMessage() {
return bindingElements(
"annotated with @ElementsIntoSet must %s a Set", bindingElementTypeVerb());
}
/**
* The error message when a the type for a binding element with {@link
* dagger.multibindings.ElementsIntoSet @ElementsIntoSet} or {@code SET_VALUES} is a raw set.
*/
protected String elementsIntoSetRawSetMessage() {
return bindingElements(
"annotated with @ElementsIntoSet cannot %s a raw Set", bindingElementTypeVerb());
}
/*** Returns an {@link ElementValidator} for validating the given {@code element}. */
protected abstract ElementValidator elementValidator(E element);
/** Validator for a single binding element. */
protected abstract class ElementValidator {
private final E element;
protected final ValidationReport.Builder report;
private final ImmutableSet qualifiers;
protected ElementValidator(E element) {
this.element = element;
this.report = ValidationReport.about(element);
qualifiers = injectionAnnotations.getQualifiers(element);
}
/** Checks the element for validity. */
private ValidationReport validate() {
checkType();
checkQualifiers();
checkMapKeys();
checkMultibindings();
checkScopes();
checkAdditionalProperties();
return report.build();
}
/** Check any additional properties of the element. Does nothing by default. */
protected void checkAdditionalProperties() {}
/**
* The type declared by this binding element. This may differ from a binding's {@link
* Key#type()}, for example in multibindings. An {@link Optional#empty()} return value indicates
* that the contributed type is ambiguous or missing, i.e. a {@code @BindsInstance} method with
* zero or many parameters.
*/
// TODO(dpb): should this be an ImmutableList, with this class checking the size?
protected abstract Optional bindingElementType();
/**
* Adds an error if the {@link #bindingElementType() binding element type} is not appropriate.
*
* Adds an error if the type is not a primitive, array, declared type, or type variable.
*
*
If the binding is not a multibinding contribution, adds an error if the type is a
* framework type.
*
*
If the element has {@link dagger.multibindings.ElementsIntoSet @ElementsIntoSet} or {@code
* SET_VALUES}, adds an error if the type is not a {@code Set} for some {@code T}
*/
protected void checkType() {
switch (ContributionType.fromBindingElement(element)) {
case UNIQUE:
// Validate that a unique binding is not attempting to bind a framework type. This
// validation is only appropriate for unique bindings because multibindings may collect
// framework types. E.g. Set> is perfectly reasonable.
checkFrameworkType();
// Validate that a unique binding is not attempting to bind an unqualified assisted type.
// This validation is only appropriate for unique bindings because multibindings may
// collect assisted types.
checkAssistedType();
// fall through
case SET:
case MAP:
bindingElementType().ifPresent(this::checkKeyType);
break;
case SET_VALUES:
checkSetValuesType();
}
}
/**
* Adds an error if {@code keyType} is not a primitive, declared type, array, or type variable.
*/
protected void checkKeyType(XType keyType) {
if (isVoid(keyType)) {
report.addError(bindingElements("must %s a value (not void)", bindingElementTypeVerb()));
} else if (!(isPrimitive(keyType)
|| isDeclared(keyType)
|| isArray(keyType)
|| isTypeVariable(keyType))) {
report.addError(badTypeMessage());
}
}
/** Adds errors for unqualified assisted types. */
private void checkAssistedType() {
if (qualifiers.isEmpty()
&& bindingElementType().isPresent()
&& isDeclared(bindingElementType().get())) {
XTypeElement keyElement = bindingElementType().get().getTypeElement();
if (isAssistedInjectionType(keyElement)) {
report.addError("Dagger does not support providing @AssistedInject types.", keyElement);
}
if (isAssistedFactoryType(keyElement)) {
report.addError("Dagger does not support providing @AssistedFactory types.", keyElement);
}
}
}
/**
* Adds an error if the type for an element with {@link
* dagger.multibindings.ElementsIntoSet @ElementsIntoSet} or {@code SET_VALUES} is not a a
* {@code Set} for a reasonable {@code T}.
*/
// TODO(gak): should we allow "covariant return" for set values?
protected void checkSetValuesType() {
bindingElementType().ifPresent(this::checkSetValuesType);
}
/** Adds an error if {@code type} is not a {@code Set} for a reasonable {@code T}. */
protected final void checkSetValuesType(XType type) {
if (!SetType.isSet(type)) {
report.addError(elementsIntoSetNotASetMessage());
} else {
SetType setType = SetType.from(type);
if (setType.isRawType()) {
report.addError(elementsIntoSetRawSetMessage());
} else {
checkKeyType(setType.elementType());
}
}
}
/**
* Adds an error if the element has more than one {@linkplain Qualifier qualifier} annotation.
*/
private void checkQualifiers() {
if (qualifiers.size() > 1) {
for (XAnnotation qualifier : qualifiers) {
report.addError(
bindingElements("may not use more than one @Qualifier"),
element,
qualifier);
}
}
}
/**
* Adds an error if an {@link dagger.multibindings.IntoMap @IntoMap} element doesn't have
* exactly one {@link dagger.MapKey @MapKey} annotation, or if an element that is {@link
* dagger.multibindings.IntoMap @IntoMap} has any.
*/
private void checkMapKeys() {
if (!allowsMultibindings.allowsMultibindings()) {
return;
}
ImmutableSet mapKeys = getMapKeys(element);
if (ContributionType.fromBindingElement(element).equals(ContributionType.MAP)) {
switch (mapKeys.size()) {
case 0:
report.addError(bindingElements("of type map must declare a map key"));
break;
case 1:
break;
default:
report.addError(bindingElements("may not have more than one map key"));
break;
}
} else if (!mapKeys.isEmpty()) {
report.addError(bindingElements("of non map type cannot declare a map key"));
}
}
/**
* Adds errors if:
*
*
* - the element doesn't allow {@linkplain MultibindingAnnotations multibinding annotations}
* and has any
*
- the element does allow them but has more than one
*
- the element has a multibinding annotation and its {@link dagger.Provides} or {@link
* dagger.producers.Produces} annotation has a {@code type} parameter.
*
*/
private void checkMultibindings() {
ImmutableSet multibindingAnnotations =
XElements.getAllAnnotations(element, MULTIBINDING_ANNOTATIONS);
switch (allowsMultibindings) {
case NO_MULTIBINDINGS:
for (XAnnotation annotation : multibindingAnnotations) {
report.addError(
bindingElements("cannot have multibinding annotations"),
element,
annotation);
}
break;
case ALLOWS_MULTIBINDINGS:
if (multibindingAnnotations.size() > 1) {
for (XAnnotation annotation : multibindingAnnotations) {
report.addError(
bindingElements("cannot have more than one multibinding annotation"),
element,
annotation);
}
}
break;
}
}
/**
* Adds an error if the element has a scope but doesn't allow scoping, or if it has more than
* one {@linkplain Scope scope} annotation.
*/
private void checkScopes() {
ImmutableSet scopes = injectionAnnotations.getScopes(element);
String error = null;
switch (allowsScoping) {
case ALLOWS_SCOPING:
if (scopes.size() <= 1) {
return;
}
error = bindingElements("cannot use more than one @Scope");
break;
case NO_SCOPING:
error = bindingElements("cannot be scoped");
break;
}
verifyNotNull(error);
for (Scope scope : scopes) {
report.addError(error, element, scope.scopeAnnotation().xprocessing());
}
}
/**
* Adds an error if the {@link #bindingElementType() type} is a {@linkplain FrameworkTypes
* framework type}.
*/
private void checkFrameworkType() {
if (bindingElementType().filter(FrameworkTypes::isFrameworkType).isPresent()) {
report.addError(bindingElements("must not %s framework types", bindingElementTypeVerb()));
}
}
}
/** Whether to check multibinding annotations. */
enum AllowsMultibindings {
/**
* This element disallows multibinding annotations, so don't bother checking for their validity.
* {@link MultibindingAnnotationsProcessingStep} will add errors if the element has any
* multibinding annotations.
*/
NO_MULTIBINDINGS,
/** This element allows multibinding annotations, so validate them. */
ALLOWS_MULTIBINDINGS,
;
private boolean allowsMultibindings() {
return this == ALLOWS_MULTIBINDINGS;
}
}
/** How to check scoping annotations. */
enum AllowsScoping {
/** This element disallows scoping, so check that no scope annotations are present. */
NO_SCOPING,
/** This element allows scoping, so validate that there's at most one scope annotation. */
ALLOWS_SCOPING,
;
}
}