dagger.internal.codegen.bindinggraphvalidation.DuplicateBindingsValidator 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.
The newest version!
/*
* 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 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.internal.codegen.model.BindingKind.INJECTION;
import static dagger.internal.codegen.model.BindingKind.MEMBERS_INJECTION;
import static java.util.Comparator.comparing;
import static javax.tools.Diagnostic.Kind.ERROR;
import androidx.room.compiler.processing.XElement;
import androidx.room.compiler.processing.XType;
import androidx.room.compiler.processing.XTypeElement;
import com.google.auto.value.AutoValue;
import com.google.common.base.Equivalence;
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 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.internal.codegen.model.Binding;
import dagger.internal.codegen.model.BindingGraph;
import dagger.internal.codegen.model.BindingGraph.ComponentNode;
import dagger.internal.codegen.model.BindingKind;
import dagger.internal.codegen.model.ComponentPath;
import dagger.internal.codegen.model.DaggerAnnotation;
import dagger.internal.codegen.model.DaggerElement;
import dagger.internal.codegen.model.DaggerTypeElement;
import dagger.internal.codegen.model.DiagnosticReporter;
import dagger.internal.codegen.model.Key;
import dagger.internal.codegen.model.Key.MultibindingContributionIdentifier;
import dagger.internal.codegen.validation.ValidationBindingGraphPlugin;
import dagger.internal.codegen.xprocessing.XTypes;
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.tools.Diagnostic;
/** Reports errors for conflicting bindings with the same key. */
final class DuplicateBindingsValidator extends ValidationBindingGraphPlugin {
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())) {
reportErrors(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(BindingWithoutComponent::index)
.filter(duplicates -> duplicates.keySet().size() > 1)
.collect(toImmutableSet());
}
private ImmutableSet> groupBindingsByKey(BindingGraph bindingGraph) {
return valueSetsForEachKey(
bindingGraph.bindings().stream()
.filter(binding -> !binding.kind().equals(MEMBERS_INJECTION))
.collect(
toImmutableSetMultimap(
binding ->
// If the "ignoreProvisionKeyWildcards" flag is enabled then ignore the
// variance in the key types here so that Foo and Foo extends Bar>
// get grouped into the same set (i.e. as duplicates).
KeyWithTypeEquivalence.forKey(
binding.key(),
compilerOptions.ignoreProvisionKeyWildcards()
? XTypes.equivalenceIgnoringVariance()
: XTypes.equivalence()),
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 reportErrors(
ImmutableSetMultimap duplicateBindings,
BindingGraph bindingGraph,
DiagnosticReporter diagnosticReporter) {
if (explicitBindingConfictsWithInject(duplicateBindings.keySet())) {
compilerOptions
.explicitBindingConflictsWithInjectValidationType()
.diagnosticKind()
.ifPresent(
diagnosticKind ->
reportExplicitBindingConflictsWithInject(
duplicateBindings.values(),
diagnosticReporter,
diagnosticKind,
bindingGraph.rootComponentNode()));
return;
}
reportDuplicateBindings(duplicateBindings.values(), bindingGraph, diagnosticReporter);
}
/**
* 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, BindingWithoutComponent::bindingKind).keys();
return bindingKinds.count(INJECTION) == 1 && bindingKinds.size() == 2;
}
private void reportExplicitBindingConflictsWithInject(
ImmutableCollection duplicateBindings,
DiagnosticReporter diagnosticReporter,
Diagnostic.Kind diagnosticKind,
ComponentNode rootComponent) {
Binding injectBinding = rootmostBindingWithKind(k -> k.equals(INJECTION), duplicateBindings);
Binding explicitBinding = rootmostBindingWithKind(k -> !k.equals(INJECTION), duplicateBindings);
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 void reportDuplicateBindings(
ImmutableCollection duplicateBindings,
BindingGraph graph,
DiagnosticReporter diagnosticReporter) {
StringBuilder message = new StringBuilder();
Binding oneBinding = duplicateBindings.asList().get(0);
ImmutableSet multibindings =
duplicateBindings.stream()
.filter(binding -> binding.kind().isMultibinding())
.collect(toImmutableSet());
if (multibindings.isEmpty()) {
message.append(oneBinding.key()).append(" is bound multiple times:");
formatDeclarations(message, 2, declarations(graph, duplicateBindings));
} else {
Binding oneMultibinding = multibindings.asList().get(0);
message.append(oneMultibinding.key()).append(" has incompatible bindings or declarations:\n");
message
.append(INDENT)
.append(multibindingTypeString(oneMultibinding))
.append(" bindings and declarations:");
formatDeclarations(message, 2, declarations(graph, multibindings));
ImmutableSet uniqueBindingDeclarations =
duplicateBindings.stream()
.filter(binding -> !binding.kind().isMultibinding())
.flatMap(binding -> declarations(graph, binding).stream())
.filter(declaration -> !(declaration instanceof MultibindingDeclaration))
.collect(toImmutableSet());
if (!uniqueBindingDeclarations.isEmpty()) {
message.append('\n').append(INDENT).append("Unique bindings and declarations:");
formatDeclarations(message, 2, uniqueBindingDeclarations);
}
}
if (compilerOptions.experimentalDaggerErrorMessages()) {
message.append(String.format("\n%sin component: [%s]", INDENT, oneBinding.componentPath()));
diagnosticReporter.reportComponent(ERROR, graph.rootComponentNode(), message.toString());
} else {
diagnosticReporter.reportBinding(ERROR, oneBinding, message.toString());
}
}
private void formatDeclarations(
StringBuilder builder,
int indentLevel,
Iterable extends BindingDeclaration> bindingDeclarations) {
bindingDeclarationFormatter.formatIndentedList(
builder, ImmutableList.copyOf(bindingDeclarations), indentLevel);
}
private ImmutableSet declarations(
BindingGraph graph, ImmutableCollection bindings) {
return bindings.stream()
.flatMap(binding -> declarations(graph, binding).stream())
.distinct()
.sorted(BindingDeclaration.COMPARATOR)
.collect(toImmutableSet());
}
private ImmutableSet declarations(BindingGraph graph, 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(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, E> 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 BindingWithoutComponent {
abstract BindingKind bindingKind();
abstract Key bindingKey();
abstract Optional bindingElement();
abstract Optional contributingModule();
static ImmutableSetMultimap index(Set bindings) {
return bindings.stream()
.collect(toImmutableSetMultimap(BindingWithoutComponent::forBinding, b -> b));
}
private static BindingWithoutComponent forBinding(Binding binding) {
return new AutoValue_DuplicateBindingsValidator_BindingWithoutComponent(
binding.kind(),
binding.key(),
binding.bindingElement().map(DaggerElement::xprocessing),
binding.contributingModule().map(DaggerTypeElement::xprocessing));
}
}
/** The identifying information about a key with the given type equivalence. */
@AutoValue
abstract static class KeyWithTypeEquivalence {
abstract Optional qualifier();
abstract Equivalence.Wrapper wrappedType();
abstract Optional multibindingContributionIdentifier();
private static KeyWithTypeEquivalence forKey(Key key, Equivalence typeEquivalence) {
return new AutoValue_DuplicateBindingsValidator_KeyWithTypeEquivalence(
key.qualifier(),
typeEquivalence.wrap(key.type().xprocessing()),
key.multibindingContributionIdentifier());
}
}
}