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.54
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 com.google.auto.common.MoreTypes.asDeclared;
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.Scopes.scopesOf;
import static dagger.internal.codegen.base.Scopes.singletonScope;
import static dagger.internal.codegen.base.Util.reentrantComputeIfAbsent;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSetMultimap;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static javax.tools.Diagnostic.Kind.ERROR;

import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.common.base.Equivalence.Wrapper;
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 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.MethodSignatureFormatter;
import dagger.internal.codegen.binding.ModuleDescriptor;
import dagger.internal.codegen.compileroption.CompilerOptions;
import dagger.internal.codegen.compileroption.ValidationType;
import dagger.internal.codegen.kotlin.KotlinMetadataUtil;
import dagger.internal.codegen.langmodel.DaggerElements;
import dagger.internal.codegen.langmodel.DaggerTypes;
import dagger.spi.model.Scope;
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.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.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeMirror;
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 DaggerElements elements; private final DaggerTypes types; private final CompilerOptions compilerOptions; private final MethodSignatureFormatter methodSignatureFormatter; private final ComponentHierarchyValidator componentHierarchyValidator; private final KotlinMetadataUtil metadataUtil; @Inject ComponentDescriptorValidator( DaggerElements elements, DaggerTypes types, CompilerOptions compilerOptions, MethodSignatureFormatter methodSignatureFormatter, ComponentHierarchyValidator componentHierarchyValidator, KotlinMetadataUtil metadataUtil) { this.elements = elements; this.types = types; this.compilerOptions = compilerOptions; this.methodSignatureFormatter = methodSignatureFormatter; this.componentHierarchyValidator = componentHierarchyValidator; this.metadataUtil = metadataUtil; } 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, TypeElement 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) .ifPresent( componentAnnotation -> { dependencyStack.push(dependency); for (TypeElement 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(); ImmutableSet scopedDependencies = scopedTypesIn( component .dependencies() .stream() .map(ComponentRequirement::typeElement) .collect(toImmutableSet())); if (!scopes.isEmpty()) { Scope singletonScope = singletonScope(elements); // Dagger 1.x scope compatibility requires this be suppress-able. if (compilerOptions.scopeCycleValidationType().diagnosticKind().isPresent() && scopes.contains(singletonScope)) { // 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().getModifiers().contains(Modifier.ABSTRACT)) { 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); DeclaredType container = asDeclared(creator.typeElement().asType()); 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(elements, metadataUtil).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).collect(toList())), creator.typeElement()); } // Validate that declared creator requirements (modules, dependencies) have unique types. ImmutableSetMultimap, Element> declaredRequirementsByType = Multimaps.filterKeys( creator.unvalidatedRequirementElements(), creatorModuleAndDependencyRequirements::contains) .entries().stream() .collect( toImmutableSetMultimap(entry -> entry.getKey().wrappedType(), Entry::getValue)); declaredRequirementsByType .asMap() .forEach( (typeWrapper, elementsForType) -> { if (elementsForType.size() > 1) { TypeMirror type = typeWrapper.get(); // 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(Element element, DeclaredType 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). switch (element.getKind()) { case METHOD: return methodSignatureFormatter.format( MoreElements.asExecutable(element), Optional.of(container)); case PARAMETER: return formatParameter(MoreElements.asVariable(element), container); default: // This method shouldn't be called with any other type of element. throw new AssertionError(); } } private String formatParameter(VariableElement parameter, DeclaredType 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.getAnnotationMirrors().stream().map(Object::toString).forEach(joiner::add); TypeMirror parameterType = resolveParameterType(parameter, container); return joiner .add(stripCommonTypePrefixes(parameterType.toString())) .add(parameter.getSimpleName()) .toString(); } private TypeMirror resolveParameterType(VariableElement parameter, DeclaredType container) { ExecutableElement method = MoreElements.asExecutable(parameter.getEnclosingElement()); int parameterIndex = method.getParameters().indexOf(parameter); ExecutableType methodType = MoreTypes.asExecutable(types.asMemberOf(container, method)); 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, TypeElement dependency, Deque> scopeStack, Deque scopedDependencyStack) { ImmutableSet scopes = scopesOf(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) .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 (TypeElement 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 (TypeElement scopedComponent : types) { message.append(INDENT); for (Scope scope : scopesOf(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 -> !scopesOf(type).isEmpty()).collect(toImmutableSet()); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy