dagger.internal.codegen.validation.ComponentCreatorValidator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dagger-compiler Show documentation
Show all versions of dagger-compiler Show documentation
A fast dependency injector for Android and Java.
/*
* Copyright (C) 2015 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.isVoid;
import static com.google.common.collect.Iterables.getOnlyElement;
import static dagger.internal.codegen.base.ComponentCreatorAnnotation.getCreatorAnnotations;
import static dagger.internal.codegen.base.Util.reentrantComputeIfAbsent;
import static dagger.internal.codegen.xprocessing.XMethodElements.hasTypeParameters;
import static dagger.internal.codegen.xprocessing.XTypeElements.getAllUnimplementedMethods;
import static dagger.internal.codegen.xprocessing.XTypeElements.hasTypeParameters;
import static dagger.internal.codegen.xprocessing.XTypes.isPrimitive;
import static dagger.internal.codegen.xprocessing.XTypes.isSubtype;
import static javax.lang.model.SourceVersion.isKeyword;
import androidx.room.compiler.processing.XConstructorElement;
import androidx.room.compiler.processing.XExecutableParameterElement;
import androidx.room.compiler.processing.XMethodElement;
import androidx.room.compiler.processing.XType;
import androidx.room.compiler.processing.XTypeElement;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ObjectArrays;
import dagger.internal.codegen.base.ClearableCache;
import dagger.internal.codegen.base.ComponentCreatorAnnotation;
import dagger.internal.codegen.binding.ErrorMessages;
import dagger.internal.codegen.binding.ErrorMessages.ComponentCreatorMessages;
import dagger.internal.codegen.binding.MethodSignatureFormatter;
import dagger.internal.codegen.javapoet.TypeNames;
import dagger.internal.codegen.kotlin.KotlinMetadataUtil;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Singleton;
/** Validates types annotated with component creator annotations. */
@Singleton
public final class ComponentCreatorValidator implements ClearableCache {
private final Map reports = new HashMap<>();
private final MethodSignatureFormatter methodSignatureFormatter;
private final KotlinMetadataUtil metadataUtil;
@Inject
ComponentCreatorValidator(
MethodSignatureFormatter methodSignatureFormatter, KotlinMetadataUtil metadataUtil) {
this.methodSignatureFormatter = methodSignatureFormatter;
this.metadataUtil = metadataUtil;
}
@Override
public void clearCache() {
reports.clear();
}
/** Validates that the given {@code type} is potentially a valid component creator type. */
public ValidationReport validate(XTypeElement type) {
return reentrantComputeIfAbsent(reports, type, this::validateUncached);
}
private ValidationReport validateUncached(XTypeElement type) {
ValidationReport.Builder report = ValidationReport.about(type);
ImmutableSet creatorAnnotations = getCreatorAnnotations(type);
if (!validateOnlyOneCreatorAnnotation(creatorAnnotations, report)) {
return report.build();
}
// Note: there's more validation in ComponentDescriptorValidator:
// - to make sure the setter methods/factory parameters mirror the deps
// - to make sure each type or key is set by only one method or parameter
ElementValidator validator =
new ElementValidator(type, report, getOnlyElement(creatorAnnotations));
return validator.validate();
}
private boolean validateOnlyOneCreatorAnnotation(
ImmutableSet creatorAnnotations,
ValidationReport.Builder report) {
// creatorAnnotations should never be empty because this should only ever be called for
// types that have been found to have some creator annotation
if (creatorAnnotations.size() > 1) {
String error =
"May not have more than one component Factory or Builder annotation on a type"
+ ": found "
+ creatorAnnotations;
report.addError(error);
return false;
}
return true;
}
/**
* Validator for a single {@link XTypeElement} that is annotated with a {@code Builder} or {@code
* Factory} annotation.
*/
private final class ElementValidator {
private final XTypeElement creator;
private final ValidationReport.Builder report;
private final ComponentCreatorAnnotation annotation;
private final ComponentCreatorMessages messages;
private ElementValidator(
XTypeElement creator,
ValidationReport.Builder report,
ComponentCreatorAnnotation annotation) {
this.creator = creator;
this.report = report;
this.annotation = annotation;
this.messages = ErrorMessages.creatorMessagesFor(annotation);
}
/** Validates the creator type. */
final ValidationReport validate() {
XTypeElement enclosingType = creator.getEnclosingTypeElement();
// If the type isn't enclosed in a component don't validate anything else since the rest of
// the messages will be bogus.
if (enclosingType == null
|| !enclosingType.hasAnnotation(annotation.componentAnnotation())) {
return report.addError(messages.mustBeInComponent()).build();
}
// If the type isn't a class or interface, don't validate anything else since the rest of the
// messages will be bogus.
if (!validateIsClassOrInterface()) {
return report.build();
}
// If the type isn't a valid creator type, don't validate anything else since the rest of the
// messages will be bogus.
if (!validateTypeRequirements()) {
return report.build();
}
switch (annotation.creatorKind()) {
case FACTORY:
validateFactory();
break;
case BUILDER:
validateBuilder();
}
return report.build();
}
/** Validates that the type is a class or interface type and returns true if it is. */
private boolean validateIsClassOrInterface() {
if (creator.isClass()) {
validateConstructor();
return true;
}
if (creator.isInterface()) {
return true;
}
report.addError(messages.mustBeClassOrInterface());
return false;
}
private void validateConstructor() {
List constructors = creator.getConstructors();
boolean valid = true;
if (constructors.size() != 1) {
valid = false;
} else {
XConstructorElement constructor = getOnlyElement(constructors);
valid = constructor.getParameters().isEmpty() && !constructor.isPrivate();
}
if (!valid) {
report.addError(messages.invalidConstructor());
}
}
/** Validates basic requirements about the type that are common to both creator kinds. */
private boolean validateTypeRequirements() {
boolean isClean = true;
if (hasTypeParameters(creator)) {
report.addError(messages.generics());
isClean = false;
}
if (creator.isPrivate()) {
report.addError(messages.isPrivate());
isClean = false;
}
if (!creator.isStatic()) {
report.addError(messages.mustBeStatic());
isClean = false;
}
// Note: Must be abstract, so no need to check for final.
if (!creator.isAbstract()) {
report.addError(messages.mustBeAbstract());
isClean = false;
}
return isClean;
}
private void validateBuilder() {
validateClassMethodName();
XMethodElement buildMethod = null;
for (XMethodElement method : getAllUnimplementedMethods(creator)) {
switch (method.getParameters().size()) {
case 0: // If this is potentially a build() method, validate it returns the correct type.
if (validateFactoryMethodReturnType(method)) {
if (buildMethod != null) {
// If we found more than one build-like method, fail.
error(
method,
messages.twoFactoryMethods(),
messages.inheritedTwoFactoryMethods(),
methodSignatureFormatter.format(buildMethod));
}
}
// We set the buildMethod regardless of the return type to reduce error spam.
buildMethod = method;
break;
case 1: // If this correctly had one parameter, make sure the return types are valid.
validateSetterMethod(method);
break;
default: // more than one parameter
error(
method,
messages.setterMethodsMustTakeOneArg(),
messages.inheritedSetterMethodsMustTakeOneArg());
break;
}
}
if (buildMethod == null) {
report.addError(messages.missingFactoryMethod());
} else {
validateNotGeneric(buildMethod);
}
}
private void validateClassMethodName() {
// Only Kotlin class can have method name the same as a Java reserved keyword, so only check
// the method name if this class is a Kotlin class.
if (metadataUtil.hasMetadata(creator)) {
metadataUtil
.getAllMethodNamesBySignature(creator)
.forEach(
(signature, name) -> {
if (isKeyword(name)) {
report.addError("Can not use a Java keyword as method name: " + signature);
}
});
}
}
private void validateSetterMethod(XMethodElement method) {
XType returnType = method.asMemberOf(creator.getType()).getReturnType();
if (!isVoid(returnType) && !isSubtype(creator.getType(), returnType)) {
error(
method,
messages.setterMethodsMustReturnVoidOrBuilder(),
messages.inheritedSetterMethodsMustReturnVoidOrBuilder());
}
validateNotGeneric(method);
XExecutableParameterElement parameter = method.getParameters().get(0);
boolean methodIsBindsInstance = method.hasAnnotation(TypeNames.BINDS_INSTANCE);
boolean parameterIsBindsInstance = parameter.hasAnnotation(TypeNames.BINDS_INSTANCE);
boolean bindsInstance = methodIsBindsInstance || parameterIsBindsInstance;
if (methodIsBindsInstance && parameterIsBindsInstance) {
error(
method,
messages.bindsInstanceNotAllowedOnBothSetterMethodAndParameter(),
messages.inheritedBindsInstanceNotAllowedOnBothSetterMethodAndParameter());
}
if (!bindsInstance && isPrimitive(parameter.getType())) {
error(
method,
messages.nonBindsInstanceParametersMayNotBePrimitives(),
messages.inheritedNonBindsInstanceParametersMayNotBePrimitives());
}
}
private void validateFactory() {
ImmutableList abstractMethods = getAllUnimplementedMethods(creator);
switch (abstractMethods.size()) {
case 0:
report.addError(messages.missingFactoryMethod());
return;
case 1:
break; // good
default:
error(
abstractMethods.get(1),
messages.twoFactoryMethods(),
messages.inheritedTwoFactoryMethods(),
methodSignatureFormatter.format(abstractMethods.get(0)));
return;
}
validateFactoryMethod(getOnlyElement(abstractMethods));
}
/** Validates that the given {@code method} is a valid component factory method. */
private void validateFactoryMethod(XMethodElement method) {
validateNotGeneric(method);
if (!validateFactoryMethodReturnType(method)) {
// If we can't determine that the single method is a valid factory method, don't bother
// validating its parameters.
return;
}
for (XExecutableParameterElement parameter : method.getParameters()) {
if (!parameter.hasAnnotation(TypeNames.BINDS_INSTANCE)
&& isPrimitive(parameter.getType())) {
error(
method,
messages.nonBindsInstanceParametersMayNotBePrimitives(),
messages.inheritedNonBindsInstanceParametersMayNotBePrimitives());
}
}
}
/**
* Validates that the factory method that actually returns a new component instance. Returns
* true if the return type was valid.
*/
private boolean validateFactoryMethodReturnType(XMethodElement method) {
XTypeElement component = creator.getEnclosingTypeElement();
XType returnType = method.asMemberOf(creator.getType()).getReturnType();
if (!isSubtype(component.getType(), returnType)) {
error(
method,
messages.factoryMethodMustReturnComponentType(),
messages.inheritedFactoryMethodMustReturnComponentType());
return false;
}
if (method.hasAnnotation(TypeNames.BINDS_INSTANCE)) {
error(
method,
messages.factoryMethodMayNotBeAnnotatedWithBindsInstance(),
messages.inheritedFactoryMethodMayNotBeAnnotatedWithBindsInstance());
return false;
}
if (!returnType.isSameType(component.getType())) {
// TODO(ronshapiro): Ideally this shouldn't return methods which are redeclared from a
// supertype, but do not change the return type. We don't have a good/simple way of checking
// that, and it doesn't seem likely, so the warning won't be too bad.
ImmutableSet declaredMethods =
ImmutableSet.copyOf(component.getDeclaredMethods());
if (!declaredMethods.isEmpty()) {
report.addWarning(
messages.factoryMethodReturnsSupertypeWithMissingMethods(
component, creator, returnType, method, declaredMethods),
method);
}
}
return true;
}
/**
* Generates one of two error messages. If the method is enclosed in the subject, we target the
* error to the method itself. Otherwise we target the error to the subject and list the method
* as an argument. (Otherwise we have no way of knowing if the method is being compiled in this
* pass too, so javac might not be able to pinpoint it's line of code.)
*/
/*
* For Component.Builder, the prototypical example would be if someone had:
* libfoo: interface SharedBuilder { void badSetter(A a, B b); }
* libbar: BarComponent { BarBuilder extends SharedBuilder } }
* ... the compiler only validates BarBuilder when compiling libbar, but it fails because
* of libfoo's SharedBuilder (which could have been compiled in a previous pass).
* So we can't point to SharedBuilder#badSetter as the subject of the BarBuilder validation
* failure.
*
* This check is a little more strict than necessary -- ideally we'd check if method's enclosing
* class was included in this compile run. But that's hard, and this is close enough.
*/
private void error(
XMethodElement method, String enclosedError, String inheritedError, Object... extraArgs) {
if (method.getEnclosingElement().equals(creator)) {
report.addError(String.format(enclosedError, extraArgs), method);
} else {
report.addError(
String.format(
inheritedError,
ObjectArrays.concat(extraArgs, methodSignatureFormatter.format(method))));
}
}
/** Validates that the given {@code method} is not generic. * */
private void validateNotGeneric(XMethodElement method) {
if (hasTypeParameters(method)) {
error(
method,
messages.methodsMayNotHaveTypeParameters(),
messages.inheritedMethodsMayNotHaveTypeParameters());
}
}
}
}