/* * 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.bindinggraphvalidation; import static com.google.common.base.Verify.verify; import static com.google.common.collect.Iterables.getOnlyElement; import static dagger.internal.codegen.base.Formatter.INDENT; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSetMultimap; import static dagger.model.BindingKind.INJECTION; import static dagger.model.BindingKind.MEMBERS_INJECTION; import static java.util.Comparator.comparing; import static javax.tools.Diagnostic.Kind.ERROR; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMultiset; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.google.common.collect.Sets; import dagger.internal.codegen.base.Formatter; import dagger.internal.codegen.binding.BindingDeclaration; import dagger.internal.codegen.binding.BindingDeclarationFormatter; import dagger.internal.codegen.binding.BindingNode; import dagger.internal.codegen.binding.MultibindingDeclaration; import dagger.internal.codegen.compileroption.CompilerOptions; import dagger.model.Binding; import dagger.model.BindingGraph; import dagger.model.BindingGraph.ComponentNode; import dagger.model.BindingKind; import dagger.model.ComponentPath; import dagger.model.Key; import dagger.spi.BindingGraphPlugin; import dagger.spi.DiagnosticReporter; import java.util.Comparator; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import javax.inject.Inject; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic.Kind; /** Reports errors for conflicting bindings with the same key. */ final class DuplicateBindingsValidator implements BindingGraphPlugin { private static final Comparator BY_LENGTH_OF_COMPONENT_PATH = comparing(binding -> binding.componentPath().components().size()); private final BindingDeclarationFormatter bindingDeclarationFormatter; private final CompilerOptions compilerOptions; @Inject DuplicateBindingsValidator( BindingDeclarationFormatter bindingDeclarationFormatter, CompilerOptions compilerOptions) { this.bindingDeclarationFormatter = bindingDeclarationFormatter; this.compilerOptions = compilerOptions; } @Override public String pluginName() { return "Dagger/DuplicateBindings"; } @Override public void visitGraph(BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter) { // If two unrelated subcomponents have the same duplicate bindings only because they install the // same two modules, then fixing the error in one subcomponent will uncover the second // subcomponent to fix. // TODO(ronshapiro): Explore ways to address such underreporting without overreporting. Set> reportedDuplicateBindingSets = new HashSet<>(); duplicateBindingSets(bindingGraph) .forEach( duplicateBindings -> { // Only report each set of duplicate bindings once, ignoring the installed component. if (reportedDuplicateBindingSets.add(duplicateBindings.keySet())) { reportDuplicateBindings(duplicateBindings, bindingGraph, diagnosticReporter); } }); } /** * Returns sets of duplicate bindings. Bindings are duplicates if they bind the same key and are * visible from the same component. Two bindings that differ only in the component that owns them * are not considered to be duplicates, because that means the same binding was "copied" down to a * descendant component because it depends on local multibindings or optional bindings. Hence each * "set" is represented as a multimap from binding element (ignoring component path) to binding. */ private ImmutableSet> duplicateBindingSets( BindingGraph bindingGraph) { return groupBindingsByKey(bindingGraph).stream() .flatMap(bindings -> mutuallyVisibleSubsets(bindings).stream()) .map(BindingElement::index) .filter(duplicates -> duplicates.keySet().size() > 1) .collect(toImmutableSet()); } private static ImmutableSet> groupBindingsByKey(BindingGraph bindingGraph) { return valueSetsForEachKey( bindingGraph.bindings().stream() .filter(binding -> !binding.kind().equals(MEMBERS_INJECTION)) .collect(toImmutableSetMultimap(Binding::key, binding -> binding))); } /** * Returns the subsets of the input set that contain bindings that are all visible from the same * component. A binding is visible from its component and all its descendants. */ private static ImmutableSet> mutuallyVisibleSubsets( Set duplicateBindings) { ImmutableListMultimap bindingsByComponentPath = Multimaps.index(duplicateBindings, Binding::componentPath); ImmutableSetMultimap.Builder mutuallyVisibleBindings = ImmutableSetMultimap.builder(); bindingsByComponentPath .asMap() .forEach( (componentPath, bindings) -> { mutuallyVisibleBindings.putAll(componentPath, bindings); for (ComponentPath ancestor = componentPath; !ancestor.atRoot(); ) { ancestor = ancestor.parent(); ImmutableList bindingsInAncestor = bindingsByComponentPath.get(ancestor); mutuallyVisibleBindings.putAll(componentPath, bindingsInAncestor); } }); return valueSetsForEachKey(mutuallyVisibleBindings.build()); } private void reportDuplicateBindings( ImmutableSetMultimap duplicateBindings, BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter) { if (explicitBindingConfictsWithInject(duplicateBindings.keySet())) { compilerOptions .explicitBindingConflictsWithInjectValidationType() .diagnosticKind() .ifPresent( diagnosticKind -> reportExplicitBindingConflictsWithInject( duplicateBindings, diagnosticReporter, diagnosticKind, bindingGraph.rootComponentNode())); return; } ImmutableSet bindings = ImmutableSet.copyOf(duplicateBindings.values()); Binding oneBinding = bindings.asList().get(0); String message = bindings.stream().anyMatch(binding -> binding.kind().isMultibinding()) ? incompatibleBindingsMessage(oneBinding, bindings, bindingGraph) : duplicateBindingMessage(oneBinding, bindings, bindingGraph); if (compilerOptions.experimentalDaggerErrorMessages()) { diagnosticReporter.reportComponent( ERROR, bindingGraph.rootComponentNode(), message); } else { diagnosticReporter.reportBinding( ERROR, oneBinding, message); } } /** * Returns {@code true} if the bindings contain one {@code @Inject} binding and one that isn't. */ private static boolean explicitBindingConfictsWithInject( ImmutableSet duplicateBindings) { ImmutableMultiset bindingKinds = Multimaps.index(duplicateBindings, BindingElement::bindingKind).keys(); return bindingKinds.count(INJECTION) == 1 && bindingKinds.size() == 2; } private void reportExplicitBindingConflictsWithInject( ImmutableSetMultimap duplicateBindings, DiagnosticReporter diagnosticReporter, Kind diagnosticKind, ComponentNode rootComponent) { Binding injectBinding = rootmostBindingWithKind(k -> k.equals(INJECTION), duplicateBindings.values()); Binding explicitBinding = rootmostBindingWithKind(k -> !k.equals(INJECTION), duplicateBindings.values()); StringBuilder message = new StringBuilder() .append(explicitBinding.key()) .append(" is bound multiple times:") .append(formatWithComponentPath(injectBinding)) .append(formatWithComponentPath(explicitBinding)) .append( "\nThis condition was never validated before, and will soon be an error. " + "See https://dagger.dev/conflicting-inject."); if (compilerOptions.experimentalDaggerErrorMessages()) { diagnosticReporter.reportComponent(diagnosticKind, rootComponent, message.toString()); } else { diagnosticReporter.reportBinding(diagnosticKind, explicitBinding, message.toString()); } } private String formatWithComponentPath(Binding binding) { return String.format( "\n%s%s [%s]", Formatter.INDENT, bindingDeclarationFormatter.format(((BindingNode) binding).delegate()), binding.componentPath()); } private String duplicateBindingMessage( Binding oneBinding, ImmutableSet duplicateBindings, BindingGraph graph) { StringBuilder message = new StringBuilder().append(oneBinding.key()).append(" is bound multiple times:"); formatDeclarations(message, 1, declarations(graph, duplicateBindings)); if (compilerOptions.experimentalDaggerErrorMessages()) { message.append(String.format("\n%sin component: [%s]", INDENT, oneBinding.componentPath())); } return message.toString(); } private String incompatibleBindingsMessage( Binding oneBinding, ImmutableSet duplicateBindings, BindingGraph graph) { Key key = oneBinding.key(); ImmutableSet multibindings = duplicateBindings.stream() .filter(binding -> binding.kind().isMultibinding()) .collect(toImmutableSet()); verify( multibindings.size() == 1, "expected only one multibinding for %s: %s", key, multibindings); StringBuilder message = new StringBuilder(); java.util.Formatter messageFormatter = new java.util.Formatter(message); messageFormatter.format("%s has incompatible bindings or declarations:\n", key); message.append(INDENT); dagger.model.Binding multibinding = getOnlyElement(multibindings); messageFormatter.format("%s bindings and declarations:", multibindingTypeString(multibinding)); formatDeclarations(message, 2, declarations(graph, multibindings)); Set uniqueBindings = Sets.filter(duplicateBindings, binding -> !binding.equals(multibinding)); message.append('\n').append(INDENT).append("Unique bindings and declarations:"); formatDeclarations( message, 2, Sets.filter( declarations(graph, uniqueBindings), declaration -> !(declaration instanceof MultibindingDeclaration))); if (compilerOptions.experimentalDaggerErrorMessages()) { message.append(String.format("\n%sin component: [%s]", INDENT, oneBinding.componentPath())); } return message.toString(); } private void formatDeclarations( StringBuilder builder, int indentLevel, Iterable bindingDeclarations) { bindingDeclarationFormatter.formatIndentedList( builder, ImmutableList.copyOf(bindingDeclarations), indentLevel); } private ImmutableSet declarations( BindingGraph graph, Set bindings) { return bindings.stream() .flatMap(binding -> declarations(graph, binding).stream()) .distinct() .sorted(BindingDeclaration.COMPARATOR) .collect(toImmutableSet()); } private ImmutableSet declarations( BindingGraph graph, dagger.model.Binding binding) { ImmutableSet.Builder declarations = ImmutableSet.builder(); BindingNode bindingNode = (BindingNode) binding; bindingNode.associatedDeclarations().forEach(declarations::add); if (bindingDeclarationFormatter.canFormat(bindingNode.delegate())) { declarations.add(bindingNode.delegate()); } else { graph.requestedBindings(binding).stream() .flatMap(requestedBinding -> declarations(graph, requestedBinding).stream()) .forEach(declarations::add); } return declarations.build(); } private String multibindingTypeString(dagger.model.Binding multibinding) { switch (multibinding.kind()) { case MULTIBOUND_MAP: return "Map"; case MULTIBOUND_SET: return "Set"; default: throw new AssertionError(multibinding); } } private static ImmutableSet> valueSetsForEachKey(Multimap multimap) { return multimap.asMap().values().stream().map(ImmutableSet::copyOf).collect(toImmutableSet()); } /** Returns the binding of the given kind that is closest to the root component. */ private static Binding rootmostBindingWithKind( Predicate bindingKindPredicate, ImmutableCollection bindings) { return bindings.stream() .filter(b -> bindingKindPredicate.test(b.kind())) .min(BY_LENGTH_OF_COMPONENT_PATH) .get(); } /** The identifying information about a binding, excluding its {@link Binding#componentPath()}. */ @AutoValue abstract static class BindingElement { abstract BindingKind bindingKind(); abstract Optional bindingElement(); abstract Optional contributingModule(); static ImmutableSetMultimap index(Set bindings) { return bindings.stream().collect(toImmutableSetMultimap(BindingElement::forBinding, b -> b)); } private static BindingElement forBinding(Binding binding) { return new AutoValue_DuplicateBindingsValidator_BindingElement( binding.kind(), binding.bindingElement(), binding.contributingModule()); } } }