All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dagger.internal.codegen.validation.ComponentDescriptorValidator Maven / Gradle / Ivy

There is a newer version: 2.52
Show newest version
/*
 * Copyright (C) 2018 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.XElementKt.isMethod;
import static androidx.room.compiler.processing.XElementKt.isMethodParameter;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Predicates.in;
import static com.google.common.collect.Collections2.transform;
import static dagger.internal.codegen.base.ComponentAnnotation.rootComponentAnnotation;
import static dagger.internal.codegen.base.DiagnosticFormatting.stripCommonTypePrefixes;
import static dagger.internal.codegen.base.Formatter.INDENT;
import static dagger.internal.codegen.base.Scopes.getReadableSource;
import static dagger.internal.codegen.base.Util.reentrantComputeIfAbsent;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSetMultimap;
import static dagger.internal.codegen.xprocessing.XElements.asMethod;
import static dagger.internal.codegen.xprocessing.XElements.asMethodParameter;
import static dagger.internal.codegen.xprocessing.XElements.getSimpleName;
import static java.util.stream.Collectors.joining;
import static javax.tools.Diagnostic.Kind.ERROR;

import androidx.room.compiler.processing.XAnnotation;
import androidx.room.compiler.processing.XElement;
import androidx.room.compiler.processing.XExecutableParameterElement;
import androidx.room.compiler.processing.XMethodElement;
import androidx.room.compiler.processing.XMethodType;
import androidx.room.compiler.processing.XType;
import androidx.room.compiler.processing.XTypeElement;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
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.DaggerSuperficialValidation;
import dagger.internal.codegen.binding.ComponentCreatorDescriptor;
import dagger.internal.codegen.binding.ComponentDescriptor;
import dagger.internal.codegen.binding.ComponentRequirement;
import dagger.internal.codegen.binding.ComponentRequirement.NullPolicy;
import dagger.internal.codegen.binding.ContributionBinding;
import dagger.internal.codegen.binding.ErrorMessages;
import dagger.internal.codegen.binding.ErrorMessages.ComponentCreatorMessages;
import dagger.internal.codegen.binding.InjectionAnnotations;
import dagger.internal.codegen.binding.MethodSignatureFormatter;
import dagger.internal.codegen.binding.ModuleDescriptor;
import dagger.internal.codegen.compileroption.CompilerOptions;
import dagger.internal.codegen.compileroption.ValidationType;
import dagger.internal.codegen.javapoet.TypeNames;
import dagger.internal.codegen.model.Scope;
import dagger.internal.codegen.xprocessing.XTypes;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
import javax.inject.Inject;
import javax.tools.Diagnostic;

/**
 * Reports errors in the component hierarchy.
 *
 * 
    *
  • Validates scope hierarchy of component dependencies and subcomponents. *
  • Reports errors if there are component dependency cycles. *
  • Reports errors if any abstract modules have non-abstract instance binding methods. *
  • Validates component creator types. *
*/ // TODO(dpb): Combine with ComponentHierarchyValidator. public final class ComponentDescriptorValidator { private final CompilerOptions compilerOptions; private final MethodSignatureFormatter methodSignatureFormatter; private final ComponentHierarchyValidator componentHierarchyValidator; private final InjectionAnnotations injectionAnnotations; private final DaggerSuperficialValidation superficialValidation; @Inject ComponentDescriptorValidator( CompilerOptions compilerOptions, MethodSignatureFormatter methodSignatureFormatter, ComponentHierarchyValidator componentHierarchyValidator, InjectionAnnotations injectionAnnotations, DaggerSuperficialValidation superficialValidation) { this.compilerOptions = compilerOptions; this.methodSignatureFormatter = methodSignatureFormatter; this.componentHierarchyValidator = componentHierarchyValidator; this.injectionAnnotations = injectionAnnotations; this.superficialValidation = superficialValidation; } public ValidationReport validate(ComponentDescriptor component) { ComponentValidation validation = new ComponentValidation(component); validation.visitComponent(component); validation.report(component).addSubreport(componentHierarchyValidator.validate(component)); return validation.buildReport(); } private final class ComponentValidation { final ComponentDescriptor rootComponent; final Map reports = new LinkedHashMap<>(); ComponentValidation(ComponentDescriptor rootComponent) { this.rootComponent = checkNotNull(rootComponent); } /** Returns a report that contains all validation messages found during traversal. */ ValidationReport buildReport() { ValidationReport.Builder report = ValidationReport.about(rootComponent.typeElement()); reports.values().forEach(subreport -> report.addSubreport(subreport.build())); return report.build(); } /** Returns the report builder for a (sub)component. */ private ValidationReport.Builder report(ComponentDescriptor component) { return reentrantComputeIfAbsent( reports, component, descriptor -> ValidationReport.about(descriptor.typeElement())); } private void reportComponentItem( Diagnostic.Kind kind, ComponentDescriptor component, String message) { report(component) .addItem(message, kind, component.typeElement(), component.annotation().annotation()); } private void reportComponentError(ComponentDescriptor component, String error) { reportComponentItem(ERROR, component, error); } void visitComponent(ComponentDescriptor component) { validateDependencyScopes(component); validateComponentDependencyHierarchy(component); validateModules(component); validateCreators(component); component.childComponents().forEach(this::visitComponent); } /** Validates that component dependencies do not form a cycle. */ private void validateComponentDependencyHierarchy(ComponentDescriptor component) { validateComponentDependencyHierarchy(component, component.typeElement(), new ArrayDeque<>()); } /** Recursive method to validate that component dependencies do not form a cycle. */ private void validateComponentDependencyHierarchy( ComponentDescriptor component, XTypeElement dependency, Deque dependencyStack) { if (dependencyStack.contains(dependency)) { // Current component has already appeared in the component chain. StringBuilder message = new StringBuilder(); message.append(component.typeElement().getQualifiedName()); message.append(" contains a cycle in its component dependencies:\n"); dependencyStack.push(dependency); appendIndentedComponentsList(message, dependencyStack); dependencyStack.pop(); reportComponentItem( compilerOptions.scopeCycleValidationType().diagnosticKind().get(), component, message.toString()); } else if (compilerOptions.validateTransitiveComponentDependencies() // Always validate direct component dependencies referenced by this component regardless // of the flag value || dependencyStack.isEmpty()) { rootComponentAnnotation(dependency, superficialValidation) .ifPresent( componentAnnotation -> { dependencyStack.push(dependency); for (XTypeElement nextDependency : componentAnnotation.dependencies()) { validateComponentDependencyHierarchy( component, nextDependency, dependencyStack); } dependencyStack.pop(); }); } } /** * Validates that among the dependencies there are no cycles within the scoping chain, and that * singleton components have no scoped dependencies. */ private void validateDependencyScopes(ComponentDescriptor component) { ImmutableSet scopes = component.scopes().stream().map(Scope::className).collect(toImmutableSet()); ImmutableSet scopedDependencies = scopedTypesIn( component.dependencies().stream() .map(ComponentRequirement::typeElement) .collect(toImmutableSet())); if (!scopes.isEmpty()) { // Dagger 1.x scope compatibility requires this be suppress-able. if (compilerOptions.scopeCycleValidationType().diagnosticKind().isPresent() && (scopes.contains(TypeNames.SINGLETON) || scopes.contains(TypeNames.SINGLETON_JAVAX))) { // Singleton is a special-case representing the longest lifetime, and therefore // @Singleton components may not depend on scoped components if (!scopedDependencies.isEmpty()) { StringBuilder message = new StringBuilder( "This @Singleton component cannot depend on scoped components:\n"); appendIndentedComponentsList(message, scopedDependencies); reportComponentItem( compilerOptions.scopeCycleValidationType().diagnosticKind().get(), component, message.toString()); } } else { // Dagger 1.x scope compatibility requires this be suppress-able. if (!compilerOptions.scopeCycleValidationType().equals(ValidationType.NONE)) { validateDependencyScopeHierarchy( component, component.typeElement(), new ArrayDeque<>(), new ArrayDeque<>()); } } } else { // Scopeless components may not depend on scoped components. if (!scopedDependencies.isEmpty()) { StringBuilder message = new StringBuilder(component.typeElement().getQualifiedName()) .append(" (unscoped) cannot depend on scoped components:\n"); appendIndentedComponentsList(message, scopedDependencies); reportComponentError(component, message.toString()); } } } private void validateModules(ComponentDescriptor component) { for (ModuleDescriptor module : component.modules()) { if (module.moduleElement().isAbstract()) { for (ContributionBinding binding : module.bindings()) { if (binding.requiresModuleInstance()) { report(component).addError(abstractModuleHasInstanceBindingMethodsError(module)); break; } } } } } private String abstractModuleHasInstanceBindingMethodsError(ModuleDescriptor module) { String methodAnnotations; switch (module.kind()) { case MODULE: methodAnnotations = "@Provides"; break; case PRODUCER_MODULE: methodAnnotations = "@Provides or @Produces"; break; default: throw new AssertionError(module.kind()); } return String.format( "%s is abstract and has instance %s methods. Consider making the methods static or " + "including a non-abstract subclass of the module instead.", module.moduleElement(), methodAnnotations); } private void validateCreators(ComponentDescriptor component) { if (!component.creatorDescriptor().isPresent()) { // If no builder, nothing to validate. return; } ComponentCreatorDescriptor creator = component.creatorDescriptor().get(); ComponentCreatorMessages messages = ErrorMessages.creatorMessagesFor(creator.annotation()); // Requirements for modules and dependencies that the creator can set Set creatorModuleAndDependencyRequirements = creator.moduleAndDependencyRequirements(); // Modules and dependencies the component requires Set componentModuleAndDependencyRequirements = component.dependenciesAndConcreteModules(); // Requirements that the creator can set that don't match any requirements that the component // actually has. Set inapplicableRequirementsOnCreator = Sets.difference( creatorModuleAndDependencyRequirements, componentModuleAndDependencyRequirements); XType container = creator.typeElement().getType(); if (!inapplicableRequirementsOnCreator.isEmpty()) { Collection excessElements = Multimaps.filterKeys( creator.unvalidatedRequirementElements(), in(inapplicableRequirementsOnCreator)) .values(); String formatted = excessElements.stream() .map(element -> formatElement(element, container)) .collect(joining(", ", "[", "]")); report(component) .addError(String.format(messages.extraSetters(), formatted), creator.typeElement()); } // Component requirements that the creator must be able to set Set mustBePassed = Sets.filter( componentModuleAndDependencyRequirements, input -> input.nullPolicy().equals(NullPolicy.THROW)); // Component requirements that the creator must be able to set, but can't Set missingRequirements = Sets.difference(mustBePassed, creatorModuleAndDependencyRequirements); if (!missingRequirements.isEmpty()) { report(component) .addError( String.format( messages.missingSetters(), missingRequirements.stream() .map(ComponentRequirement::type) .map(XTypes::toStableString) .collect(toImmutableList())), creator.typeElement()); } // Validate that declared creator requirements (modules, dependencies) have unique types. ImmutableSetMultimap declaredRequirementsByType = Multimaps.filterKeys( creator.unvalidatedRequirementElements(), creatorModuleAndDependencyRequirements::contains) .entries() .stream() .collect( toImmutableSetMultimap( entry -> entry.getKey().type().getTypeName(), Entry::getValue)); declaredRequirementsByType .asMap() .forEach( (type, elementsForType) -> { if (elementsForType.size() > 1) { // TODO(cgdecker): Attach this error message to the factory method rather than // the component type if the elements are factory method parameters AND the // factory method is defined by the factory type itself and not by a supertype. report(component) .addError( String.format( messages.multipleSettersForModuleOrDependencyType(), type, transform( elementsForType, element -> formatElement(element, container))), creator.typeElement()); } }); // TODO(cgdecker): Duplicate binding validation should handle the case of multiple elements // that set the same bound-instance Key, but validating that here would make it fail faster // for subcomponents. } private String formatElement(XElement element, XType container) { // TODO(cgdecker): Extract some or all of this to another class? // But note that it does different formatting for parameters than // DaggerElements.elementToString(Element). if (isMethod(element)) { return methodSignatureFormatter.format(asMethod(element), Optional.of(container)); } else if (isMethodParameter(element)) { return formatParameter(asMethodParameter(element), container); } // This method shouldn't be called with any other type of element. throw new AssertionError(); } private String formatParameter(XExecutableParameterElement parameter, XType container) { // TODO(cgdecker): Possibly leave the type (and annotations?) off of the parameters here and // just use their names, since the type will be redundant in the context of the error message. StringJoiner joiner = new StringJoiner(" "); parameter.getAllAnnotations().stream() .map(XAnnotation::getQualifiedName) .forEach(joiner::add); XType parameterType = resolveParameterType(parameter, container); return joiner .add(stripCommonTypePrefixes(parameterType.getTypeName().toString())) .add(getSimpleName(parameter)) .toString(); } private XType resolveParameterType(XExecutableParameterElement parameter, XType container) { checkArgument(isMethod(parameter.getEnclosingElement())); XMethodElement method = asMethod(parameter.getEnclosingElement()); int parameterIndex = method.getParameters().indexOf(parameter); XMethodType methodType = method.asMemberOf(container); return methodType.getParameterTypes().get(parameterIndex); } /** * Validates that scopes do not participate in a scoping cycle - that is to say, scoped * components are in a hierarchical relationship terminating with Singleton. * *

As a side-effect, this means scoped components cannot have a dependency cycle between * themselves, since a component's presence within its own dependency path implies a cyclical * relationship between scopes. However, cycles in component dependencies are explicitly checked * in {@link #validateComponentDependencyHierarchy(ComponentDescriptor)}. */ private void validateDependencyScopeHierarchy( ComponentDescriptor component, XTypeElement dependency, Deque> scopeStack, Deque scopedDependencyStack) { ImmutableSet scopes = injectionAnnotations.getScopes(dependency); if (stackOverlaps(scopeStack, scopes)) { scopedDependencyStack.push(dependency); // Current scope has already appeared in the component chain. StringBuilder message = new StringBuilder(); message.append(component.typeElement().getQualifiedName()); message.append(" depends on scoped components in a non-hierarchical scope ordering:\n"); appendIndentedComponentsList(message, scopedDependencyStack); if (compilerOptions.scopeCycleValidationType().diagnosticKind().isPresent()) { reportComponentItem( compilerOptions.scopeCycleValidationType().diagnosticKind().get(), component, message.toString()); } scopedDependencyStack.pop(); } else if (compilerOptions.validateTransitiveComponentDependencies() // Always validate direct component dependencies referenced by this component regardless // of the flag value || scopedDependencyStack.isEmpty()) { // TODO(beder): transitively check scopes of production components too. rootComponentAnnotation(dependency, superficialValidation) .filter(componentAnnotation -> !componentAnnotation.isProduction()) .ifPresent( componentAnnotation -> { ImmutableSet scopedDependencies = scopedTypesIn(componentAnnotation.dependencies()); if (!scopedDependencies.isEmpty()) { // empty can be ignored (base-case) scopeStack.push(scopes); scopedDependencyStack.push(dependency); for (XTypeElement scopedDependency : scopedDependencies) { validateDependencyScopeHierarchy( component, scopedDependency, scopeStack, scopedDependencyStack); } scopedDependencyStack.pop(); scopeStack.pop(); } }); // else: we skip component dependencies which are not components } } private boolean stackOverlaps(Deque> stack, ImmutableSet set) { for (ImmutableSet entry : stack) { if (!Sets.intersection(entry, set).isEmpty()) { return true; } } return false; } /** Appends and formats a list of indented component types (with their scope annotations). */ private void appendIndentedComponentsList(StringBuilder message, Iterable types) { for (XTypeElement scopedComponent : types) { message.append(INDENT); for (Scope scope : injectionAnnotations.getScopes(scopedComponent)) { message.append(getReadableSource(scope)).append(' '); } message .append(stripCommonTypePrefixes(scopedComponent.getQualifiedName().toString())) .append('\n'); } } /** * Returns a set of type elements containing only those found in the input set that have a * scoping annotation. */ private ImmutableSet scopedTypesIn(Collection types) { return types.stream() .filter(type -> !injectionAnnotations.getScopes(type).isEmpty()) .collect(toImmutableSet()); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy