| /* |
| * 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<TypeElement> validate(ComponentDescriptor componentDescriptor) { |
| ValidationReport.Builder<TypeElement> 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.<ComponentDescriptor, Scope>create()); |
| } |
| validateProductionModuleUniqueness(report, componentDescriptor, LinkedHashMultimap.create()); |
| return report.build(); |
| } |
| |
| private void validateSubcomponentMethods( |
| ValidationReport.Builder<?> report, |
| ComponentDescriptor componentDescriptor, |
| ImmutableMap<TypeElement, TypeElement> 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<TypeElement, TypeElement>() |
| .putAll(existingModuleToOwners) |
| .putAll( |
| Maps.toMap( |
| Sets.difference( |
| childComponent.moduleTypes(), existingModuleToOwners.keySet()), |
| constant(childComponent.typeElement()))) |
| .build()); |
| }); |
| } |
| |
| private void validateFactoryMethodParameters( |
| ValidationReport.Builder<?> report, |
| ComponentMethodDescriptor subcomponentMethodDescriptor, |
| ImmutableMap<TypeElement, TypeElement> 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<TypeElement> report, |
| ComponentDescriptor subject, |
| SetMultimap<ComponentDescriptor, Scope> scopesByComponent) { |
| scopesByComponent.putAll(subject, subject.scopes()); |
| |
| for (ComponentDescriptor childComponent : subject.childComponents()) { |
| validateScopeHierarchy(report, childComponent, scopesByComponent); |
| } |
| |
| scopesByComponent.removeAll(subject); |
| |
| Predicate<Scope> 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<ComponentDescriptor, Scope> overlappingScopes = |
| Multimaps.filterValues(scopesByComponent, subjectScopes); |
| if (!overlappingScopes.isEmpty()) { |
| StringBuilder error = |
| new StringBuilder() |
| .append(subject.typeElement().getQualifiedName()) |
| .append(" has conflicting scopes:"); |
| for (Map.Entry<ComponentDescriptor, Scope> 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<TypeElement> report, |
| ComponentDescriptor componentDescriptor, |
| SetMultimap<ComponentDescriptor, ModuleDescriptor> producerModulesByComponent) { |
| ImmutableSet<ModuleDescriptor> 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<ComponentDescriptor, ModuleDescriptor> 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<ComponentDescriptor, Collection<ModuleDescriptor>> 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<TypeElement> report, |
| ComponentDescriptor component, |
| // TODO(ronshapiro): optimize ModuleDescriptor.hashCode()/equals. Otherwise this could be |
| // quite costly |
| SetMultimap<ComponentDescriptor, ModuleDescriptor> modulesWithScopes) { |
| ImmutableSet<ModuleDescriptor> 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<ComponentDescriptor, ModuleDescriptor> 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<ComponentDescriptor, ModuleDescriptor> 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<Scope> 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(); |
| } |
| } |