blob: eb0ef6cff1754fb7400479fefd5d0bb1ced882b0 [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.bindinggraphvalidation;
import static com.google.common.base.Verify.verify;
import static com.google.common.collect.Iterables.getLast;
import static dagger.internal.codegen.base.Keys.isValidImplicitProvisionKey;
import static dagger.internal.codegen.base.Keys.isValidMembersInjectionKey;
import static dagger.internal.codegen.base.RequestKinds.canBeSatisfiedByProductionBinding;
import static dagger.internal.codegen.binding.DependencyRequestFormatter.DOUBLE_INDENT;
import static dagger.internal.codegen.extension.DaggerStreams.instancesOf;
import static dagger.internal.codegen.xprocessing.XTypes.isWildcard;
import static javax.tools.Diagnostic.Kind.ERROR;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import dagger.internal.codegen.binding.DependencyRequestFormatter;
import dagger.internal.codegen.binding.InjectBindingRegistry;
import dagger.internal.codegen.validation.DiagnosticMessageGenerator;
import dagger.internal.codegen.validation.ValidationBindingGraphPlugin;
import dagger.spi.model.Binding;
import dagger.spi.model.BindingGraph;
import dagger.spi.model.BindingGraph.ComponentNode;
import dagger.spi.model.BindingGraph.DependencyEdge;
import dagger.spi.model.BindingGraph.Edge;
import dagger.spi.model.BindingGraph.MissingBinding;
import dagger.spi.model.BindingGraph.Node;
import dagger.spi.model.ComponentPath;
import dagger.spi.model.DiagnosticReporter;
import dagger.spi.model.Key;
import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Inject;
/** Reports errors for missing bindings. */
final class MissingBindingValidator extends ValidationBindingGraphPlugin {
private final InjectBindingRegistry injectBindingRegistry;
private final DependencyRequestFormatter dependencyRequestFormatter;
private final DiagnosticMessageGenerator.Factory diagnosticMessageGeneratorFactory;
@Inject
MissingBindingValidator(
InjectBindingRegistry injectBindingRegistry,
DependencyRequestFormatter dependencyRequestFormatter,
DiagnosticMessageGenerator.Factory diagnosticMessageGeneratorFactory) {
this.injectBindingRegistry = injectBindingRegistry;
this.dependencyRequestFormatter = dependencyRequestFormatter;
this.diagnosticMessageGeneratorFactory = diagnosticMessageGeneratorFactory;
}
@Override
public String pluginName() {
return "Dagger/MissingBinding";
}
@Override
public void visitGraph(BindingGraph graph, DiagnosticReporter diagnosticReporter) {
// Don't report missing bindings when validating a full binding graph or a graph built from a
// subcomponent.
if (graph.isFullBindingGraph() || graph.rootComponentNode().isSubcomponent()) {
return;
}
graph
.missingBindings()
.forEach(missingBinding -> reportMissingBinding(missingBinding, graph, diagnosticReporter));
}
private void reportMissingBinding(
MissingBinding missingBinding, BindingGraph graph, DiagnosticReporter diagnosticReporter) {
List<ComponentPath> alternativeComponents =
graph.bindings(missingBinding.key()).stream()
.map(Binding::componentPath)
.distinct()
.collect(Collectors.toList());
// Print component name for each binding along the dependency path if the missing binding
// exists in a different component than expected
if (alternativeComponents.isEmpty()) {
diagnosticReporter.reportBinding(
ERROR, missingBinding, missingBindingErrorMessage(missingBinding, graph));
} else {
diagnosticReporter.reportComponent(
ERROR,
graph.componentNode(missingBinding.componentPath()).get(),
missingBindingErrorMessage(missingBinding, graph)
+ wrongComponentErrorMessage(missingBinding, alternativeComponents, graph));
}
}
private String missingBindingErrorMessage(MissingBinding missingBinding, BindingGraph graph) {
Key key = missingBinding.key();
StringBuilder errorMessage = new StringBuilder();
// Wildcards should have already been checked by DependencyRequestValidator.
verify(!isWildcard(key.type().xprocessing()), "unexpected wildcard request: %s", key);
// TODO(ronshapiro): replace "provided" with "satisfied"?
errorMessage.append(key).append(" cannot be provided without ");
if (isValidImplicitProvisionKey(key)) {
errorMessage.append("an @Inject constructor or ");
}
errorMessage.append("an @Provides-"); // TODO(dpb): s/an/a
if (allIncomingDependenciesCanUseProduction(missingBinding, graph)) {
errorMessage.append(" or @Produces-");
}
errorMessage.append("annotated method.");
if (isValidMembersInjectionKey(key) && typeHasInjectionSites(key)) {
errorMessage.append(
" This type supports members injection but cannot be implicitly provided.");
}
return errorMessage.toString();
}
private String wrongComponentErrorMessage(
MissingBinding missingBinding,
List<ComponentPath> alternativeComponentPath,
BindingGraph graph) {
ImmutableSet<DependencyEdge> entryPoints =
graph.entryPointEdgesDependingOnBinding(missingBinding);
DiagnosticMessageGenerator generator = diagnosticMessageGeneratorFactory.create(graph);
ImmutableList<DependencyEdge> dependencyTrace =
generator.dependencyTrace(missingBinding, entryPoints);
StringBuilder message =
graph.isFullBindingGraph()
? new StringBuilder()
: new StringBuilder(dependencyTrace.size() * 100 /* a guess heuristic */);
// Check in which component the missing binding is requested. This can be different from the
// component the missing binding is in because we'll try to search up the parent components for
// a binding which makes missing bindings end up at the root component. This is different from
// the place we are logically requesting the binding from. Note that this is related to the
// particular dependency trace being shown and so is not necessarily stable.
String missingComponentName =
getComponentFromDependencyEdge(dependencyTrace.get(0), graph, false);
boolean hasSameComponentName = false;
for (ComponentPath component : alternativeComponentPath) {
message.append("\nA binding for ").append(missingBinding.key()).append(" exists in ");
String currentComponentName = component.currentComponent().className().canonicalName();
if (currentComponentName.contentEquals(missingComponentName)) {
hasSameComponentName = true;
message.append("[").append(component).append("]");
} else {
message.append(currentComponentName);
}
message.append(":");
}
for (DependencyEdge edge : dependencyTrace) {
String line = dependencyRequestFormatter.format(edge.dependencyRequest());
if (line.isEmpty()) {
continue;
}
// If we ran into a rare case where the component names collide and we need to show the full
// path, only show the full path for the first dependency request. This is guaranteed to be
// the component in question since the logic for checking for a collision uses the first
// edge in the trace. Do not expand subsequent component paths to reduce spam.
String componentName =
String.format("[%s] ", getComponentFromDependencyEdge(edge, graph, hasSameComponentName));
hasSameComponentName = false;
message.append("\n").append(line.replace(DOUBLE_INDENT, DOUBLE_INDENT + componentName));
}
if (!dependencyTrace.isEmpty()) {
generator.appendComponentPathUnlessAtRoot(message, source(getLast(dependencyTrace), graph));
}
message.append(
generator.getRequestsNotInTrace(
dependencyTrace, generator.requests(missingBinding), entryPoints));
return message.toString();
}
private boolean allIncomingDependenciesCanUseProduction(
MissingBinding missingBinding, BindingGraph graph) {
return graph.network().inEdges(missingBinding).stream()
.flatMap(instancesOf(DependencyEdge.class))
.allMatch(edge -> dependencyCanBeProduction(edge, graph));
}
// TODO(ronshapiro): merge with
// ProvisionDependencyOnProduerBindingValidator.dependencyCanUseProduction
private boolean dependencyCanBeProduction(DependencyEdge edge, BindingGraph graph) {
Node source = graph.network().incidentNodes(edge).source();
if (source instanceof ComponentNode) {
return canBeSatisfiedByProductionBinding(edge.dependencyRequest().kind());
}
if (source instanceof dagger.spi.model.Binding) {
return ((dagger.spi.model.Binding) source).isProduction();
}
throw new IllegalArgumentException(
"expected a dagger.spi.model.Binding or ComponentNode: " + source);
}
private boolean typeHasInjectionSites(Key key) {
return injectBindingRegistry
.getOrFindMembersInjectionBinding(key)
.map(binding -> !binding.injectionSites().isEmpty())
.orElse(false);
}
private static String getComponentFromDependencyEdge(
DependencyEdge edge, BindingGraph graph, boolean completePath) {
ComponentPath componentPath = graph.network().incidentNodes(edge).source().componentPath();
return completePath
? componentPath.toString()
: componentPath.currentComponent().className().canonicalName();
}
private Node source(Edge edge, BindingGraph graph) {
return graph.network().incidentNodes(edge).source();
}
}