blob: 7d7b9e3a99e218558ad5eebd51288c79ecbab1a3 [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.validation;
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.langmodel.DaggerElements.DECLARATION_ORDER;
import static dagger.internal.codegen.langmodel.DaggerElements.closestEnclosingTypeElement;
import static java.util.Collections.min;
import static java.util.Comparator.comparing;
import static java.util.Comparator.comparingInt;
import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
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.langmodel.DaggerTypes;
import dagger.model.BindingGraph;
import dagger.model.BindingGraph.DependencyEdge;
import dagger.model.BindingGraph.Edge;
import dagger.model.BindingGraph.MaybeBinding;
import dagger.model.BindingGraph.Node;
import dagger.model.ComponentPath;
import java.util.Comparator;
import java.util.Set;
import java.util.function.Function;
import javax.inject.Inject;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
/** Helper class for generating diagnostic messages. */
public final class DiagnosticMessageGenerator {
/** Injectable factory for {@code DiagnosticMessageGenerator}. */
public static final class Factory {
private final DaggerTypes types;
private final DependencyRequestFormatter dependencyRequestFormatter;
private final ElementFormatter elementFormatter;
@Inject
Factory(
DaggerTypes types,
DependencyRequestFormatter dependencyRequestFormatter,
ElementFormatter elementFormatter) {
this.types = types;
this.dependencyRequestFormatter = dependencyRequestFormatter;
this.elementFormatter = elementFormatter;
}
/** Creates a {@code DiagnosticMessageGenerator} for the given binding graph. */
public DiagnosticMessageGenerator create(BindingGraph graph) {
return new DiagnosticMessageGenerator(
graph, types, 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<TypeElement, Iterable<TypeElement>> supertypes;
/** The shortest path (value) from an entry point (column) to a binding (row). */
private final Table<MaybeBinding, DependencyEdge, ImmutableList<Node>> shortestPaths =
HashBasedTable.create();
private static <K, V> Function<K, V> memoize(Function<K, V> 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<K, V> uncachedAsBaseFunction = uncached::apply;
LoadingCache<K, V> cache =
CacheBuilder.newBuilder().build(CacheLoader.from(uncachedAsBaseFunction));
// Second, explicitly convert LoadingCache to j.u.f.Function.
@SuppressWarnings("deprecation") // uncachedAsBaseFunction throws only unchecked exceptions
Function<K, V> memoized = cache::apply;
return memoized;
}
private DiagnosticMessageGenerator(
BindingGraph graph,
DaggerTypes types,
DependencyRequestFormatter dependencyRequestFormatter,
ElementFormatter elementFormatter) {
this.graph = graph;
this.dependencyRequestFormatter = dependencyRequestFormatter;
this.elementFormatter = elementFormatter;
supertypes =
memoize(
component -> transform(types.supertypes(component.asType()), MoreTypes::asTypeElement));
}
public String getMessage(MaybeBinding binding) {
ImmutableSet<DependencyEdge> entryPoints = graph.entryPointEdgesDependingOnBinding(binding);
ImmutableSet<DependencyEdge> requests = requests(binding);
ImmutableList<DependencyEdge> dependencyTrace = dependencyTrace(binding, entryPoints);
return getMessageInternal(dependencyTrace, requests, entryPoints);
}
public String getMessage(DependencyEdge dependencyEdge) {
ImmutableSet<DependencyEdge> requests = ImmutableSet.of(dependencyEdge);
ImmutableSet<DependencyEdge> entryPoints;
ImmutableList<DependencyEdge> 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
dagger.model.Binding binding = (dagger.model.Binding) source(dependencyEdge);
entryPoints = graph.entryPointEdgesDependingOnBinding(binding);
dependencyTrace =
ImmutableList.<DependencyEdge>builder()
.add(dependencyEdge)
.addAll(dependencyTrace(binding, entryPoints))
.build();
}
return getMessageInternal(dependencyTrace, requests, entryPoints);
}
private String getMessageInternal(
ImmutableList<DependencyEdge> dependencyTrace,
ImmutableSet<DependencyEdge> requests,
ImmutableSet<DependencyEdge> entryPoints) {
StringBuilder message =
graph.isFullBindingGraph()
? new StringBuilder()
: new StringBuilder(dependencyTrace.size() * 100 /* a guess heuristic */);
// Print the dependency trace unless it's a full binding graph
if (!graph.isFullBindingGraph()) {
dependencyTrace.forEach(
edge -> dependencyRequestFormatter.appendFormatLine(message, edge.dependencyRequest()));
if (!dependencyTrace.isEmpty()) {
appendComponentPathUnlessAtRoot(message, source(getLast(dependencyTrace)));
}
}
// Print any dependency requests that aren't shown as part of the dependency trace.
ImmutableSet<Element> requestsToPrint =
requests.stream()
// if printing entry points, skip entry points and the traced request
.filter(
request ->
graph.isFullBindingGraph()
|| (!request.isEntryPoint() && !isTracedRequest(dependencyTrace, request)))
.map(request -> request.dependencyRequest().requestElement())
.flatMap(presentValues())
.collect(toImmutableSet());
if (!requestsToPrint.isEmpty()) {
message
.append("\nIt is")
.append(graph.isFullBindingGraph() ? " " : " also ")
.append("requested at:");
elementFormatter.formatIndentedList(message, requestsToPrint, 1);
}
// Print the remaining entry points, showing which component they're in, unless it's a full
// binding graph
if (!graph.isFullBindingGraph() && 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<DependencyEdge> entryPointFormatter =
new Formatter<DependencyEdge>() {
@Override
public String format(DependencyEdge object) {
Element requestElement = object.dependencyRequest().requestElement().get();
StringBuilder element = 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())) {
element.append(String.format(" [%s]", componentPath));
}
return element.toString();
}
};
private static boolean isTracedRequest(
ImmutableList<DependencyEdge> dependencyTrace, DependencyEdge request) {
return !dependencyTrace.isEmpty() && request.equals(dependencyTrace.get(0));
}
/**
* Returns the dependency trace from one of the {@code entryPoints} to {@code binding} to {@code
* message} as a list <i>ending with</i> the entry point.
*/
// TODO(ronshapiro): Adding a DependencyPath type to dagger.model could be useful, i.e.
// bindingGraph.shortestPathFromEntryPoint(DependencyEdge, MaybeBindingNode)
ImmutableList<DependencyEdge> dependencyTrace(
MaybeBinding binding, ImmutableSet<DependencyEdge> 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<Node> shortestBindingPath =
shortestPathFromEntryPoint(entryPointForTrace, binding);
verify(
!shortestBindingPath.isEmpty(),
"no dependency path from %s to %s in %s",
entryPointForTrace,
binding,
graph);
ImmutableList.Builder<DependencyEdge> dependencyTrace = ImmutableList.builder();
dependencyTrace.add(entryPointForTrace);
for (int i = 0; i < shortestBindingPath.size() - 1; i++) {
Set<Edge> 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. */
ImmutableSet<DependencyEdge> 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.
*/
Comparator<DependencyEdge> 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.
*/
Comparator<DependencyEdge> shortestDependencyPathFirst(MaybeBinding binding) {
return comparing(entryPoint -> shortestPathFromEntryPoint(entryPoint, binding).size());
}
ImmutableList<Node> 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.
*
* <p>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.
*/
Comparator<DependencyEdge> nearestComponentSupertypeFirst() {
return comparingInt(
entryPoint ->
indexOf(
supertypes.apply(componentContainingEntryPoint(entryPoint)),
equalTo(typeDeclaringEntryPoint(entryPoint))));
}
TypeElement componentContainingEntryPoint(DependencyEdge entryPoint) {
return source(entryPoint).componentPath().currentComponent();
}
TypeElement typeDeclaringEntryPoint(DependencyEdge entryPoint) {
return MoreElements.asType(
entryPoint.dependencyRequest().requestElement().get().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.
*/
Comparator<DependencyEdge> requestEnclosingTypeName() {
return comparing(
edge ->
closestEnclosingTypeElement(edge.dependencyRequest().requestElement().get())
.getQualifiedName()
.toString());
}
/**
* Returns a comparator that sorts edges in the order in which their request elements were
* declared in their declaring type.
*
* <p>Only useful to compare edges whose request elements were declared in the same type.
*/
Comparator<DependencyEdge> requestElementDeclarationOrder() {
return comparing(edge -> edge.dependencyRequest().requestElement().get(), DECLARATION_ORDER);
}
private Node source(Edge edge) {
return graph.network().incidentNodes(edge).source();
}
}