dagger2.internal.codegen.BindingGraphValidator.java Source code

Java tutorial

Introduction

Here is the source code for dagger2.internal.codegen.BindingGraphValidator.java

Source

/*
 * Copyright (C) 2015 Google, Inc.
 *
 * 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 dagger2.internal.codegen;

import com.google.auto.common.AnnotationMirrors;
import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.auto.value.AutoValue;
import com.google.common.base.Equivalence;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import dagger2.Component;
import dagger2.internal.codegen.ComponentDescriptor.BuilderSpec;
import dagger2.internal.codegen.ComponentDescriptor.ComponentMethodDescriptor;
import dagger2.internal.codegen.ContributionBinding.BindingType;
import dagger2.internal.codegen.ValidationReport.Builder;
import dagger2.internal.codegen.writer.TypeNames;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.Formatter;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.inject.Singleton;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.SimpleTypeVisitor6;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;

import static com.google.auto.common.MoreElements.getAnnotationMirror;
import static com.google.auto.common.MoreTypes.isTypeOf;
import static com.google.common.base.Verify.verify;
import static com.google.common.collect.Iterables.getOnlyElement;
import static dagger2.internal.codegen.ConfigurationAnnotations.getComponentDependencies;
import static dagger2.internal.codegen.ErrorMessages.INDENT;
import static dagger2.internal.codegen.ErrorMessages.MEMBERS_INJECTION_WITH_UNBOUNDED_TYPE;
import static dagger2.internal.codegen.ErrorMessages.NULLABLE_TO_NON_NULLABLE;
import static dagger2.internal.codegen.ErrorMessages.REQUIRES_AT_INJECT_CONSTRUCTOR_OR_PROVIDER_FORMAT;
import static dagger2.internal.codegen.ErrorMessages.REQUIRES_AT_INJECT_CONSTRUCTOR_OR_PROVIDER_OR_PRODUCER_FORMAT;
import static dagger2.internal.codegen.ErrorMessages.REQUIRES_PROVIDER_FORMAT;
import static dagger2.internal.codegen.ErrorMessages.REQUIRES_PROVIDER_OR_PRODUCER_FORMAT;
import static dagger2.internal.codegen.ErrorMessages.stripCommonTypePrefixes;
import static dagger2.internal.codegen.InjectionAnnotations.getScopeAnnotation;

public class BindingGraphValidator implements Validator<BindingGraph> {

    private final Types types;
    private final InjectBindingRegistry injectBindingRegistry;
    private final ValidationType scopeCycleValidationType;
    private final Diagnostic.Kind nullableValidationType;
    private final ProvisionBindingFormatter provisionBindingFormatter;
    private final ProductionBindingFormatter productionBindingFormatter;
    private final MethodSignatureFormatter methodSignatureFormatter;
    private final DependencyRequestFormatter dependencyRequestFormatter;
    private final KeyFormatter keyFormatter;

    BindingGraphValidator(Types types, InjectBindingRegistry injectBindingRegistry,
            ValidationType scopeCycleValidationType, Diagnostic.Kind nullableValidationType,
            ProvisionBindingFormatter provisionBindingFormatter,
            ProductionBindingFormatter productionBindingFormatter,
            MethodSignatureFormatter methodSignatureFormatter,
            DependencyRequestFormatter dependencyRequestFormatter, KeyFormatter keyFormatter) {
        this.types = types;
        this.injectBindingRegistry = injectBindingRegistry;
        this.scopeCycleValidationType = scopeCycleValidationType;
        this.nullableValidationType = nullableValidationType;
        this.provisionBindingFormatter = provisionBindingFormatter;
        this.productionBindingFormatter = productionBindingFormatter;
        this.methodSignatureFormatter = methodSignatureFormatter;
        this.dependencyRequestFormatter = dependencyRequestFormatter;
        this.keyFormatter = keyFormatter;
    }

    @Override
    public ValidationReport<BindingGraph> validate(final BindingGraph subject) {
        final ValidationReport.Builder<BindingGraph> reportBuilder = ValidationReport.Builder.about(subject);
        return validate(subject, reportBuilder);
    }

    private ValidationReport<BindingGraph> validate(final BindingGraph subject,
            final ValidationReport.Builder<BindingGraph> reportBuilder) {
        ImmutableMap<BindingKey, ResolvedBindings> resolvedBindings = subject.resolvedBindings();

        validateComponentScope(subject, reportBuilder, resolvedBindings);
        validateDependencyScopes(subject, reportBuilder);
        validateBuilders(subject, reportBuilder);

        for (ComponentMethodDescriptor componentMethod : subject.componentDescriptor().componentMethods()) {
            Optional<DependencyRequest> entryPoint = componentMethod.dependencyRequest();
            if (entryPoint.isPresent()) {
                traverseRequest(entryPoint.get(), new ArrayDeque<ResolvedRequest>(), Sets.<BindingKey>newHashSet(),
                        subject, reportBuilder, Sets.<DependencyRequest>newHashSet());
            }
        }

        validateSubcomponents(subject, reportBuilder);
        return reportBuilder.build();
    }

    private void traverseRequest(DependencyRequest request, Deque<ResolvedRequest> bindingPath,
            Set<BindingKey> keysInPath, BindingGraph graph, ValidationReport.Builder<BindingGraph> reportBuilder,
            Set<DependencyRequest> resolvedRequests) {
        verify(bindingPath.size() == keysInPath.size(), "mismatched path vs keys -- (%s vs %s)", bindingPath,
                keysInPath);
        BindingKey requestKey = request.bindingKey();
        if (keysInPath.contains(requestKey)) {
            reportCycle(request, bindingPath, reportBuilder);
            return;
        }

        // If request has already been resolved, avoid re-traversing the binding path.
        if (resolvedRequests.add(request)) {
            ResolvedRequest resolvedRequest = ResolvedRequest.create(request, graph);
            bindingPath.push(resolvedRequest);
            keysInPath.add(requestKey);
            validateResolvedBinding(bindingPath, resolvedRequest.binding(), reportBuilder);

            for (Binding binding : resolvedRequest.binding().bindings()) {
                for (DependencyRequest nextRequest : binding.implicitDependencies()) {
                    traverseRequest(nextRequest, bindingPath, keysInPath, graph, reportBuilder, resolvedRequests);
                }
            }
            bindingPath.poll();
            keysInPath.remove(requestKey);
        }
    }

    private void validateSubcomponents(BindingGraph graph, ValidationReport.Builder<BindingGraph> reportBuilder) {
        for (Entry<ExecutableElement, BindingGraph> subgraphEntry : graph.subgraphs().entrySet()) {
            validate(subgraphEntry.getValue(), reportBuilder);
        }
    }

    /**
     * Validates that the set of bindings resolved is consistent with the type of the binding, and
     * returns true if the bindings are valid.
     */
    private boolean validateResolvedBinding(Deque<ResolvedRequest> path, ResolvedBindings resolvedBinding,
            Builder<BindingGraph> reportBuilder) {
        if (resolvedBinding.bindings().isEmpty()) {
            reportMissingBinding(path, reportBuilder);
            return false;
        }

        ImmutableSet.Builder<ProvisionBinding> provisionBindingsBuilder = ImmutableSet.builder();
        ImmutableSet.Builder<ProductionBinding> productionBindingsBuilder = ImmutableSet.builder();
        ImmutableSet.Builder<MembersInjectionBinding> membersInjectionBindingsBuilder = ImmutableSet.builder();
        for (Binding binding : resolvedBinding.bindings()) {
            if (binding instanceof ProvisionBinding) {
                provisionBindingsBuilder.add((ProvisionBinding) binding);
            }
            if (binding instanceof ProductionBinding) {
                productionBindingsBuilder.add((ProductionBinding) binding);
            }
            if (binding instanceof MembersInjectionBinding) {
                membersInjectionBindingsBuilder.add((MembersInjectionBinding) binding);
            }
        }
        ImmutableSet<ProvisionBinding> provisionBindings = provisionBindingsBuilder.build();
        ImmutableSet<ProductionBinding> productionBindings = productionBindingsBuilder.build();
        ImmutableSet<MembersInjectionBinding> membersInjectionBindings = membersInjectionBindingsBuilder.build();

        switch (resolvedBinding.bindingKey().kind()) {
        case CONTRIBUTION:
            if (!membersInjectionBindings.isEmpty()) {
                throw new IllegalArgumentException(
                        "contribution binding keys should never have members injection bindings");
            }
            Set<ContributionBinding> combined = Sets.union(provisionBindings, productionBindings);
            if (!validateNullability(path.peek().request(), combined, reportBuilder)) {
                return false;
            }
            if (!productionBindings.isEmpty() && doesPathRequireProvisionOnly(path)) {
                reportProviderMayNotDependOnProducer(path, reportBuilder);
                return false;
            }
            if ((provisionBindings.size() + productionBindings.size()) <= 1) {
                return true;
            }
            ImmutableListMultimap<BindingType, ContributionBinding> bindingsByType = ContributionBinding
                    .bindingTypesFor(Iterables.<ContributionBinding>concat(provisionBindings, productionBindings));
            if (bindingsByType.keySet().size() > 1) {
                reportMultipleBindingTypes(path, reportBuilder);
                return false;
            } else if (getOnlyElement(bindingsByType.keySet()).equals(BindingType.UNIQUE)) {
                reportDuplicateBindings(path, reportBuilder);
                return false;
            }
            break;
        case MEMBERS_INJECTION:
            if (!provisionBindings.isEmpty() || !productionBindings.isEmpty()) {
                throw new IllegalArgumentException(
                        "members injection binding keys should never have contribution bindings");
            }
            if (membersInjectionBindings.size() > 1) {
                reportDuplicateBindings(path, reportBuilder);
                return false;
            }
            if (membersInjectionBindings.size() == 1) {
                MembersInjectionBinding binding = getOnlyElement(membersInjectionBindings);
                if (!validateMembersInjectionBinding(binding, path, reportBuilder)) {
                    return false;
                }
            }
            break;
        default:
            throw new AssertionError();
        }
        return true;
    }

    /** Ensures that if the request isn't nullable, then each contribution is also not nullable. */
    private boolean validateNullability(DependencyRequest request, Set<ContributionBinding> bindings,
            Builder<BindingGraph> reportBuilder) {
        boolean valid = true;
        if (!request.isNullable()) {
            String typeName = null;
            for (ContributionBinding binding : bindings) {
                if (binding.nullableType().isPresent()) {
                    String methodSignature;
                    if (binding instanceof ProvisionBinding) {
                        ProvisionBinding provisionBinding = (ProvisionBinding) binding;
                        methodSignature = provisionBindingFormatter.format(provisionBinding);
                    } else {
                        ProductionBinding productionBinding = (ProductionBinding) binding;
                        methodSignature = productionBindingFormatter.format(productionBinding);
                    }
                    // Note: the method signature will include the @Nullable in it!
                    // TODO(sameb): Sometimes javac doesn't include the Element in its output.
                    // (Maybe this happens if the code was already compiled before this point?)
                    // ... we manually print ouf the request in that case, otherwise the error
                    // message is kind of useless.
                    if (typeName == null) {
                        typeName = TypeNames.forTypeMirror(request.key().type()).toString();
                    }
                    reportBuilder.addItem(
                            String.format(NULLABLE_TO_NON_NULLABLE, typeName, methodSignature) + "\n at: "
                                    + dependencyRequestFormatter.format(request),
                            nullableValidationType, request.requestElement());
                    valid = false;
                }
            }
        }
        return valid;
    }

    /**
     * Validates a members injection binding, returning false (and reporting the error) if it wasn't
     * valid.
     */
    private boolean validateMembersInjectionBinding(MembersInjectionBinding binding,
            final Deque<ResolvedRequest> path, final Builder<BindingGraph> reportBuilder) {
        return binding.key().type().accept(new SimpleTypeVisitor6<Boolean, Void>() {
            @Override
            protected Boolean defaultAction(TypeMirror e, Void p) {
                reportBuilder.addItem("Invalid members injection request.", path.peek().request().requestElement());
                return false;
            }

            @Override
            public Boolean visitDeclared(DeclaredType type, Void ignored) {
                // If the key has type arguments, validate that each type argument is declared.
                // Otherwise the type argument may be a wildcard (or other type), and we can't
                // resolve that to actual types.  If the arg was an array, validate the type
                // of the array.
                for (TypeMirror arg : type.getTypeArguments()) {
                    boolean declared;
                    switch (arg.getKind()) {
                    case ARRAY:
                        declared = MoreTypes.asArray(arg).getComponentType()
                                .accept(new SimpleTypeVisitor6<Boolean, Void>() {
                                    @Override
                                    protected Boolean defaultAction(TypeMirror e, Void p) {
                                        return false;
                                    }

                                    @Override
                                    public Boolean visitDeclared(DeclaredType t, Void p) {
                                        for (TypeMirror arg : t.getTypeArguments()) {
                                            if (!arg.accept(this, null)) {
                                                return false;
                                            }
                                        }
                                        return true;
                                    }

                                    @Override
                                    public Boolean visitArray(ArrayType t, Void p) {
                                        return t.getComponentType().accept(this, null);
                                    }

                                    @Override
                                    public Boolean visitPrimitive(PrimitiveType t, Void p) {
                                        return true;
                                    }
                                }, null);
                        break;
                    case DECLARED:
                        declared = true;
                        break;
                    default:
                        declared = false;
                    }
                    if (!declared) {
                        ImmutableList<String> printableDependencyPath = FluentIterable.from(path)
                                .transform(REQUEST_FROM_RESOLVED_REQUEST).transform(dependencyRequestFormatter)
                                .filter(Predicates.not(Predicates.equalTo(""))).toList().reverse();
                        reportBuilder.addItem(
                                String.format(MEMBERS_INJECTION_WITH_UNBOUNDED_TYPE, arg.toString(),
                                        type.toString(), Joiner.on('\n').join(printableDependencyPath)),
                                path.peek().request().requestElement());
                        return false;
                    }
                }

                TypeElement element = MoreElements.asType(type.asElement());
                // Also validate that the key is not the erasure of a generic type.
                // If it is, that means the user referred to Foo<T> as just 'Foo',
                // which we don't allow.  (This is a judgement call -- we *could*
                // allow it and instantiate the type bounds... but we don't.)
                if (!MoreTypes.asDeclared(element.asType()).getTypeArguments().isEmpty()
                        && types.isSameType(types.erasure(element.asType()), type)) {
                    ImmutableList<String> printableDependencyPath = FluentIterable.from(path)
                            .transform(REQUEST_FROM_RESOLVED_REQUEST).transform(dependencyRequestFormatter)
                            .filter(Predicates.not(Predicates.equalTo(""))).toList().reverse();
                    reportBuilder.addItem(
                            String.format(ErrorMessages.MEMBERS_INJECTION_WITH_RAW_TYPE, type.toString(),
                                    Joiner.on('\n').join(printableDependencyPath)),
                            path.peek().request().requestElement());
                    return false;
                }

                return true; // valid
            }
        }, null);
    }

    /**
     * Validates that among the dependencies are at most one scoped dependency,
     * that there are no cycles within the scoping chain, and that singleton
     * components have no scoped dependencies.
     */
    private void validateDependencyScopes(BindingGraph subject, Builder<BindingGraph> reportBuilder) {
        ComponentDescriptor descriptor = subject.componentDescriptor();
        Optional<AnnotationMirror> scope = subject.componentDescriptor().scope();
        ImmutableSet<TypeElement> scopedDependencies = scopedTypesIn(descriptor.dependencies());
        if (scope.isPresent()) {
            // Dagger 1.x scope compatibility requires this be suppress-able.
            if (scopeCycleValidationType.diagnosticKind().isPresent()
                    && isTypeOf(Singleton.class, scope.get().getAnnotationType())) {
                // 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);
                    reportBuilder.addItem(message.toString(), scopeCycleValidationType.diagnosticKind().get(),
                            descriptor.componentDefinitionType(), descriptor.componentAnnotation());
                }
            } else if (scopedDependencies.size() > 1) {
                // Scoped components may depend on at most one scoped component.
                StringBuilder message = new StringBuilder(ErrorMessages.format(scope.get())).append(' ')
                        .append(descriptor.componentDefinitionType().getQualifiedName())
                        .append(" depends on more than one scoped component:\n");
                appendIndentedComponentsList(message, scopedDependencies);
                reportBuilder.addItem(message.toString(), descriptor.componentDefinitionType(),
                        descriptor.componentAnnotation());
            } else {
                // Dagger 1.x scope compatibility requires this be suppress-able.
                if (!scopeCycleValidationType.equals(ValidationType.NONE)) {
                    validateScopeHierarchy(descriptor.componentDefinitionType(),
                            descriptor.componentDefinitionType(), reportBuilder,
                            new ArrayDeque<Equivalence.Wrapper<AnnotationMirror>>(), new ArrayDeque<TypeElement>());
                }
            }
        } else {
            // Scopeless components may not depend on scoped components.
            if (!scopedDependencies.isEmpty()) {
                StringBuilder message = new StringBuilder(descriptor.componentDefinitionType().getQualifiedName())
                        .append(" (unscoped) cannot depend on scoped components:\n");
                appendIndentedComponentsList(message, scopedDependencies);
                reportBuilder.addItem(message.toString(), descriptor.componentDefinitionType(),
                        descriptor.componentAnnotation());
            }
        }
    }

    private void validateBuilders(BindingGraph subject, Builder<BindingGraph> reportBuilder) {
        ComponentDescriptor componentDesc = subject.componentDescriptor();
        if (!componentDesc.builderSpec().isPresent()) {
            // If no builder, nothing to validate.
            return;
        }

        Set<TypeElement> allDependents = Sets.union(
                Sets.union(subject.transitiveModules().keySet(), componentDesc.dependencies()),
                componentDesc.executorDependency().asSet());
        Set<TypeElement> requiredDependents = Sets.filter(allDependents, new Predicate<TypeElement>() {
            @Override
            public boolean apply(TypeElement input) {
                return !Util.componentCanMakeNewInstances(input);
            }
        });
        final BuilderSpec spec = componentDesc.builderSpec().get();
        Map<TypeElement, ExecutableElement> allSetters = spec.methodMap();

        ErrorMessages.ComponentBuilderMessages msgs = ErrorMessages
                .builderMsgsFor(subject.componentDescriptor().kind());
        Set<TypeElement> extraSetters = Sets.difference(allSetters.keySet(), allDependents);
        if (!extraSetters.isEmpty()) {
            Collection<ExecutableElement> excessMethods = Maps.filterKeys(allSetters, Predicates.in(extraSetters))
                    .values();
            Iterable<String> formatted = FluentIterable.from(excessMethods)
                    .transform(new Function<ExecutableElement, String>() {
                        @Override
                        public String apply(ExecutableElement input) {
                            return methodSignatureFormatter.format(input,
                                    Optional.of(MoreTypes.asDeclared(spec.builderDefinitionType().asType())));
                        }
                    });
            reportBuilder.addItem(String.format(msgs.extraSetters(), formatted), spec.builderDefinitionType());
        }

        Set<TypeElement> missingSetters = Sets.difference(requiredDependents, allSetters.keySet());
        if (!missingSetters.isEmpty()) {
            reportBuilder.addItem(String.format(msgs.missingSetters(), missingSetters),
                    spec.builderDefinitionType());
        }
    }

    /**
     * Append and format 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);
            Optional<AnnotationMirror> scope = getScopeAnnotation(scopedComponent);
            if (scope.isPresent()) {
                message.append(ErrorMessages.format(scope.get())).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(Set<TypeElement> types) {
        return FluentIterable.from(types).filter(new Predicate<TypeElement>() {
            @Override
            public boolean apply(TypeElement input) {
                return getScopeAnnotation(input).isPresent();
            }
        }).toSet();
    }

    /**
     * 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.
     */
    private void validateScopeHierarchy(TypeElement rootComponent, TypeElement componentType,
            Builder<BindingGraph> reportBuilder, Deque<Equivalence.Wrapper<AnnotationMirror>> scopeStack,
            Deque<TypeElement> scopedDependencyStack) {
        Optional<AnnotationMirror> scope = getScopeAnnotation(componentType);
        if (scope.isPresent()) {
            Equivalence.Wrapper<AnnotationMirror> wrappedScope = AnnotationMirrors.equivalence().wrap(scope.get());
            if (scopeStack.contains(wrappedScope)) {
                scopedDependencyStack.push(componentType);
                // Current scope has already appeared in the component chain.
                StringBuilder message = new StringBuilder();
                message.append(rootComponent.getQualifiedName());
                message.append(" depends on scoped components in a non-hierarchical scope ordering:\n");
                appendIndentedComponentsList(message, scopedDependencyStack);
                if (scopeCycleValidationType.diagnosticKind().isPresent()) {
                    reportBuilder.addItem(message.toString(), scopeCycleValidationType.diagnosticKind().get(),
                            rootComponent, getAnnotationMirror(rootComponent, Component.class).get());
                }
                scopedDependencyStack.pop();
            } else {
                Optional<AnnotationMirror> componentAnnotation = getAnnotationMirror(componentType,
                        Component.class);
                if (componentAnnotation.isPresent()) {
                    ImmutableSet<TypeElement> scopedDependencies = scopedTypesIn(
                            MoreTypes.asTypeElements(getComponentDependencies(componentAnnotation.get())));
                    if (scopedDependencies.size() == 1) {
                        // empty can be ignored (base-case), and > 1 is a different error reported separately.
                        scopeStack.push(wrappedScope);
                        scopedDependencyStack.push(componentType);
                        validateScopeHierarchy(rootComponent, getOnlyElement(scopedDependencies), reportBuilder,
                                scopeStack, scopedDependencyStack);
                        scopedDependencyStack.pop();
                        scopeStack.pop();
                    }
                } // else: we skip component dependencies which are not components
            }
        }
    }

    /**
     * Validates that the scope (if any) of this component are compatible with the scopes of the
     * bindings available in this component
     */
    void validateComponentScope(final BindingGraph subject,
            final ValidationReport.Builder<BindingGraph> reportBuilder,
            ImmutableMap<BindingKey, ResolvedBindings> resolvedBindings) {
        Optional<Equivalence.Wrapper<AnnotationMirror>> componentScope = subject.componentDescriptor()
                .wrappedScope();
        ImmutableSet.Builder<String> incompatiblyScopedMethodsBuilder = ImmutableSet.builder();
        for (ResolvedBindings bindings : resolvedBindings.values()) {
            if (bindings.bindingKey().kind().equals(BindingKey.Kind.CONTRIBUTION)) {
                for (ContributionBinding contributionBinding : bindings.ownedContributionBindings()) {
                    if (contributionBinding instanceof ProvisionBinding) {
                        ProvisionBinding provisionBinding = (ProvisionBinding) contributionBinding;
                        if (provisionBinding.scope().isPresent()
                                && !componentScope.equals(provisionBinding.wrappedScope())) {
                            // Scoped components cannot reference bindings to @Provides methods or @Inject
                            // types decorated by a different scope annotation. Unscoped components cannot
                            // reference to scoped @Provides methods or @Inject types decorated by any
                            // scope annotation.
                            switch (provisionBinding.bindingKind()) {
                            case PROVISION:
                                ExecutableElement provisionMethod = MoreElements
                                        .asExecutable(provisionBinding.bindingElement());
                                incompatiblyScopedMethodsBuilder
                                        .add(methodSignatureFormatter.format(provisionMethod));
                                break;
                            case INJECTION:
                                incompatiblyScopedMethodsBuilder
                                        .add(stripCommonTypePrefixes(provisionBinding.scope().get().toString())
                                                + " class "
                                                + provisionBinding.bindingTypeElement().getQualifiedName());
                                break;
                            default:
                                throw new IllegalStateException();
                            }
                        }
                    }
                }
            }
        }
        ImmutableSet<String> incompatiblyScopedMethods = incompatiblyScopedMethodsBuilder.build();
        if (!incompatiblyScopedMethods.isEmpty()) {
            TypeElement componentType = subject.componentDescriptor().componentDefinitionType();
            StringBuilder message = new StringBuilder(componentType.getQualifiedName());
            if (componentScope.isPresent()) {
                message.append(" scoped with ");
                message.append(stripCommonTypePrefixes(ErrorMessages.format(componentScope.get().get())));
                message.append(" may not reference bindings with different scopes:\n");
            } else {
                message.append(" (unscoped) may not reference scoped bindings:\n");
            }
            for (String method : incompatiblyScopedMethods) {
                message.append(ErrorMessages.INDENT).append(method).append("\n");
            }
            reportBuilder.addItem(message.toString(), componentType,
                    subject.componentDescriptor().componentAnnotation());
        }
    }

    @SuppressWarnings("resource") // Appendable is a StringBuilder.
    private void reportProviderMayNotDependOnProducer(Deque<ResolvedRequest> path,
            ValidationReport.Builder<BindingGraph> reportBuilder) {
        StringBuilder errorMessage = new StringBuilder();
        if (path.size() == 1) {
            new Formatter(errorMessage).format(ErrorMessages.PROVIDER_ENTRY_POINT_MAY_NOT_DEPEND_ON_PRODUCER_FORMAT,
                    keyFormatter.format(path.peek().request().key()));
        } else {
            ImmutableSet<ProvisionBinding> dependentProvisions = provisionsDependingOnLatestRequest(path);
            // TODO(user): Consider displaying all dependent provisions in the error message. If we do
            // that, should we display all productions that depend on them also?
            new Formatter(errorMessage).format(ErrorMessages.PROVIDER_MAY_NOT_DEPEND_ON_PRODUCER_FORMAT,
                    keyFormatter.format(dependentProvisions.iterator().next().key()));
        }
        reportBuilder.addItem(errorMessage.toString(), path.getLast().request().requestElement());
    }

    private void reportMissingBinding(Deque<ResolvedRequest> path,
            ValidationReport.Builder<BindingGraph> reportBuilder) {
        Key key = path.peek().request().key();
        TypeMirror type = key.type();
        String typeName = TypeNames.forTypeMirror(type).toString();
        boolean requiresContributionMethod = !key.isValidImplicitProvisionKey(types);
        boolean requiresProvision = doesPathRequireProvisionOnly(path);
        StringBuilder errorMessage = new StringBuilder();
        String requiresErrorMessageFormat = requiresContributionMethod
                ? requiresProvision ? REQUIRES_PROVIDER_FORMAT : REQUIRES_PROVIDER_OR_PRODUCER_FORMAT
                : requiresProvision ? REQUIRES_AT_INJECT_CONSTRUCTOR_OR_PROVIDER_FORMAT
                        : REQUIRES_AT_INJECT_CONSTRUCTOR_OR_PROVIDER_OR_PRODUCER_FORMAT;
        errorMessage.append(String.format(requiresErrorMessageFormat, typeName));
        if (key.isValidMembersInjectionKey()
                && !injectBindingRegistry.getOrFindMembersInjectionBinding(key).injectionSites().isEmpty()) {
            errorMessage.append(" ").append(ErrorMessages.MEMBERS_INJECTION_DOES_NOT_IMPLY_PROVISION);
        }
        ImmutableList<String> printableDependencyPath = FluentIterable.from(path)
                .transform(REQUEST_FROM_RESOLVED_REQUEST).transform(dependencyRequestFormatter)
                .filter(Predicates.not(Predicates.equalTo(""))).toList().reverse();
        for (String dependency : printableDependencyPath.subList(1, printableDependencyPath.size())) {
            errorMessage.append("\n").append(dependency);
        }
        reportBuilder.addItem(errorMessage.toString(), path.getLast().request().requestElement());
    }

    /**
     * Returns whether the given dependency path would require the most recent request to be resolved
     * by only provision bindings.
     */
    private boolean doesPathRequireProvisionOnly(Deque<ResolvedRequest> path) {
        if (path.size() == 1) {
            // if this is an entry-point, then we check the request
            switch (path.peek().request().kind()) {
            case INSTANCE:
            case PROVIDER:
            case LAZY:
            case MEMBERS_INJECTOR:
                return true;
            case PRODUCER:
            case PRODUCED:
            case FUTURE:
                return false;
            default:
                throw new AssertionError();
            }
        }
        // otherwise, the second-most-recent bindings determine whether the most recent one must be a
        // provision
        ImmutableSet<ProvisionBinding> dependentProvisions = provisionsDependingOnLatestRequest(path);
        return !dependentProvisions.isEmpty();
    }

    /**
     * Returns any provision bindings resolved for the second-most-recent request in the given path;
     * that is, returns those provision bindings that depend on the latest request in the path.
     */
    private ImmutableSet<ProvisionBinding> provisionsDependingOnLatestRequest(Deque<ResolvedRequest> path) {
        Iterator<ResolvedRequest> iterator = path.iterator();
        final DependencyRequest request = iterator.next().request();
        ResolvedRequest previousResolvedRequest = iterator.next();
        @SuppressWarnings("unchecked") // validated by instanceof below
        ImmutableSet<ProvisionBinding> bindings = (ImmutableSet<ProvisionBinding>) FluentIterable
                .from(previousResolvedRequest.binding().bindings()).filter(new Predicate<Binding>() {
                    @Override
                    public boolean apply(Binding binding) {
                        return binding instanceof ProvisionBinding
                                && binding.implicitDependencies().contains(request);
                    }
                }).toSet();
        return bindings;
    }

    private static final int DUPLICATE_SIZE_LIMIT = 10;

    @SuppressWarnings("resource") // Appendable is a StringBuilder.
    private void reportDuplicateBindings(Deque<ResolvedRequest> path,
            ValidationReport.Builder<BindingGraph> reportBuilder) {
        ResolvedBindings resolvedBinding = path.peek().binding();
        StringBuilder builder = new StringBuilder();
        new Formatter(builder).format(ErrorMessages.DUPLICATE_BINDINGS_FOR_KEY_FORMAT,
                keyFormatter.format(path.peek().request().key()));
        for (Binding binding : Iterables.limit(resolvedBinding.bindings(), DUPLICATE_SIZE_LIMIT)) {
            builder.append('\n').append(INDENT);
            // TODO(user): Refactor the formatters so we don't need these instanceof checks.
            if (binding instanceof ProvisionBinding) {
                builder.append(provisionBindingFormatter.format((ProvisionBinding) binding));
            } else if (binding instanceof ProductionBinding) {
                builder.append(productionBindingFormatter.format((ProductionBinding) binding));
            }
        }
        int numberOfOtherBindings = resolvedBinding.bindings().size() - DUPLICATE_SIZE_LIMIT;
        if (numberOfOtherBindings > 0) {
            builder.append('\n').append(INDENT).append("and ").append(numberOfOtherBindings).append(" other");
        }
        if (numberOfOtherBindings > 1) {
            builder.append('s');
        }
        reportBuilder.addItem(builder.toString(), path.getLast().request().requestElement());
    }

    @SuppressWarnings("resource") // Appendable is a StringBuilder.
    private void reportMultipleBindingTypes(Deque<ResolvedRequest> path,
            ValidationReport.Builder<BindingGraph> reportBuilder) {
        ResolvedBindings resolvedBinding = path.peek().binding();
        StringBuilder builder = new StringBuilder();
        new Formatter(builder).format(ErrorMessages.MULTIPLE_BINDING_TYPES_FOR_KEY_FORMAT,
                keyFormatter.format(path.peek().request().key()));
        ImmutableListMultimap<BindingType, ContributionBinding> bindingsByType = ContributionBinding
                .bindingTypesFor(resolvedBinding.contributionBindings());
        for (BindingType type : Ordering.natural().immutableSortedCopy(bindingsByType.keySet())) {
            builder.append(INDENT);
            builder.append(formatBindingType(type));
            builder.append(" bindings:\n");
            for (ContributionBinding binding : bindingsByType.get(type)) {
                builder.append(INDENT).append(INDENT);
                if (binding instanceof ProvisionBinding) {
                    builder.append(provisionBindingFormatter.format((ProvisionBinding) binding));
                } else if (binding instanceof ProductionBinding) {
                    builder.append(productionBindingFormatter.format((ProductionBinding) binding));
                }
                builder.append('\n');
            }
        }
        reportBuilder.addItem(builder.toString(), path.getLast().request().requestElement());
    }

    private String formatBindingType(BindingType type) {
        switch (type) {
        case MAP:
            return "Map";
        case SET:
            return "Set";
        case UNIQUE:
            return "Unique";
        default:
            throw new IllegalStateException("Unknown binding type: " + type);
        }
    }

    private void reportCycle(DependencyRequest request, Deque<ResolvedRequest> path,
            final ValidationReport.Builder<BindingGraph> reportBuilder) {
        ImmutableList<DependencyRequest> pathElements = ImmutableList.<DependencyRequest>builder().add(request)
                .addAll(Iterables.transform(path, REQUEST_FROM_RESOLVED_REQUEST)).build();
        ImmutableList<String> printableDependencyPath = FluentIterable.from(pathElements)
                .transform(dependencyRequestFormatter).filter(Predicates.not(Predicates.equalTo(""))).toList()
                .reverse();
        DependencyRequest rootRequest = path.getLast().request();
        TypeElement componentType = MoreElements.asType(rootRequest.requestElement().getEnclosingElement());
        // TODO(cgruber): Restructure to provide a hint for the start and end of the cycle.
        reportBuilder
                .addItem(
                        String.format(ErrorMessages.CONTAINS_DEPENDENCY_CYCLE_FORMAT,
                                componentType.getQualifiedName(), rootRequest.requestElement().getSimpleName(),
                                Joiner.on("\n")
                                        .join(printableDependencyPath.subList(1, printableDependencyPath.size()))),
                        rootRequest.requestElement());
    }

    @AutoValue
    abstract static class ResolvedRequest {
        abstract DependencyRequest request();

        abstract ResolvedBindings binding();

        static ResolvedRequest create(DependencyRequest request, BindingGraph graph) {
            BindingKey bindingKey = request.bindingKey();
            ResolvedBindings resolvedBindings = graph.resolvedBindings().get(bindingKey);
            return new AutoValue_BindingGraphValidator_ResolvedRequest(request,
                    resolvedBindings == null
                            ? ResolvedBindings.create(bindingKey, ImmutableSet.<Binding>of(),
                                    ImmutableSet.<Binding>of())
                            : resolvedBindings);
        }
    }

    private static final Function<ResolvedRequest, DependencyRequest> REQUEST_FROM_RESOLVED_REQUEST = new Function<ResolvedRequest, DependencyRequest>() {
        @Override
        public DependencyRequest apply(ResolvedRequest resolvedRequest) {
            return resolvedRequest.request();
        }
    };

    abstract static class Traverser {
        abstract boolean visitResolvedRequest(Deque<ResolvedRequest> path);
    }
}