blob: aa20232b95aff1f48fb44ef9eaff7e4ff427b367 [file] [log] [blame]
/*
* 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.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.
*
* <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 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<TypeElement> 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<TypeElement>> reports =
new LinkedHashMap<>();
ComponentValidation(ComponentDescriptor rootComponent) {
this.rootComponent = checkNotNull(rootComponent);
}
/** Returns a report that contains all validation messages found during traversal. */
ValidationReport<TypeElement> buildReport() {
ValidationReport.Builder<TypeElement> 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<TypeElement> 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<TypeElement> 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<Scope> scopes = component.scopes();
ImmutableSet<TypeElement> 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<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);
DeclaredType container = asDeclared(creator.typeElement().asType());
if (!inapplicableRequirementsOnCreator.isEmpty()) {
Collection<Element> 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(elements, types, metadataUtil).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<Wrapper<TypeMirror>, 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.
*
* <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,
TypeElement dependency,
Deque<ImmutableSet<Scope>> scopeStack,
Deque<TypeElement> scopedDependencyStack) {
ImmutableSet<Scope> 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<TypeElement> 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 <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<TypeElement> 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<TypeElement> scopedTypesIn(Collection<TypeElement> types) {
return types.stream().filter(type -> !scopesOf(type).isEmpty()).collect(toImmutableSet());
}
}
}