| /* |
| * 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.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 java.util.stream.Collectors.toList; |
| 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.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.tools.Diagnostic; |
| |
| /** |
| * Reports errors in the component hierarchy. |
| * |
| * <ul> |
| * <li>Validates scope hierarchy of component dependencies and subcomponents. |
| * <li>Reports errors if there are component dependency cycles. |
| * <li>Reports errors if any abstract modules have non-abstract instance binding methods. |
| * <li>Validates component creator types. |
| * </ul> |
| */ |
| // 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<ComponentDescriptor, ValidationReport.Builder> 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<XTypeElement> 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<ClassName> scopes = |
| component.scopes().stream().map(Scope::className).collect(toImmutableSet()); |
| ImmutableSet<XTypeElement> 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<ComponentRequirement> creatorModuleAndDependencyRequirements = |
| creator.moduleAndDependencyRequirements(); |
| // Modules and dependencies the component requires |
| Set<ComponentRequirement> componentModuleAndDependencyRequirements = |
| component.dependenciesAndConcreteModules(); |
| |
| // Requirements that the creator can set that don't match any requirements that the component |
| // actually has. |
| Set<ComponentRequirement> inapplicableRequirementsOnCreator = |
| Sets.difference( |
| creatorModuleAndDependencyRequirements, componentModuleAndDependencyRequirements); |
| |
| XType container = creator.typeElement().getType(); |
| if (!inapplicableRequirementsOnCreator.isEmpty()) { |
| Collection<XElement> 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<ComponentRequirement> mustBePassed = |
| Sets.filter( |
| componentModuleAndDependencyRequirements, |
| input -> input.nullPolicy().equals(NullPolicy.THROW)); |
| // Component requirements that the creator must be able to set, but can't |
| Set<ComponentRequirement> 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<TypeName, XElement> 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.getEnclosingMethodElement())); |
| XMethodElement method = asMethod(parameter.getEnclosingMethodElement()); |
| 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. |
| * |
| * <p>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<ImmutableSet<Scope>> scopeStack, |
| Deque<XTypeElement> scopedDependencyStack) { |
| ImmutableSet<Scope> 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<XTypeElement> 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 <T> boolean stackOverlaps(Deque<ImmutableSet<T>> stack, ImmutableSet<T> set) { |
| for (ImmutableSet<T> 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<XTypeElement> 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<XTypeElement> scopedTypesIn(Collection<XTypeElement> types) { |
| return types.stream() |
| .filter(type -> !injectionAnnotations.getScopes(type).isEmpty()) |
| .collect(toImmutableSet()); |
| } |
| } |
| } |