/* * 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 com.google.auto.common.MoreElements.isAnnotationPresent; import static com.google.common.collect.Iterables.getOnlyElement; import static dagger.internal.codegen.base.Util.reentrantComputeIfAbsent; import static dagger.internal.codegen.binding.ComponentCreatorAnnotation.getCreatorAnnotations; import static javax.lang.model.element.Modifier.ABSTRACT; import static javax.lang.model.element.Modifier.PRIVATE; import static javax.lang.model.element.Modifier.STATIC; import static javax.lang.model.util.ElementFilter.methodsIn; import com.google.auto.common.MoreElements; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ObjectArrays; import dagger.BindsInstance; import dagger.internal.codegen.base.ClearableCache; import dagger.internal.codegen.binding.ComponentCreatorAnnotation; import dagger.internal.codegen.binding.ErrorMessages; import dagger.internal.codegen.binding.ErrorMessages.ComponentCreatorMessages; import dagger.internal.codegen.langmodel.DaggerElements; import dagger.internal.codegen.langmodel.DaggerTypes; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; 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; /** Validates types annotated with component creator annotations. */ @Singleton public final class ComponentCreatorValidator implements ClearableCache { private final DaggerElements elements; private final DaggerTypes types; private final Map> reports = new HashMap<>(); @Inject ComponentCreatorValidator(DaggerElements elements, DaggerTypes types) { this.elements = elements; this.types = types; } @Override public void clearCache() { reports.clear(); } /** Validates that the given {@code type} is potentially a valid component creator type. */ public ValidationReport validate(TypeElement type) { return reentrantComputeIfAbsent(reports, type, this::validateUncached); } private ValidationReport validateUncached(TypeElement 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 TypeElement} that is annotated with a {@code Builder} or {@code * Factory} annotation. */ private final class ElementValidator { private final TypeElement type; private final Element component; private final ValidationReport.Builder report; private final ComponentCreatorAnnotation annotation; private final ComponentCreatorMessages messages; private ElementValidator( TypeElement type, ValidationReport.Builder report, ComponentCreatorAnnotation annotation) { this.type = type; this.component = type.getEnclosingElement(); this.report = report; this.annotation = annotation; this.messages = ErrorMessages.creatorMessagesFor(annotation); } /** Validates the creator type. */ final ValidationReport validate() { if (!isAnnotationPresent(component, annotation.componentAnnotation())) { report.addError(messages.mustBeInComponent()); } // 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(); } validateTypeRequirements(); 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() { switch (type.getKind()) { case CLASS: validateConstructor(); return true; case INTERFACE: return true; default: report.addError(messages.mustBeClassOrInterface()); } return false; } private void validateConstructor() { List allElements = type.getEnclosedElements(); List constructors = ElementFilter.constructorsIn(allElements); boolean valid = true; if (constructors.size() != 1) { valid = false; } else { ExecutableElement constructor = getOnlyElement(constructors); valid = constructor.getParameters().isEmpty() && !constructor.getModifiers().contains(PRIVATE); } if (!valid) { report.addError(messages.invalidConstructor()); } } /** Validates basic requirements about the type that are common to both creator kinds. */ private void validateTypeRequirements() { if (!type.getTypeParameters().isEmpty()) { report.addError(messages.generics()); } Set modifiers = type.getModifiers(); if (modifiers.contains(PRIVATE)) { report.addError(messages.isPrivate()); } if (!modifiers.contains(STATIC)) { report.addError(messages.mustBeStatic()); } // Note: Must be abstract, so no need to check for final. if (!modifiers.contains(ABSTRACT)) { report.addError(messages.mustBeAbstract()); } } private void validateBuilder() { ExecutableElement buildMethod = null; for (ExecutableElement method : elements.getUnimplementedMethods(type)) { 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(), 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 validateSetterMethod(ExecutableElement method) { TypeMirror returnType = types.resolveExecutableType(method, type.asType()).getReturnType(); if (returnType.getKind() != TypeKind.VOID && !types.isSubtype(type.asType(), returnType)) { error( method, messages.setterMethodsMustReturnVoidOrBuilder(), messages.inheritedSetterMethodsMustReturnVoidOrBuilder()); } validateNotGeneric(method); VariableElement parameter = method.getParameters().get(0); boolean methodIsBindsInstance = isAnnotationPresent(method, BindsInstance.class); boolean parameterIsBindsInstance = isAnnotationPresent(parameter, BindsInstance.class); boolean bindsInstance = methodIsBindsInstance || parameterIsBindsInstance; if (methodIsBindsInstance && parameterIsBindsInstance) { error( method, messages.bindsInstanceNotAllowedOnBothSetterMethodAndParameter(), messages.inheritedBindsInstanceNotAllowedOnBothSetterMethodAndParameter()); } if (!bindsInstance && parameter.asType().getKind().isPrimitive()) { error( method, messages.nonBindsInstanceParametersMayNotBePrimitives(), messages.inheritedNonBindsInstanceParametersMayNotBePrimitives()); } } private void validateFactory() { ImmutableList abstractMethods = elements.getUnimplementedMethods(type).asList(); switch (abstractMethods.size()) { case 0: report.addError(messages.missingFactoryMethod()); return; case 1: break; // good default: error( abstractMethods.get(1), messages.twoFactoryMethods(), messages.inheritedTwoFactoryMethods(), abstractMethods.get(0)); return; } validateFactoryMethod(getOnlyElement(abstractMethods)); } /** Validates that the given {@code method} is a valid component factory method. */ private void validateFactoryMethod(ExecutableElement 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 (VariableElement parameter : method.getParameters()) { if (!isAnnotationPresent(parameter, BindsInstance.class) && parameter.asType().getKind().isPrimitive()) { 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(ExecutableElement method) { TypeMirror returnType = types.resolveExecutableType(method, type.asType()).getReturnType(); if (!types.isSubtype(component.asType(), returnType)) { error( method, messages.factoryMethodMustReturnComponentType(), messages.inheritedFactoryMethodMustReturnComponentType()); return false; } if (isAnnotationPresent(method, BindsInstance.class)) { error( method, messages.factoryMethodMayNotBeAnnotatedWithBindsInstance(), messages.inheritedFactoryMethodMayNotBeAnnotatedWithBindsInstance()); return false; } TypeElement componentType = MoreElements.asType(component); if (!types.isSameType(componentType.asType(), returnType)) { ImmutableSet methodsOnlyInComponent = methodsOnlyInComponent(componentType); if (!methodsOnlyInComponent.isEmpty()) { report.addWarning( messages.factoryMethodReturnsSupertypeWithMissingMethods( componentType, type, returnType, method, methodsOnlyInComponent), 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( ExecutableElement method, String enclosedError, String inheritedError, Object... extraArgs) { if (method.getEnclosingElement().equals(type)) { report.addError(String.format(enclosedError, extraArgs), method); } else { report.addError(String.format(inheritedError, ObjectArrays.concat(extraArgs, method))); } } /** Validates that the given {@code method} is not generic. * */ private void validateNotGeneric(ExecutableElement method) { if (!method.getTypeParameters().isEmpty()) { error( method, messages.methodsMayNotHaveTypeParameters(), messages.inheritedMethodsMayNotHaveTypeParameters()); } } /** * Returns all methods defind in {@code componentType} which are not inherited from a supertype. */ private ImmutableSet methodsOnlyInComponent(TypeElement componentType) { // 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. return ImmutableSet.copyOf(methodsIn(componentType.getEnclosedElements())); } } }