/* * 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.common.collect.Iterables.getOnlyElement; import static dagger.internal.codegen.base.ComponentAnnotation.isComponentAnnotation; import static dagger.internal.codegen.base.ComponentAnnotation.subcomponentAnnotation; import static dagger.internal.codegen.base.ComponentCreatorAnnotation.getCreatorAnnotations; import static dagger.internal.codegen.base.ModuleAnnotation.isModuleAnnotation; import static dagger.internal.codegen.base.Util.reentrantComputeIfAbsent; import static dagger.internal.codegen.binding.ConfigurationAnnotations.getSubcomponentCreator; import static dagger.internal.codegen.extension.DaggerCollectors.toOptional; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; import static dagger.internal.codegen.xprocessing.XAnnotations.getClassName; import static dagger.internal.codegen.xprocessing.XElements.getSimpleName; import static dagger.internal.codegen.xprocessing.XElements.hasAnyAnnotation; import static dagger.internal.codegen.xprocessing.XTypeElements.hasTypeParameters; import static dagger.internal.codegen.xprocessing.XTypeElements.isEffectivelyPrivate; import static dagger.internal.codegen.xprocessing.XTypeElements.isEffectivelyPublic; import static dagger.internal.codegen.xprocessing.XTypes.areEquivalentTypes; import static dagger.internal.codegen.xprocessing.XTypes.isDeclared; import static java.util.stream.Collectors.joining; import static kotlin.streams.jdk8.StreamsKt.asStream; import androidx.room.compiler.processing.XAnnotation; import androidx.room.compiler.processing.XAnnotationValue; import androidx.room.compiler.processing.XMethodElement; import androidx.room.compiler.processing.XProcessingEnv; import androidx.room.compiler.processing.XType; import androidx.room.compiler.processing.XTypeElement; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ListMultimap; import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Multimaps; import com.google.common.collect.Sets; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.TypeName; import dagger.internal.codegen.base.ComponentCreatorAnnotation; import dagger.internal.codegen.base.DaggerSuperficialValidation; import dagger.internal.codegen.base.ModuleKind; import dagger.internal.codegen.binding.BindingGraphFactory; import dagger.internal.codegen.binding.ComponentDescriptorFactory; import dagger.internal.codegen.binding.InjectionAnnotations; import dagger.internal.codegen.binding.MethodSignatureFormatter; import dagger.internal.codegen.javapoet.TypeNames; import dagger.internal.codegen.xprocessing.XElements; import dagger.spi.model.BindingGraph; import dagger.spi.model.Scope; import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; /** * A {@linkplain ValidationReport validator} for {@link dagger.Module}s or {@link * dagger.producers.ProducerModule}s. */ @Singleton public final class ModuleValidator { private static final ImmutableSet SUBCOMPONENT_TYPES = ImmutableSet.of(TypeNames.SUBCOMPONENT, TypeNames.PRODUCTION_SUBCOMPONENT); private static final ImmutableSet SUBCOMPONENT_CREATOR_TYPES = ImmutableSet.of( TypeNames.SUBCOMPONENT_BUILDER, TypeNames.SUBCOMPONENT_FACTORY, TypeNames.PRODUCTION_SUBCOMPONENT_BUILDER, TypeNames.PRODUCTION_SUBCOMPONENT_FACTORY); private static final Optional> ANDROID_PROCESSOR; private static final String CONTRIBUTES_ANDROID_INJECTOR_NAME = "dagger.android.ContributesAndroidInjector"; private static final String ANDROID_PROCESSOR_NAME = "dagger.android.processor.AndroidProcessor"; static { Class clazz; try { clazz = Class.forName(ANDROID_PROCESSOR_NAME, false, ModuleValidator.class.getClassLoader()); } catch (ClassNotFoundException ignored) { clazz = null; } ANDROID_PROCESSOR = Optional.ofNullable(clazz); } private final AnyBindingMethodValidator anyBindingMethodValidator; private final MethodSignatureFormatter methodSignatureFormatter; private final ComponentDescriptorFactory componentDescriptorFactory; private final BindingGraphFactory bindingGraphFactory; private final BindingGraphValidator bindingGraphValidator; private final InjectionAnnotations injectionAnnotations; private final DaggerSuperficialValidation superficialValidation; private final XProcessingEnv processingEnv; private final Map cache = new HashMap<>(); private final Set knownModules = new HashSet<>(); @Inject ModuleValidator( AnyBindingMethodValidator anyBindingMethodValidator, MethodSignatureFormatter methodSignatureFormatter, ComponentDescriptorFactory componentDescriptorFactory, BindingGraphFactory bindingGraphFactory, BindingGraphValidator bindingGraphValidator, InjectionAnnotations injectionAnnotations, DaggerSuperficialValidation superficialValidation, XProcessingEnv processingEnv) { this.anyBindingMethodValidator = anyBindingMethodValidator; this.methodSignatureFormatter = methodSignatureFormatter; this.componentDescriptorFactory = componentDescriptorFactory; this.bindingGraphFactory = bindingGraphFactory; this.bindingGraphValidator = bindingGraphValidator; this.injectionAnnotations = injectionAnnotations; this.superficialValidation = superficialValidation; this.processingEnv = processingEnv; } /** * Adds {@code modules} to the set of module types that will be validated during this compilation * step. If a component or module includes a module that is not in this set, that included module * is assumed to be valid because it was processed in a previous compilation step. If it were * invalid, that previous compilation step would have failed and blocked this one. * *

This logic depends on this method being called before {@linkplain #validate(XTypeElement) * validating} any module or {@linkplain #validateReferencedModules(XTypeElement, ModuleKind, Set, * DiagnosticReporter.Builder) component}. */ public void addKnownModules(Collection modules) { knownModules.addAll(modules); } /** Returns a validation report for a module type. */ public ValidationReport validate(XTypeElement module) { return validate(module, new HashSet<>()); } private ValidationReport validate(XTypeElement module, Set visitedModules) { if (visitedModules.add(module)) { return reentrantComputeIfAbsent(cache, module, m -> validateUncached(module, visitedModules)); } return ValidationReport.about(module).build(); } private ValidationReport validateUncached(XTypeElement module, Set visitedModules) { ValidationReport.Builder builder = ValidationReport.about(module); ModuleKind moduleKind = ModuleKind.forAnnotatedElement(module).get(); Optional contributesAndroidInjector = Optional.ofNullable(processingEnv.findTypeElement(CONTRIBUTES_ANDROID_INJECTOR_NAME)) .map(XTypeElement::getType); List moduleMethods = module.getDeclaredMethods(); List bindingMethods = new ArrayList<>(); for (XMethodElement moduleMethod : moduleMethods) { if (anyBindingMethodValidator.isBindingMethod(moduleMethod)) { builder.addSubreport(anyBindingMethodValidator.validate(moduleMethod)); bindingMethods.add(moduleMethod); } for (XAnnotation annotation : moduleMethod.getAllAnnotations()) { if (!ANDROID_PROCESSOR.isPresent() && contributesAndroidInjector.isPresent() && areEquivalentTypes(contributesAndroidInjector.get(), annotation.getType())) { builder.addSubreport( ValidationReport.about(moduleMethod) .addError( String.format( "@%s was used, but %s was not found on the processor path", CONTRIBUTES_ANDROID_INJECTOR_NAME, ANDROID_PROCESSOR_NAME)) .build()); break; } } } if (bindingMethods.stream() .map(ModuleMethodKind::ofMethod) .collect(toImmutableSet()) .containsAll( EnumSet.of(ModuleMethodKind.ABSTRACT_DECLARATION, ModuleMethodKind.INSTANCE_BINDING))) { builder.addError( String.format( "A @%s may not contain both non-static and abstract binding methods", moduleKind.annotation().simpleName())); } validateModuleVisibility(module, moduleKind, builder); ImmutableListMultimap bindingMethodsByName = Multimaps.index(bindingMethods, XElements::getSimpleName); validateMethodsWithSameName(builder, bindingMethodsByName); if (!module.isInterface()) { validateBindingMethodOverrides( module, builder, Multimaps.index(moduleMethods, XElements::getSimpleName), bindingMethodsByName); } validateModifiers(module, builder); validateReferencedModules(module, moduleKind, visitedModules, builder); validateReferencedSubcomponents(module, moduleKind, builder); validateNoScopeAnnotationsOnModuleElement(module, moduleKind, builder); validateSelfCycles(module, moduleKind, builder); module.getEnclosedTypeElements().stream() .filter(XTypeElement::isCompanionObject) .collect(toOptional()) .ifPresent(companionModule -> validateCompanionModule(companionModule, builder)); if (builder.build().isClean() && bindingGraphValidator.shouldDoFullBindingGraphValidation(module)) { validateModuleBindings(module, builder); } return builder.build(); } private void validateReferencedSubcomponents( XTypeElement subject, ModuleKind moduleKind, ValidationReport.Builder builder) { XAnnotation moduleAnnotation = moduleKind.getModuleAnnotation(subject); for (XAnnotationValue subcomponentValue : moduleAnnotation.getAsAnnotationValueList("subcomponents")) { XType type = subcomponentValue.asType(); if (!isDeclared(type)) { builder.addError( type + " is not a valid subcomponent type", subject, moduleAnnotation, subcomponentValue); continue; } XTypeElement subcomponentElement = type.getTypeElement(); if (hasAnyAnnotation(subcomponentElement, SUBCOMPONENT_TYPES)) { validateSubcomponentHasBuilder(subject, subcomponentElement, moduleAnnotation, builder); } else { builder.addError( hasAnyAnnotation(subcomponentElement, SUBCOMPONENT_CREATOR_TYPES) ? moduleSubcomponentsIncludesCreator(subcomponentElement) : moduleSubcomponentsIncludesNonSubcomponent(subcomponentElement), subject, moduleAnnotation, subcomponentValue); } } } private static String moduleSubcomponentsIncludesNonSubcomponent(XTypeElement notSubcomponent) { return notSubcomponent.getQualifiedName() + " is not a @Subcomponent or @ProductionSubcomponent"; } private String moduleSubcomponentsIncludesCreator(XTypeElement moduleSubcomponentsAttribute) { XTypeElement subcomponentType = moduleSubcomponentsAttribute.getEnclosingTypeElement(); ComponentCreatorAnnotation creatorAnnotation = getOnlyElement(getCreatorAnnotations(moduleSubcomponentsAttribute)); return String.format( "%s is a @%s.%s. Did you mean to use %s?", moduleSubcomponentsAttribute.getQualifiedName(), subcomponentAnnotation(subcomponentType, superficialValidation).get().simpleName(), creatorAnnotation.creatorKind().typeName(), subcomponentType.getQualifiedName()); } private void validateSubcomponentHasBuilder( XTypeElement subject, XTypeElement subcomponentAttribute, XAnnotation moduleAnnotation, ValidationReport.Builder builder) { if (getSubcomponentCreator(subcomponentAttribute).isPresent()) { return; } builder.addError( moduleSubcomponentsDoesntHaveCreator(subcomponentAttribute, moduleAnnotation), subject, moduleAnnotation); } private String moduleSubcomponentsDoesntHaveCreator( XTypeElement subcomponent, XAnnotation moduleAnnotation) { return String.format( "%1$s doesn't have a @%2$s.Builder or @%2$s.Factory, which is required when used with " + "@%3$s.subcomponents", subcomponent.getQualifiedName(), subcomponentAnnotation(subcomponent, superficialValidation).get().simpleName(), getClassName(moduleAnnotation).simpleName()); } enum ModuleMethodKind { ABSTRACT_DECLARATION, INSTANCE_BINDING, STATIC_BINDING, ; static ModuleMethodKind ofMethod(XMethodElement moduleMethod) { if (moduleMethod.isStatic()) { return STATIC_BINDING; } else if (moduleMethod.isAbstract()) { return ABSTRACT_DECLARATION; } else { return INSTANCE_BINDING; } } } private void validateModifiers(XTypeElement subject, ValidationReport.Builder builder) { // This coupled with the check for abstract modules in ComponentValidator guarantees that // only modules without type parameters are referenced from @Component(modules={...}). if (hasTypeParameters(subject) && !subject.isAbstract()) { builder.addError("Modules with type parameters must be abstract", subject); } } private void validateMethodsWithSameName( ValidationReport.Builder builder, ListMultimap bindingMethodsByName) { bindingMethodsByName.asMap().values().stream() .filter(methods -> methods.size() > 1) .flatMap(Collection::stream) .forEach( duplicateMethod -> { builder.addError( "Cannot have more than one binding method with the same name in a single module", duplicateMethod); }); } private void validateReferencedModules( XTypeElement subject, ModuleKind moduleKind, Set visitedModules, ValidationReport.Builder builder) { // Validate that all the modules we include are valid for inclusion. XAnnotation mirror = moduleKind.getModuleAnnotation(subject); builder.addSubreport( validateReferencedModules( subject, mirror, moduleKind.legalIncludedModuleKinds(), visitedModules)); } /** * Validates modules included in a given module or installed in a given component. * *

Checks that the referenced modules are non-generic types annotated with {@code @Module} or * {@code @ProducerModule}. * *

If the referenced module is in the {@linkplain #addKnownModules(Collection) known modules * set} and has errors, reports an error at that module's inclusion. * * @param annotatedType the annotated module or component * @param annotation the annotation specifying the referenced modules ({@code @Component}, * {@code @ProductionComponent}, {@code @Subcomponent}, {@code @ProductionSubcomponent}, * {@code @Module}, or {@code @ProducerModule}) * @param validModuleKinds the module kinds that the annotated type is permitted to include */ ValidationReport validateReferencedModules( XTypeElement annotatedType, XAnnotation annotation, ImmutableSet validModuleKinds, Set visitedModules) { superficialValidation.validateAnnotationOf(annotatedType, annotation); ValidationReport.Builder subreport = ValidationReport.about(annotatedType); // TODO(bcorso): Consider creating a DiagnosticLocation object to encapsulate the location in a // single object to avoid duplication across all reported errors for (XAnnotationValue includedModule : getModules(annotation)) { XType type = includedModule.asType(); if (!isDeclared(type)) { subreport.addError( String.format("%s is not a valid module type.", type), annotatedType, annotation, includedModule); continue; } XTypeElement module = type.getTypeElement(); if (hasTypeParameters(module)) { subreport.addError( String.format( "%s is listed as a module, but has type parameters", module.getQualifiedName()), annotatedType, annotation, includedModule); } ImmutableSet validModuleAnnotations = validModuleKinds.stream().map(ModuleKind::annotation).collect(toImmutableSet()); if (!hasAnyAnnotation(module, validModuleAnnotations)) { subreport.addError( String.format( "%s is listed as a module, but is not annotated with %s", module.getQualifiedName(), (validModuleAnnotations.size() > 1 ? "one of " : "") + validModuleAnnotations.stream() .map(otherClass -> "@" + otherClass.simpleName()) .collect(joining(", "))), annotatedType, annotation, includedModule); } else if (knownModules.contains(module) && !validate(module, visitedModules).isClean()) { subreport.addError( String.format("%s has errors", module.getQualifiedName()), annotatedType, annotation, includedModule); } if (module.isCompanionObject()) { subreport.addError( String.format( "%s is listed as a module, but it is a companion object class. " + "Add @Module to the enclosing class and reference that instead.", module.getQualifiedName()), annotatedType, annotation, includedModule); } } return subreport.build(); } private static ImmutableList getModules(XAnnotation annotation) { if (isModuleAnnotation(annotation)) { return ImmutableList.copyOf(annotation.getAsAnnotationValueList("includes")); } if (isComponentAnnotation(annotation)) { return ImmutableList.copyOf(annotation.getAsAnnotationValueList("modules")); } throw new IllegalArgumentException(String.format("unsupported annotation: %s", annotation)); } private void validateBindingMethodOverrides( XTypeElement subject, ValidationReport.Builder builder, ImmutableListMultimap moduleMethodsByName, ImmutableListMultimap bindingMethodsByName) { // For every binding method, confirm it overrides nothing *and* nothing overrides it. // Consider the following hierarchy: // class Parent { // @Provides Foo a() {} // @Provides Foo b() {} // Foo c() {} // } // class Child extends Parent { // @Provides Foo a() {} // Foo b() {} // @Provides Foo c() {} // } // In each of those cases, we want to fail. "a" is clear, "b" because Child is overriding // a binding method in Parent, and "c" because Child is defining a binding method that overrides // Parent. XTypeElement currentClass = subject; XType objectType = processingEnv.findType(TypeName.OBJECT); // We keep track of visited methods so we don't spam with multiple failures. Set visitedMethods = Sets.newHashSet(); ListMultimap allMethodsByName = MultimapBuilder.hashKeys().arrayListValues().build(moduleMethodsByName); while (!currentClass.getSuperType().isSameType(objectType)) { currentClass = currentClass.getSuperType().getTypeElement(); List superclassMethods = currentClass.getDeclaredMethods(); for (XMethodElement superclassMethod : superclassMethods) { String name = getSimpleName(superclassMethod); // For each method in the superclass, confirm our binding methods don't override it for (XMethodElement bindingMethod : bindingMethodsByName.get(name)) { if (visitedMethods.add(bindingMethod) && bindingMethod.overrides(superclassMethod, subject)) { builder.addError( String.format( "Binding methods may not override another method. Overrides: %s", methodSignatureFormatter.format(superclassMethod)), bindingMethod); } } // For each binding method in superclass, confirm our methods don't override it. if (anyBindingMethodValidator.isBindingMethod(superclassMethod)) { for (XMethodElement method : allMethodsByName.get(name)) { if (visitedMethods.add(method) && method.overrides(superclassMethod, subject)) { builder.addError( String.format( "Binding methods may not be overridden in modules. Overrides: %s", methodSignatureFormatter.format(superclassMethod)), method); } } } // TODO(b/202521399): Add a test for cases that add to this map. allMethodsByName.put(getSimpleName(superclassMethod), superclassMethod); } } } private void validateModuleVisibility( XTypeElement moduleElement, ModuleKind moduleKind, ValidationReport.Builder reportBuilder) { if (moduleElement.isPrivate()) { reportBuilder.addError("Modules cannot be private.", moduleElement); } else if (isEffectivelyPrivate(moduleElement)) { reportBuilder.addError("Modules cannot be enclosed in private types.", moduleElement); } if (isEffectivelyPublic(moduleElement)) { ImmutableSet invalidVisibilityIncludes = getModuleIncludesWithInvalidVisibility(moduleKind.getModuleAnnotation(moduleElement)); if (!invalidVisibilityIncludes.isEmpty()) { reportBuilder.addError( String.format( "This module is public, but it includes non-public (or effectively non-public) " + "modules (%s) that have non-static, non-abstract binding methods. Either " + "reduce the visibility of this module, make the included modules " + "public, or make all of the binding methods on the included modules " + "abstract or static.", formatListForErrorMessage(invalidVisibilityIncludes.asList())), moduleElement); } } } private ImmutableSet getModuleIncludesWithInvalidVisibility( XAnnotation moduleAnnotation) { return moduleAnnotation.getAnnotationValue("includes").asTypeList().stream() .map(XType::getTypeElement) .filter(include -> !isEffectivelyPublic(include)) .filter(this::requiresModuleInstance) .collect(toImmutableSet()); } /** * Returns {@code true} if a module instance is needed for any of the binding methods on the given * {@code module}. This is the case when the module has any binding methods that are neither * {@code abstract} nor {@code static}. Alternatively, if the module is a Kotlin Object then the * binding methods are considered {@code static}, requiring no module instance. */ private boolean requiresModuleInstance(XTypeElement module) { // Note: We use XTypeElement#getAllMethods() rather than XTypeElement#getDeclaredMethods() here // because we need to include binding methods declared in supertypes because unlike most other // validations being done in this class, which assume that supertype binding methods will be // validated in a separate call to the validator since the supertype itself must be a @Module, // we need to look at all the binding methods in the module's type hierarchy here. return !(module.isKotlinObject() || module.isCompanionObject()) && !asStream(module.getAllMethods()) .filter(anyBindingMethodValidator::isBindingMethod) .allMatch(method -> method.isAbstract() || method.isStatic()); } private void validateNoScopeAnnotationsOnModuleElement( XTypeElement module, ModuleKind moduleKind, ValidationReport.Builder report) { for (Scope scope : injectionAnnotations.getScopes(module)) { report.addError( String.format( "@%ss cannot be scoped. Did you mean to scope a method instead?", moduleKind.annotation().simpleName()), module, scope.scopeAnnotation().xprocessing()); } } private void validateSelfCycles( XTypeElement module, ModuleKind moduleKind, ValidationReport.Builder builder) { XAnnotation moduleAnnotation = moduleKind.getModuleAnnotation(module); moduleAnnotation.getAsAnnotationValueList("includes").stream() .filter(includedModule -> areEquivalentTypes(module.getType(), includedModule.asType())) .forEach( includedModule -> builder.addError( String.format( "@%s cannot include themselves.", moduleKind.annotation().simpleName()), module, moduleAnnotation, includedModule)); } private void validateCompanionModule( XTypeElement companionModule, ValidationReport.Builder builder) { List companionBindingMethods = new ArrayList<>(); for (XMethodElement companionMethod : companionModule.getDeclaredMethods()) { if (anyBindingMethodValidator.isBindingMethod(companionMethod)) { builder.addSubreport(anyBindingMethodValidator.validate(companionMethod)); companionBindingMethods.add(companionMethod); } // On normal modules only overriding other binding methods is disallowed, but for companion // objects we are prohibiting any override. For this can rely on checking the @Override // annotation since the Kotlin compiler will always produce them for overriding methods. if (companionMethod.hasAnnotation(TypeNames.OVERRIDE)) { builder.addError( "Binding method in companion object may not override another method.", companionMethod); } // TODO(danysantiago): Be strict about the usage of @JvmStatic, i.e. tell user to remove it. } ImmutableListMultimap bindingMethodsByName = Multimaps.index(companionBindingMethods, XElements::getSimpleName); validateMethodsWithSameName(builder, bindingMethodsByName); // If there are provision methods, then check the visibility. Companion objects are composed by // an inner class and a static field, it is not enough to check the visibility on the type // element or the field, therefore we check the metadata. if (!companionBindingMethods.isEmpty() && companionModule.isPrivate()) { builder.addError( "A Companion Module with binding methods cannot be private.", companionModule); } } private void validateModuleBindings(XTypeElement module, ValidationReport.Builder report) { BindingGraph bindingGraph = bindingGraphFactory .create(componentDescriptorFactory.moduleComponentDescriptor(module), true) .topLevelBindingGraph(); if (!bindingGraphValidator.isValid(bindingGraph)) { // Since the validator uses a DiagnosticReporter to report errors, the ValdiationReport won't // have any Items for them. We have to tell the ValidationReport that some errors were // reported for the subject. report.markDirty(); } } private static String formatListForErrorMessage(List things) { switch (things.size()) { case 0: return ""; case 1: return things.get(0).toString(); default: StringBuilder output = new StringBuilder(); Joiner.on(", ").appendTo(output, things.subList(0, things.size() - 1)); output.append(" and ").append(things.get(things.size() - 1)); return output.toString(); } } }