blob: 4557745ee8db8689644305a92ae23d10b83721e8 [file] [log] [blame]
/*
* 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 com.google.auto.common.MoreTypes.asTypeElement;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Verify.verifyNotNull;
import static dagger.internal.codegen.base.Scopes.scopesOf;
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.langmodel.DaggerElements.getAnnotationMirror;
import static javax.lang.model.type.TypeKind.ARRAY;
import static javax.lang.model.type.TypeKind.DECLARED;
import static javax.lang.model.type.TypeKind.TYPEVAR;
import static javax.lang.model.type.TypeKind.VOID;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.FormatMethod;
import dagger.MapKey;
import dagger.Provides;
import dagger.internal.codegen.base.ContributionType;
import dagger.internal.codegen.base.FrameworkTypes;
import dagger.internal.codegen.base.MultibindingAnnotations;
import dagger.internal.codegen.base.SetType;
import dagger.internal.codegen.binding.InjectionAnnotations;
import dagger.model.Key;
import dagger.model.Scope;
import dagger.multibindings.ElementsIntoSet;
import dagger.multibindings.IntoMap;
import dagger.producers.Produces;
import java.lang.annotation.Annotation;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.inject.Qualifier;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
/** A validator for elements that represent binding declarations. */
public abstract class BindingElementValidator<E extends Element> {
private final Class<? extends Annotation> bindingAnnotation;
private final AllowsMultibindings allowsMultibindings;
private final AllowsScoping allowsScoping;
private final Map<E, ValidationReport<E>> cache = new HashMap<>();
private final InjectionAnnotations injectionAnnotations;
/**
* Creates a validator object.
*
* @param bindingAnnotation the annotation on an element that identifies it as a binding element
*/
protected BindingElementValidator(
Class<? extends Annotation> bindingAnnotation,
AllowsMultibindings allowsMultibindings,
AllowsScoping allowsScoping,
InjectionAnnotations injectionAnnotations) {
this.bindingAnnotation = bindingAnnotation;
this.allowsMultibindings = allowsMultibindings;
this.allowsScoping = allowsScoping;
this.injectionAnnotations = injectionAnnotations;
}
/** Returns a {@link ValidationReport} for {@code element}. */
final ValidationReport<E> validate(E element) {
return reentrantComputeIfAbsent(cache, element, this::validateUncached);
}
private ValidationReport<E> validateUncached(E element) {
return elementValidator(element).validate();
}
/**
* Returns an error message of the form "&lt;{@link #bindingElements()}&gt; <i>rule</i>", where
* <i>rule</i> 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
* 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
* 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 {
protected final E element;
protected final ValidationReport.Builder<E> report;
protected ElementValidator(E element) {
this.element = element;
this.report = ValidationReport.about(element);
}
/** Checks the element for validity. */
private ValidationReport<E> 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<TypeMirror>, with this class checking the size?
protected abstract Optional<TypeMirror> bindingElementType();
/**
* Adds an error if the {@link #bindingElementType() binding element type} is not appropriate.
*
* <p>Adds an error if the type is not a primitive, array, declared type, or type variable.
*
* <p>If the binding is not a multibinding contribution, adds an error if the type is a
* framework type.
*
* <p>If the element has {@link ElementsIntoSet @ElementsIntoSet} or {@code SET_VALUES}, adds an
* error if the type is not a {@code Set<T>} 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<Provider<Foo>> is perfectly reasonable. */
checkFrameworkType();
// fall through
case SET:
case MAP:
bindingElementType().ifPresent(type -> checkKeyType(type));
break;
case SET_VALUES:
checkSetValuesType();
}
}
/**
* Adds an error if {@code keyType} is not a primitive, declared type, array, or type variable.
*/
protected void checkKeyType(TypeMirror keyType) {
TypeKind kind = keyType.getKind();
if (kind.equals(VOID)) {
report.addError(bindingElements("must %s a value (not void)", bindingElementTypeVerb()));
} else if (kind == DECLARED) {
checkNotAssistedInject(keyType);
} else if (!(kind.isPrimitive() || kind.equals(ARRAY) || kind.equals(TYPEVAR))) {
report.addError(badTypeMessage());
}
}
/** Adds errors for a method return type. */
private void checkNotAssistedInject(TypeMirror keyType) {
checkState(keyType.getKind() == TypeKind.DECLARED);
TypeElement keyElement = asTypeElement(keyType);
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 ElementsIntoSet @ElementsIntoSet} or
* {@code SET_VALUES} is not a a {@code Set<T>} for a reasonable {@code T}.
*/
// TODO(gak): should we allow "covariant return" for set values?
protected void checkSetValuesType() {
bindingElementType().ifPresent(keyType -> checkSetValuesType(keyType));
}
/** Adds an error if {@code type} is not a {@code Set<T>} for a reasonable {@code T}. */
protected final void checkSetValuesType(TypeMirror 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() {
ImmutableCollection<? extends AnnotationMirror> qualifiers =
injectionAnnotations.getQualifiers(element);
if (qualifiers.size() > 1) {
for (AnnotationMirror qualifier : qualifiers) {
report.addError(
bindingElements("may not use more than one @Qualifier"),
element,
qualifier);
}
}
}
/**
* Adds an error if an {@link IntoMap @IntoMap} element doesn't have exactly one {@link
* MapKey @MapKey} annotation, or if an element that is {@link IntoMap @IntoMap} has any.
*/
private void checkMapKeys() {
if (!allowsMultibindings.allowsMultibindings()) {
return;
}
ImmutableSet<? extends AnnotationMirror> 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:
*
* <ul>
* <li>the element doesn't allow {@linkplain MultibindingAnnotations multibinding annotations}
* and has any
* <li>the element does allow them but has more than one
* <li>the element has a multibinding annotation and its {@link Provides} or {@link Produces}
* annotation has a {@code type} parameter.
* </ul>
*/
private void checkMultibindings() {
ImmutableSet<AnnotationMirror> multibindingAnnotations =
MultibindingAnnotations.forElement(element);
switch (allowsMultibindings) {
case NO_MULTIBINDINGS:
for (AnnotationMirror annotation : multibindingAnnotations) {
report.addError(
bindingElements("cannot have multibinding annotations"),
element,
annotation);
}
break;
case ALLOWS_MULTIBINDINGS:
if (multibindingAnnotations.size() > 1) {
for (AnnotationMirror annotation : multibindingAnnotations) {
report.addError(
bindingElements("cannot have more than one multibinding annotation"),
element,
annotation);
}
}
break;
}
// TODO(ronshapiro): move this into ProvidesMethodValidator
if (bindingAnnotation.equals(Provides.class)) {
AnnotationMirror bindingAnnotationMirror =
getAnnotationMirror(element, bindingAnnotation).get();
boolean usesProvidesType = false;
for (ExecutableElement member : bindingAnnotationMirror.getElementValues().keySet()) {
usesProvidesType |= member.getSimpleName().contentEquals("type");
}
if (usesProvidesType && !multibindingAnnotations.isEmpty()) {
report.addError(
"@Provides.type cannot be used with multibinding annotations", element);
}
}
}
/**
* 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<Scope> scopes = scopesOf(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());
}
}
/**
* 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,
;
}
}