dagger.internal.codegen.validation.DiagnosticMessageGenerator Maven / Gradle / Ivy
Show all versions of dagger-compiler Show documentation
/*
* 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 androidx.room.compiler.processing.XElementKt.isTypeElement;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Predicates.equalTo;
import static com.google.common.base.Verify.verify;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.getLast;
import static com.google.common.collect.Iterables.indexOf;
import static com.google.common.collect.Iterables.transform;
import static dagger.internal.codegen.base.ElementFormatter.elementToString;
import static dagger.internal.codegen.extension.DaggerGraphs.shortestPath;
import static dagger.internal.codegen.extension.DaggerStreams.instancesOf;
import static dagger.internal.codegen.extension.DaggerStreams.presentValues;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet;
import static dagger.internal.codegen.xprocessing.XElements.asExecutable;
import static dagger.internal.codegen.xprocessing.XElements.asTypeElement;
import static dagger.internal.codegen.xprocessing.XElements.closestEnclosingTypeElement;
import static dagger.internal.codegen.xprocessing.XElements.isExecutable;
import static java.util.Collections.min;
import static java.util.Comparator.comparing;
import static java.util.Comparator.comparingInt;
import androidx.room.compiler.processing.XElement;
import androidx.room.compiler.processing.XType;
import androidx.room.compiler.processing.XTypeElement;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Table;
import dagger.internal.codegen.base.ElementFormatter;
import dagger.internal.codegen.base.Formatter;
import dagger.internal.codegen.binding.DependencyRequestFormatter;
import dagger.internal.codegen.model.Binding;
import dagger.internal.codegen.model.BindingGraph;
import dagger.internal.codegen.model.BindingGraph.DependencyEdge;
import dagger.internal.codegen.model.BindingGraph.Edge;
import dagger.internal.codegen.model.BindingGraph.MaybeBinding;
import dagger.internal.codegen.model.BindingGraph.Node;
import dagger.internal.codegen.model.ComponentPath;
import dagger.internal.codegen.model.DaggerElement;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import javax.inject.Inject;
/** Helper class for generating diagnostic messages. */
public final class DiagnosticMessageGenerator {
/** Injectable factory for {@code DiagnosticMessageGenerator}. */
public static final class Factory {
private final DependencyRequestFormatter dependencyRequestFormatter;
private final ElementFormatter elementFormatter;
@Inject
Factory(
DependencyRequestFormatter dependencyRequestFormatter,
ElementFormatter elementFormatter) {
this.dependencyRequestFormatter = dependencyRequestFormatter;
this.elementFormatter = elementFormatter;
}
/** Creates a {@code DiagnosticMessageGenerator} for the given binding graph. */
public DiagnosticMessageGenerator create(BindingGraph graph) {
return new DiagnosticMessageGenerator(graph, dependencyRequestFormatter, elementFormatter);
}
}
private final BindingGraph graph;
private final DependencyRequestFormatter dependencyRequestFormatter;
private final ElementFormatter elementFormatter;
/** A cached function from type to all of its supertypes in breadth-first order. */
private final Function> supertypes;
/** The shortest path (value) from an entry point (column) to a binding (row). */
private final Table> shortestPaths =
HashBasedTable.create();
private static Function memoize(Function uncached) {
// If Android Guava is on the processor path, then c.g.c.b.Function (which LoadingCache
// implements) does not extend j.u.f.Function.
// TODO(erichang): Fix current breakages and try to remove this to enforce not having this on
// processor path.
// First, explicitly convert uncached to c.g.c.b.Function because CacheLoader.from() expects
// one.
com.google.common.base.Function uncachedAsBaseFunction = uncached::apply;
LoadingCache cache =
CacheBuilder.newBuilder().build(CacheLoader.from(uncachedAsBaseFunction));
// Second, explicitly convert LoadingCache to j.u.f.Function.
@SuppressWarnings("deprecation") // uncachedAsBaseFunction throws only unchecked exceptions
Function memoized = cache::apply;
return memoized;
}
private DiagnosticMessageGenerator(
BindingGraph graph,
DependencyRequestFormatter dependencyRequestFormatter,
ElementFormatter elementFormatter) {
this.graph = graph;
this.dependencyRequestFormatter = dependencyRequestFormatter;
this.elementFormatter = elementFormatter;
supertypes =
memoize(component -> transform(component.getType().getSuperTypes(), XType::getTypeElement));
}
public String getMessage(MaybeBinding binding) {
ImmutableSet entryPoints = graph.entryPointEdgesDependingOnBinding(binding);
ImmutableSet requests = requests(binding);
ImmutableList dependencyTrace = dependencyTrace(binding, entryPoints);
return getMessageInternal(dependencyTrace, requests, entryPoints);
}
public String getMessage(DependencyEdge dependencyEdge) {
ImmutableSet requests = ImmutableSet.of(dependencyEdge);
ImmutableSet entryPoints;
ImmutableList dependencyTrace;
if (dependencyEdge.isEntryPoint()) {
entryPoints = ImmutableSet.of(dependencyEdge);
dependencyTrace = ImmutableList.of(dependencyEdge);
} else {
// It's not an entry point, so it's part of a binding
Binding binding = (Binding) source(dependencyEdge);
entryPoints = graph.entryPointEdgesDependingOnBinding(binding);
dependencyTrace =
ImmutableList.builder()
.add(dependencyEdge)
.addAll(dependencyTrace(binding, entryPoints))
.build();
}
return getMessageInternal(dependencyTrace, requests, entryPoints);
}
private String getMessageInternal(
ImmutableList dependencyTrace,
ImmutableSet requests,
ImmutableSet entryPoints) {
StringBuilder message = new StringBuilder(dependencyTrace.size() * 100 /* a guess heuristic */);
dependencyTrace.forEach(
edge -> dependencyRequestFormatter.appendFormatLine(message, edge.dependencyRequest()));
if (!dependencyTrace.isEmpty()) {
appendComponentPathUnlessAtRoot(message, source(getLast(dependencyTrace)));
}
message.append(getRequestsNotInTrace(dependencyTrace, requests, entryPoints));
return message.toString();
}
public String getRequestsNotInTrace(
ImmutableList dependencyTrace,
ImmutableSet requests,
ImmutableSet entryPoints) {
StringBuilder message = new StringBuilder();
// Print any dependency requests that aren't shown as part of the dependency trace.
ImmutableSet requestsToPrint =
requests.stream()
// if printing entry points, skip entry points and the traced request
.filter(request -> !request.isEntryPoint())
.filter(request -> !isTracedRequest(dependencyTrace, request))
.map(request -> request.dependencyRequest().requestElement())
.flatMap(presentValues())
.map(DaggerElement::xprocessing)
.collect(toImmutableSet());
if (!requestsToPrint.isEmpty()) {
message.append("\nIt is also requested at:");
elementFormatter.formatIndentedList(message, requestsToPrint, 1);
}
// Print the remaining entry points, showing which component they're in
if (entryPoints.size() > 1) {
message.append("\nThe following other entry points also depend on it:");
entryPointFormatter.formatIndentedList(
message,
entryPoints.stream()
.filter(entryPoint -> !entryPoint.equals(getLast(dependencyTrace)))
.sorted(
// 1. List entry points in components closest to the root first.
// 2. List entry points declared in a component before those in a supertype.
// 3. List entry points in declaration order in their declaring type.
rootComponentFirst()
.thenComparing(nearestComponentSupertypeFirst())
.thenComparing(requestElementDeclarationOrder()))
.collect(toImmutableList()),
1);
}
return message.toString();
}
public void appendComponentPathUnlessAtRoot(StringBuilder message, Node node) {
if (!node.componentPath().equals(graph.rootComponentNode().componentPath())) {
message.append(String.format(" [%s]", node.componentPath()));
}
}
private final Formatter entryPointFormatter =
new Formatter() {
@Override
public String format(DependencyEdge object) {
XElement requestElement = object.dependencyRequest().requestElement().get().xprocessing();
StringBuilder builder = new StringBuilder(elementToString(requestElement));
// For entry points declared in subcomponents or supertypes of the root component,
// append the component path to make clear to the user which component it's in.
ComponentPath componentPath = source(object).componentPath();
if (!componentPath.atRoot()
|| !requestElement
.getEnclosingElement()
.equals(componentPath.rootComponent().xprocessing())) {
builder.append(String.format(" [%s]", componentPath));
}
return builder.toString();
}
};
private boolean isTracedRequest(
ImmutableList dependencyTrace, DependencyEdge request) {
return !dependencyTrace.isEmpty()
&& request.dependencyRequest().equals(dependencyTrace.get(0).dependencyRequest())
// Comparing the dependency request is not enough since the request is just the key.
// Instead, we check that the target incident node is the same.
&& graph.network().incidentNodes(request).target()
.equals(graph.network().incidentNodes(dependencyTrace.get(0)).target());
}
/**
* Returns the dependency trace from one of the {@code entryPoints} to {@code binding} to {@code
* message} as a list ending with the entry point.
*/
// TODO(ronshapiro): Adding a DependencyPath type to dagger.internal.codegen.model could be
// useful, i.e.
// bindingGraph.shortestPathFromEntryPoint(DependencyEdge, MaybeBindingNode)
public ImmutableList dependencyTrace(
MaybeBinding binding, ImmutableSet entryPoints) {
// Module binding graphs may have bindings unreachable from any entry points. If there are
// no entry points for this DiagnosticInfo, don't try to print a dependency trace.
if (entryPoints.isEmpty()) {
return ImmutableList.of();
}
// Show the full dependency trace for one entry point.
DependencyEdge entryPointForTrace =
min(
entryPoints,
// prefer entry points in components closest to the root
rootComponentFirst()
// then prefer entry points with a short dependency path to the error
.thenComparing(shortestDependencyPathFirst(binding))
// then prefer entry points declared in the component to those declared in a
// supertype
.thenComparing(nearestComponentSupertypeFirst())
// finally prefer entry points declared first in their enclosing type
.thenComparing(requestElementDeclarationOrder()));
ImmutableList shortestBindingPath =
shortestPathFromEntryPoint(entryPointForTrace, binding);
verify(
!shortestBindingPath.isEmpty(),
"no dependency path from %s to %s in %s",
entryPointForTrace,
binding,
graph);
ImmutableList.Builder dependencyTrace = ImmutableList.builder();
dependencyTrace.add(entryPointForTrace);
for (int i = 0; i < shortestBindingPath.size() - 1; i++) {
Set dependenciesBetween =
graph
.network()
.edgesConnecting(shortestBindingPath.get(i), shortestBindingPath.get(i + 1));
// If a binding requests a key more than once, any of them should be fine to get to the
// shortest path
dependencyTrace.add((DependencyEdge) Iterables.get(dependenciesBetween, 0));
}
return dependencyTrace.build().reverse();
}
/** Returns all the nonsynthetic dependency requests for a binding. */
public ImmutableSet requests(MaybeBinding binding) {
return graph.network().inEdges(binding).stream()
.flatMap(instancesOf(DependencyEdge.class))
.filter(edge -> edge.dependencyRequest().requestElement().isPresent())
.sorted(requestEnclosingTypeName().thenComparing(requestElementDeclarationOrder()))
.collect(toImmutableSet());
}
/**
* Returns a comparator that sorts entry points in components whose paths from the root are
* shorter first.
*/
private Comparator rootComponentFirst() {
return comparingInt(entryPoint -> source(entryPoint).componentPath().components().size());
}
/**
* Returns a comparator that puts entry points whose shortest dependency path to {@code binding}
* is shortest first.
*/
private Comparator shortestDependencyPathFirst(MaybeBinding binding) {
return comparing(entryPoint -> shortestPathFromEntryPoint(entryPoint, binding).size());
}
private ImmutableList shortestPathFromEntryPoint(
DependencyEdge entryPoint, MaybeBinding binding) {
return shortestPaths
.row(binding)
.computeIfAbsent(
entryPoint,
ep ->
shortestPath(
node ->
filter(graph.network().successors(node), MaybeBinding.class::isInstance),
graph.network().incidentNodes(ep).target(),
binding));
}
/**
* Returns a comparator that sorts entry points in by the distance of the type that declares them
* from the type of the component that contains them.
*
* For instance, an entry point declared directly in the component type would sort before one
* declared in a direct supertype, which would sort before one declared in a supertype of a
* supertype.
*/
private Comparator nearestComponentSupertypeFirst() {
return comparingInt(
entryPoint ->
indexOf(
supertypes.apply(componentContainingEntryPoint(entryPoint)),
equalTo(typeDeclaringEntryPoint(entryPoint))));
}
private XTypeElement componentContainingEntryPoint(DependencyEdge entryPoint) {
return source(entryPoint).componentPath().currentComponent().xprocessing();
}
private XTypeElement typeDeclaringEntryPoint(DependencyEdge entryPoint) {
return asTypeElement(
entryPoint.dependencyRequest().requestElement().get().xprocessing().getEnclosingElement());
}
/**
* Returns a comparator that sorts dependency edges lexicographically by the qualified name of the
* type that contains them. Only appropriate for edges with request elements.
*/
private Comparator requestEnclosingTypeName() {
return comparing(
edge ->
closestEnclosingTypeElement(
edge.dependencyRequest().requestElement().get().xprocessing())
.getQualifiedName());
}
/**
* Returns a comparator that sorts edges in the order in which their request elements were
* declared in their declaring type.
*
* Only useful to compare edges whose request elements were declared in the same type.
*/
private Comparator requestElementDeclarationOrder() {
return comparing(
edge -> edge.dependencyRequest().requestElement().get().xprocessing(),
// TODO(bcorso): This is inefficient as it requires each element to iterate through all of
// its siblings to find its order. Ideally, the order of all elements would be calculated in
// a single pass and cached, but the organization of the current code makes that a bit
// difficult. I'm leaving this for now since this is only called on failures.
comparing(
element -> {
XElement enclosingElement = element.getEnclosingElement();
checkState(isTypeElement(enclosingElement) || isExecutable(enclosingElement));
List extends XElement> siblings =
isTypeElement(enclosingElement)
? asTypeElement(enclosingElement).getEnclosedElements()
// For parameter elements, element.getEnclosingElement().getEnclosedElements()
// is empty, so instead look at the parameter list of the enclosing executable
: asExecutable(enclosingElement).getParameters();
return siblings.indexOf(element);
}));
}
private Node source(Edge edge) {
return graph.network().incidentNodes(edge).source();
}
}