/*
* Copyright 2010 The Closure Compiler 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 com.github.jsdossier.jscomp;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Multiset;
import com.google.javascript.jscomp.deps.DependencyInfo;
import com.google.javascript.jscomp.deps.SortedDependencies;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
/**
* This is a fork of com.google.javascript.jscomp.deps.ClosureSortedDependencies, which was deleted
* from the Closure Compiler in
* https://github.com/google/closure-compiler/commit/ba594106504e598b43f5b83837e129ea916d920c
*
* TODO(jleyba): Delete this class after updating to a Closure Compiler release that includes the
* above commit.
*
* Original class comment follows.
* -----------------------------------------------------------------------------------------------
*
* A sorted list of inputs with dependency information.
* <p>
* Uses a stable topological sort to make sure that an
* input always comes after its dependencies.
* <p>
* Also exposes other information about the inputs, like which inputs
* do not provide symbols.
*
* @author nicksantos@google.com (Nick Santos)
*/
public final class ClosureSortedDependencies<INPUT extends DependencyInfo>
implements SortedDependencies<INPUT> {
private final List<INPUT> inputs;
// A topologically sorted list of the inputs.
private final List<INPUT> sortedList;
// A list of all the inputs that do not have provides.
private final List<INPUT> noProvides;
private final Map<String, INPUT> provideMap = new HashMap<>();
public ClosureSortedDependencies(List<INPUT> inputs) {
this.inputs = new ArrayList<>(inputs);
noProvides = new ArrayList<>();
// Collect all symbols provided in these files.
for (INPUT input : inputs) {
Collection<String> currentProvides = input.getProvides();
if (currentProvides.isEmpty()) {
noProvides.add(input);
}
for (String provide : currentProvides) {
provideMap.put(provide, input);
}
}
// Get the direct dependencies.
final Multimap<INPUT, INPUT> deps = HashMultimap.create();
for (INPUT input : inputs) {
for (String req : input.getRequires()) {
INPUT dep = provideMap.get(req);
if (dep != null && dep != input) {
deps.put(input, dep);
}
}
}
// Sort the inputs by sucking in 0-in-degree nodes until we're done.
sortedList = topologicalStableSort(inputs, deps);
// The dependency graph of inputs has a cycle iff sortedList is a proper
// subset of inputs. Also, it has a cycle iff the subgraph
// (inputs - sortedList) has a cycle. It's fairly easy to prove this
// by the lemma that a graph has a cycle iff it has a subgraph where
// no nodes have out-degree 0. I'll leave the proof of this as an exercise
// to the reader.
if (sortedList.size() < inputs.size()) {
List<INPUT> subGraph = new ArrayList<>(inputs);
subGraph.removeAll(sortedList);
throw new IllegalArgumentException(
"cycle detected: " + cycleToString(findCycle(subGraph)));
}
}
@Override
public INPUT getInputProviding(String symbol) {
if (provideMap.containsKey(symbol)) {
return provideMap.get(symbol);
}
throw new IllegalArgumentException("Missing provide for " + symbol);
}
@Override
public INPUT maybeGetInputProviding(String symbol) {
return provideMap.get(symbol);
}
/**
* Returns the first circular dependency found. Expressed as a list of
* items in reverse dependency order (the second element depends on the
* first, etc.).
*/
private List<INPUT> findCycle(List<INPUT> subGraph) {
return findCycle(subGraph.get(0), new HashSet<>(subGraph), new HashSet<>());
}
private List<INPUT> findCycle(INPUT current, Set<INPUT> subGraph, Set<INPUT> covered) {
if (covered.add(current)) {
List<INPUT> cycle = findCycle(
findRequireInSubGraphOrFail(current, subGraph),
subGraph, covered);
if (current == cycle.get(0)) {
return cycle;
} else if (cycle.size() == 1 && cycle.get(0) != current) {
cycle.add(current);
// Don't add the input to the list if the cycle has closed already.
} else if (cycle.get(0) != current
&& cycle.get(0) != cycle.get(cycle.size() - 1)) {
if (cycle.get(cycle.size() - 1) != current) {
cycle.add(current);
}
}
return cycle;
} else {
// Explicitly use the add() method, to prevent a generics constructor
// warning that is dumb. The condition it's protecting is
// obscure, and I think people have proposed that it be removed.
List<INPUT> cycle = new ArrayList<>();
cycle.add(current);
return cycle;
}
}
private INPUT findRequireInSubGraphOrFail(INPUT input, Set<INPUT> subGraph) {
for (String symbol : input.getRequires()) {
INPUT candidate = provideMap.get(symbol);
if (subGraph.contains(candidate)) {
return candidate;
}
}
throw new IllegalStateException("no require found in subgraph");
}
/**
* @param cycle A cycle in reverse-dependency order.
*/
private String cycleToString(List<INPUT> cycle) {
List<String> symbols = new ArrayList<>();
for (int i = cycle.size() - 1; i >= 0; i--) {
symbols.add(cycle.get(i).getProvides().iterator().next());
}
symbols.add(symbols.get(0));
return Joiner.on(" -> ").join(symbols);
}
@Override
public List<INPUT> getSortedList() {
return Collections.unmodifiableList(sortedList);
}
/**
* Gets all the dependencies of the given roots. The inputs must be returned
* in a stable order. In other words, if A comes before B, and A does not
* transitively depend on B, then A must also come before B in the returned
* list.
*/
@Override
public List<INPUT> getSortedDependenciesOf(List<INPUT> roots) {
return getDependenciesOf(roots, true);
}
@Override
public List<INPUT> getDependenciesOf(List<INPUT> roots, boolean sorted) {
Preconditions.checkArgument(inputs.containsAll(roots));
Set<INPUT> included = new HashSet<>();
Deque<INPUT> worklist = new ArrayDeque<>(roots);
while (!worklist.isEmpty()) {
INPUT current = worklist.pop();
if (included.add(current)) {
for (String req : current.getRequires()) {
INPUT dep = provideMap.get(req);
if (dep != null) {
worklist.add(dep);
}
}
}
}
ImmutableList.Builder<INPUT> builder = ImmutableList.builder();
for (INPUT current : (sorted ? sortedList : inputs)) {
if (included.contains(current)) {
builder.add(current);
}
}
return builder.build();
}
@Override
public List<INPUT> getInputsWithoutProvides() {
return Collections.unmodifiableList(noProvides);
}
private static <T> List<T> topologicalStableSort(
List<T> items, Multimap<T, T> deps) {
if (items.isEmpty()) {
// Priority queue blows up if we give it a size of 0. Since we need
// to special case this either way, just bail out.
return new ArrayList<>();
}
final Map<T, Integer> originalIndex = new HashMap<>();
for (int i = 0; i < items.size(); i++) {
originalIndex.put(items.get(i), i);
}
PriorityQueue<T> inDegreeZero = new PriorityQueue<>(items.size(),
(a, b) -> originalIndex.get(a) - originalIndex.get(b));
List<T> result = new ArrayList<>();
Multiset<T> inDegree = HashMultiset.create();
Multimap<T, T> reverseDeps = ArrayListMultimap.create();
Multimaps.invertFrom(deps, reverseDeps);
// First, add all the inputs with in-degree 0.
for (T item : items) {
Collection<T> itemDeps = deps.get(item);
inDegree.add(item, itemDeps.size());
if (itemDeps.isEmpty()) {
inDegreeZero.add(item);
}
}
// Then, iterate to a fixed point over the reverse dependency graph.
while (!inDegreeZero.isEmpty()) {
T item = inDegreeZero.remove();
result.add(item);
for (T inWaiting : reverseDeps.get(item)) {
inDegree.remove(inWaiting, 1);
if (inDegree.count(inWaiting) == 0) {
inDegreeZero.add(inWaiting);
}
}
}
return result;
}
}