/* * 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.common.base.Functions.constant; import static com.google.common.base.Predicates.and; import static com.google.common.base.Predicates.in; import static com.google.common.base.Predicates.not; import static dagger.internal.codegen.base.Scopes.getReadableSource; import static dagger.internal.codegen.base.Scopes.uniqueScopeOf; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; import com.google.auto.common.MoreTypes; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Maps; import com.google.common.collect.Multimaps; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; import dagger.internal.codegen.binding.ComponentDescriptor; import dagger.internal.codegen.binding.ComponentDescriptor.ComponentMethodDescriptor; import dagger.internal.codegen.binding.ModuleDescriptor; import dagger.internal.codegen.binding.ModuleKind; import dagger.internal.codegen.compileroption.CompilerOptions; import dagger.model.Scope; import java.util.Collection; import java.util.Formatter; import java.util.Map; import javax.inject.Inject; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; /** Validates the relationships between parent components and subcomponents. */ final class ComponentHierarchyValidator { private static final Joiner COMMA_SEPARATED_JOINER = Joiner.on(", "); private final CompilerOptions compilerOptions; @Inject ComponentHierarchyValidator(CompilerOptions compilerOptions) { this.compilerOptions = compilerOptions; } ValidationReport validate(ComponentDescriptor componentDescriptor) { ValidationReport.Builder report = ValidationReport.about(componentDescriptor.typeElement()); validateSubcomponentMethods( report, componentDescriptor, Maps.toMap(componentDescriptor.moduleTypes(), constant(componentDescriptor.typeElement()))); validateRepeatedScopedDeclarations(report, componentDescriptor, LinkedHashMultimap.create()); if (compilerOptions.scopeCycleValidationType().diagnosticKind().isPresent()) { validateScopeHierarchy( report, componentDescriptor, LinkedHashMultimap.create()); } validateProductionModuleUniqueness(report, componentDescriptor, LinkedHashMultimap.create()); return report.build(); } private void validateSubcomponentMethods( ValidationReport.Builder report, ComponentDescriptor componentDescriptor, ImmutableMap existingModuleToOwners) { componentDescriptor .childComponentsDeclaredByFactoryMethods() .forEach( (method, childComponent) -> { if (childComponent.hasCreator()) { report.addError( "Components may not have factory methods for subcomponents that define a " + "builder.", method.methodElement()); } else { validateFactoryMethodParameters(report, method, existingModuleToOwners); } validateSubcomponentMethods( report, childComponent, new ImmutableMap.Builder() .putAll(existingModuleToOwners) .putAll( Maps.toMap( Sets.difference( childComponent.moduleTypes(), existingModuleToOwners.keySet()), constant(childComponent.typeElement()))) .build()); }); } private void validateFactoryMethodParameters( ValidationReport.Builder report, ComponentMethodDescriptor subcomponentMethodDescriptor, ImmutableMap existingModuleToOwners) { for (VariableElement factoryMethodParameter : subcomponentMethodDescriptor.methodElement().getParameters()) { TypeElement moduleType = MoreTypes.asTypeElement(factoryMethodParameter.asType()); TypeElement originatingComponent = existingModuleToOwners.get(moduleType); if (originatingComponent != null) { /* Factory method tries to pass a module that is already present in the parent. * This is an error. */ report.addError( String.format( "%s is present in %s. A subcomponent cannot use an instance of a " + "module that differs from its parent.", moduleType.getSimpleName(), originatingComponent.getQualifiedName()), factoryMethodParameter); } } } /** * Checks that components do not have any scopes that are also applied on any of their ancestors. */ private void validateScopeHierarchy( ValidationReport.Builder report, ComponentDescriptor subject, SetMultimap scopesByComponent) { scopesByComponent.putAll(subject, subject.scopes()); for (ComponentDescriptor childComponent : subject.childComponents()) { validateScopeHierarchy(report, childComponent, scopesByComponent); } scopesByComponent.removeAll(subject); Predicate subjectScopes = subject.isProduction() // TODO(beder): validate that @ProductionScope is only applied on production components ? and(in(subject.scopes()), not(Scope::isProductionScope)) : in(subject.scopes()); SetMultimap overlappingScopes = Multimaps.filterValues(scopesByComponent, subjectScopes); if (!overlappingScopes.isEmpty()) { StringBuilder error = new StringBuilder() .append(subject.typeElement().getQualifiedName()) .append(" has conflicting scopes:"); for (Map.Entry entry : overlappingScopes.entries()) { Scope scope = entry.getValue(); error .append("\n ") .append(entry.getKey().typeElement().getQualifiedName()) .append(" also has ") .append(getReadableSource(scope)); } report.addItem( error.toString(), compilerOptions.scopeCycleValidationType().diagnosticKind().get(), subject.typeElement()); } } private void validateProductionModuleUniqueness( ValidationReport.Builder report, ComponentDescriptor componentDescriptor, SetMultimap producerModulesByComponent) { ImmutableSet producerModules = componentDescriptor.modules().stream() .filter(module -> module.kind().equals(ModuleKind.PRODUCER_MODULE)) .collect(toImmutableSet()); producerModulesByComponent.putAll(componentDescriptor, producerModules); for (ComponentDescriptor childComponent : componentDescriptor.childComponents()) { validateProductionModuleUniqueness(report, childComponent, producerModulesByComponent); } producerModulesByComponent.removeAll(componentDescriptor); SetMultimap repeatedModules = Multimaps.filterValues(producerModulesByComponent, producerModules::contains); if (repeatedModules.isEmpty()) { return; } StringBuilder error = new StringBuilder(); Formatter formatter = new Formatter(error); formatter.format("%s repeats @ProducerModules:", componentDescriptor.typeElement()); for (Map.Entry> entry : repeatedModules.asMap().entrySet()) { formatter.format("\n %s also installs: ", entry.getKey().typeElement()); COMMA_SEPARATED_JOINER .appendTo(error, Iterables.transform(entry.getValue(), m -> m.moduleElement())); } report.addError(error.toString()); } private void validateRepeatedScopedDeclarations( ValidationReport.Builder report, ComponentDescriptor component, // TODO(ronshapiro): optimize ModuleDescriptor.hashCode()/equals. Otherwise this could be // quite costly SetMultimap modulesWithScopes) { ImmutableSet modules = component.modules().stream().filter(this::hasScopedDeclarations).collect(toImmutableSet()); modulesWithScopes.putAll(component, modules); for (ComponentDescriptor childComponent : component.childComponents()) { validateRepeatedScopedDeclarations(report, childComponent, modulesWithScopes); } modulesWithScopes.removeAll(component); SetMultimap repeatedModules = Multimaps.filterValues(modulesWithScopes, modules::contains); if (repeatedModules.isEmpty()) { return; } report.addError( repeatedModulesWithScopeError(component, ImmutableSetMultimap.copyOf(repeatedModules))); } private boolean hasScopedDeclarations(ModuleDescriptor module) { return !moduleScopes(module).isEmpty(); } private String repeatedModulesWithScopeError( ComponentDescriptor component, ImmutableSetMultimap repeatedModules) { StringBuilder error = new StringBuilder() .append(component.typeElement().getQualifiedName()) .append(" repeats modules with scoped bindings or declarations:"); repeatedModules .asMap() .forEach( (conflictingComponent, conflictingModules) -> { error .append("\n - ") .append(conflictingComponent.typeElement().getQualifiedName()) .append(" also includes:"); for (ModuleDescriptor conflictingModule : conflictingModules) { error .append("\n - ") .append(conflictingModule.moduleElement().getQualifiedName()) .append(" with scopes: ") .append(COMMA_SEPARATED_JOINER.join(moduleScopes(conflictingModule))); } }); return error.toString(); } private ImmutableSet moduleScopes(ModuleDescriptor module) { return FluentIterable.concat(module.allBindingDeclarations()) .transform(declaration -> uniqueScopeOf(declaration.bindingElement().get())) .filter(scope -> scope.isPresent() && !scope.get().isReusable()) .transform(scope -> scope.get()) .toSet(); } }