dagger.internal.codegen.validation.ComponentHierarchyValidator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dagger-compiler Show documentation
Show all versions of dagger-compiler Show documentation
A fast dependency injector for Android and Java.
/*
* 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 static dagger.internal.codegen.xprocessing.XElements.getSimpleName;
import androidx.room.compiler.processing.XExecutableParameterElement;
import androidx.room.compiler.processing.XTypeElement;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
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.spi.model.Scope;
import java.util.Collection;
import java.util.Formatter;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
/** 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 (XExecutableParameterElement factoryMethodParameter :
subcomponentMethodDescriptor.methodElement().getParameters()) {
XTypeElement moduleType = factoryMethodParameter.getType().getTypeElement();
if (existingModuleToOwners.containsKey(moduleType)) {
/* 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.",
getSimpleName(moduleType),
existingModuleToOwners.get(moduleType).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 module.allBindingDeclarations().stream()
.map(declaration -> uniqueScopeOf(declaration.bindingElement().get()))
.filter(scope -> scope.isPresent() && !scope.get().isReusable())
.map(Optional::get)
.collect(toImmutableSet());
}
}