// Copyright 2016 Pants project contributors (see CONTRIBUTORS.md).
// Licensed under the Apache License, Version 2.0 (see LICENSE).
package com.twitter.intellij.pants.service.project.model.graph;
import com.google.common.collect.Sets;
import com.intellij.openapi.diagnostic.Logger;
import com.twitter.intellij.pants.PantsException;
import com.twitter.intellij.pants.service.project.model.TargetInfo;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
public class BuildGraph {
public static final String ERROR_ORPHANED_NODE = "Missing link in build graph. Orphan nodes: %s";
public static final String ERROR_NO_TARGET_ROOT =
"No target roots found in build graph. Please make sure Pants export version >= 1.0.9";
private static Logger logger = Logger.getInstance("#" + BuildGraph.class.getName());
private Set<BuildGraphNode> allNodes = new HashSet<>();
public class OrphanedNodeException extends PantsException {
public OrphanedNodeException(String message) {
super(message);
}
}
public class NoTargetRootException extends PantsException {
public NoTargetRootException(String message) {
super(message);
}
}
public BuildGraph(Map<String, TargetInfo> targets) {
allNodes.addAll(
targets
.entrySet()
.stream()
.map(BuildGraphNode::new)
.collect(Collectors.toList())
);
// then process their relationships, dependencies and dependees
for (BuildGraphNode node : allNodes) {
Set<String> deps = node.getTargetInfo().getTargets();
for (String dep : deps) {
// FIXME: getNode is currently linear search.
Optional<BuildGraphNode> depNode = getNode(dep);
if (depNode.isPresent()) {
node.addDependency(depNode.get());
depNode.get().addDepeedee(node);
}
else {
logger.error(String.format("No build graph node found for %s", dep));
}
}
}
}
// FIXME: not the most efficient way to find max depth yet.
public int getMaxDepth() {
int depth = 0;
Set<BuildGraphNode> currentNodeSet = getTargetRoots();
while (true) {
Set<BuildGraphNode> dependencyNodes = currentNodeSet.stream()
.map(BuildGraphNode::getDependencies)
.flatMap(Set::stream)
.collect(Collectors.toSet());
int sizeBeforeAdd = currentNodeSet.size();
currentNodeSet.addAll(dependencyNodes);
if (currentNodeSet.size() == sizeBeforeAdd) {
if (currentNodeSet.size() == allNodes.size()) {
return depth;
}
else {
Set<BuildGraphNode> orphanNodes = Sets.difference(allNodes, currentNodeSet);
throw new OrphanedNodeException(String.format(ERROR_ORPHANED_NODE, orphanNodes));
}
}
depth++;
}
}
// level 0 - target roots
// level 1 - target roots + direct deps
// ...
public Set<BuildGraphNode> getNodesUpToLevel(int level) {
// Holds the current scope of build graph.
Set<BuildGraphNode> results = getTargetRoots();
for (int i = 0; i < level; i++) {
Set<BuildGraphNode> dependencies = new HashSet<>();
for (BuildGraphNode node : results) {
dependencies.addAll(node.getDependencies());
}
results.addAll(dependencies);
// All nodes are in, no need to iterate more.
if (results.size() == allNodes.size()) {
break;
}
}
return results;
}
private Set<BuildGraphNode> getTargetRoots() {
Set<BuildGraphNode> targetRoots = allNodes.stream().filter(BuildGraphNode::isTargetRoot).collect(Collectors.toSet());
if (targetRoots.isEmpty()) {
throw new NoTargetRootException(ERROR_NO_TARGET_ROOT);
}
return targetRoots;
}
private Optional<BuildGraphNode> getNode(String targetAddress) {
return allNodes.stream()
.filter(n -> n.getAddress().equals(targetAddress))
.findFirst();
}
}