All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.JSModuleGraph Maven / Gradle / Ivy

There is a newer version: 9.0.8
Show newest version
/*
 * Copyright 2008 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.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Ordering;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.javascript.jscomp.deps.Es6SortedDependencies;
import com.google.javascript.jscomp.deps.SortedDependencies;
import com.google.javascript.jscomp.deps.SortedDependencies.MissingProvideException;
import com.google.javascript.jscomp.graph.LinkedDirectedGraph;
import com.google.javascript.jscomp.parsing.parser.util.format.SimpleFormat;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A {@link JSModule} dependency graph that assigns a depth to each module and can answer
 * depth-related queries about them. For the purposes of this class, a module's depth is defined as
 * the number of hops in the longest (non cyclic) path from the module to a module with no
 * dependencies.
 */
public final class JSModuleGraph implements Serializable {

  private final JSModule[] modules;

  /**
   * selfPlusTransitiveDeps[i] = indices of all modules that modules[i] depends on, including
   * itself.
   */
  private final BitSet[] selfPlusTransitiveDeps;

  /**
   * subtreeSize[i] = Number of modules that transitively depend on modules[i], including itself.
   */
  private final int[] subtreeSize;

  /**
   * Lists of modules at each depth. modulesByDepth.get(3) is a list of the modules at
   * depth 3, for example.
   */
  private final List> modulesByDepth;

  /**
   * dependencyMap is a cache of dependencies that makes the dependsOn function faster. Each map
   * entry associates a starting JSModule with the set of JSModules that are transitively dependent
   * on the starting module.
   *
   * 

If the cache returns null, then the entry hasn't been filled in for that module. * *

NOTE: JSModule has identity semantics so this map implementation is safe */ private final Map> dependencyMap = new IdentityHashMap<>(); /** Creates a module graph from a list of modules in dependency order. */ public JSModuleGraph(JSModule[] modulesInDepOrder) { this(Arrays.asList(modulesInDepOrder)); } /** Creates a module graph from a list of modules in dependency order. */ public JSModuleGraph(List modulesInDepOrder) { modules = new JSModule[modulesInDepOrder.size()]; // n = number of modules // Populate modules O(n) for (int moduleIndex = 0; moduleIndex < modules.length; ++moduleIndex) { final JSModule module = modulesInDepOrder.get(moduleIndex); checkState(module.getIndex() == -1, "Module index already set: %s", module); module.setIndex(moduleIndex); modules[moduleIndex] = module; } // Determine depth for all modules. // m = number of edges in the graph // O(n*m) modulesByDepth = initModulesByDepth(); // Determine transitive deps for all modules. // O(n*m * log(n)) (probably a bit better than that) selfPlusTransitiveDeps = initTransitiveDepsBitSets(); // O(n*m) subtreeSize = initSubtreeSize(); } private List> initModulesByDepth() { final List> tmpModulesByDepth = new ArrayList<>(); for (int moduleIndex = 0; moduleIndex < modules.length; ++moduleIndex) { final JSModule module = modules[moduleIndex]; checkState(module.getDepth() == -1, "Module depth already set: %s", module); int depth = 0; for (JSModule dep : module.getDependencies()) { int depDepth = dep.getDepth(); if (depDepth < 0) { throw new ModuleDependenceException(SimpleFormat.format( "Modules not in dependency order: %s preceded %s", module.getName(), dep.getName()), module, dep); } depth = Math.max(depth, depDepth + 1); } module.setDepth(depth); if (depth == tmpModulesByDepth.size()) { tmpModulesByDepth.add(new ArrayList()); } tmpModulesByDepth.get(depth).add(module); } return tmpModulesByDepth; } private BitSet[] initTransitiveDepsBitSets() { BitSet[] array = new BitSet[modules.length]; for (int moduleIndex = 0; moduleIndex < modules.length; ++moduleIndex) { final JSModule module = modules[moduleIndex]; BitSet selfPlusTransitiveDeps = new BitSet(moduleIndex + 1); array[moduleIndex] = selfPlusTransitiveDeps; selfPlusTransitiveDeps.set(moduleIndex); // O(moduleIndex * log64(moduleIndex)) for (JSModule dep : module.getDependencies()) { // Add this dependency and all of its dependencies to the current module. // O(log64(moduleIndex)) selfPlusTransitiveDeps.or(array[dep.getIndex()]); } } return array; } private int[] initSubtreeSize() { int[] subtreeSize = new int[modules.length]; for (int dependentIndex = 0; dependentIndex < modules.length; ++dependentIndex) { BitSet dependencies = selfPlusTransitiveDeps[dependentIndex]; // Iterating backward through the bitset is slightly more efficient, since it avoids // considering later modules, which this one cannot depend on. for (int requiredIndex = dependentIndex; requiredIndex >= 0; requiredIndex = dependencies.previousSetBit(requiredIndex - 1)) { subtreeSize[requiredIndex] += 1; // Count dependent in required module's subtree. } } return subtreeSize; } /** * This only exists as a temprorary workaround. * @deprecated Fix the tests that use this. */ @Deprecated public void breakThisGraphSoItsModulesCanBeReused() { for (JSModule m : modules) { m.resetThisModuleSoItCanBeReused(); } } /** * Gets an iterable over all modules in dependency order. */ Iterable getAllModules() { return Arrays.asList(modules); } /** * Gets all modules indexed by name. */ Map getModulesByName() { Map result = new HashMap<>(); for (JSModule m : modules) { result.put(m.getName(), m); } return result; } /** * Gets the total number of modules. */ int getModuleCount() { return modules.length; } /** * Gets the root module. */ JSModule getRootModule() { return Iterables.getOnlyElement(modulesByDepth.get(0)); } /** * Returns a JSON representation of the JSModuleGraph. Specifically a * JsonArray of "Modules" where each module has a * - "name" * - "dependencies" (list of module names) * - "transitive-dependencies" (list of module names, deepest first) * - "inputs" (list of file names) * @return List of module JSONObjects. */ @GwtIncompatible("com.google.gson") JsonArray toJson() { JsonArray modules = new JsonArray(); for (JSModule module : getAllModules()) { JsonObject node = new JsonObject(); node.add("name", new JsonPrimitive(module.getName())); JsonArray deps = new JsonArray(); node.add("dependencies", deps); for (JSModule m : module.getDependencies()) { deps.add(new JsonPrimitive(m.getName())); } JsonArray transitiveDeps = new JsonArray(); node.add("transitive-dependencies", transitiveDeps); for (JSModule m : getTransitiveDepsDeepestFirst(module)) { transitiveDeps.add(new JsonPrimitive(m.getName())); } JsonArray inputs = new JsonArray(); node.add("inputs", inputs); for (CompilerInput input : module.getInputs()) { inputs.add(new JsonPrimitive( input.getSourceFile().getOriginalPath())); } modules.add(node); } return modules; } /** * Determines whether this module depends on a given module. Note that a * module never depends on itself, as that dependency would be cyclic. */ public boolean dependsOn(JSModule src, JSModule m) { return src != m && selfPlusTransitiveDeps[src.getIndex()].get(m.getIndex()); } /** * Finds the module with the fewest transitive dependents on which all of the given modules depend * and that is a subtree of the given parent module tree. * *

If no such subtree can be found, the parent module is returned. * *

If multiple candidates have the same number of dependents, the module farthest down in the * total ordering of modules will be chosen. * * @param parentTree module on which the result must depend * @param dependentModules indices of modules to consider * @return A module on which all of the argument modules depend */ public JSModule getSmallestCoveringSubtree(JSModule parentTree, BitSet dependentModules) { checkState(!dependentModules.isEmpty()); // Candidate modules are those that all of the given dependent modules depend on, including // themselves. The dependent module with the smallest index might be our answer, if all // the other modules depend on it. int minDependentModuleIndex = modules.length; final BitSet candidates = new BitSet(modules.length); candidates.set(0, modules.length, true); for (int dependentIndex = dependentModules.nextSetBit(0); dependentIndex >= 0; dependentIndex = dependentModules.nextSetBit(dependentIndex + 1)) { minDependentModuleIndex = Math.min(minDependentModuleIndex, dependentIndex); candidates.and(selfPlusTransitiveDeps[dependentIndex]); } checkState( !candidates.isEmpty(), "No common dependency found for %s", dependentModules); // All candidates must have an index <= the smallest dependent module index. // Work backwards through the candidates starting with the dependent module with the smallest // index. For each candidate, we'll remove all of the modules it depends on from consideration, // since they must all have larger subtrees than the one we're considering. int parentTreeIndex = parentTree.getIndex(); // default to parent tree if we don't find anything better int bestCandidateIndex = parentTreeIndex; for (int candidateIndex = candidates.previousSetBit(minDependentModuleIndex); candidateIndex >= 0; candidateIndex = candidates.previousSetBit(candidateIndex - 1)) { BitSet candidatePlusTransitiveDeps = selfPlusTransitiveDeps[candidateIndex]; if (candidatePlusTransitiveDeps.get(parentTreeIndex)) { // candidate is a subtree of parentTree candidates.andNot(candidatePlusTransitiveDeps); if (subtreeSize[candidateIndex] < subtreeSize[bestCandidateIndex]) { bestCandidateIndex = candidateIndex; } } // eliminate candidates that are not a subtree of parentTree } return modules[bestCandidateIndex]; } /** * Finds the deepest common dependency of two modules, not including the two * modules themselves. * * @param m1 A module in this graph * @param m2 A module in this graph * @return The deepest common dep of {@code m1} and {@code m2}, or null if * they have no common dependencies */ JSModule getDeepestCommonDependency(JSModule m1, JSModule m2) { int m1Depth = m1.getDepth(); int m2Depth = m2.getDepth(); // According our definition of depth, the result must have a strictly // smaller depth than either m1 or m2. for (int depth = Math.min(m1Depth, m2Depth) - 1; depth >= 0; depth--) { List modulesAtDepth = modulesByDepth.get(depth); // Look at the modules at this depth in reverse order, so that we use the // original ordering of the modules to break ties (later meaning deeper). for (int i = modulesAtDepth.size() - 1; i >= 0; i--) { JSModule m = modulesAtDepth.get(i); if (dependsOn(m1, m) && dependsOn(m2, m)) { return m; } } } return null; } /** * Finds the deepest common dependency of two modules, including the * modules themselves. * * @param m1 A module in this graph * @param m2 A module in this graph * @return The deepest common dep of {@code m1} and {@code m2}, or null if * they have no common dependencies */ public JSModule getDeepestCommonDependencyInclusive( JSModule m1, JSModule m2) { if (m2 == m1 || dependsOn(m2, m1)) { return m1; } else if (dependsOn(m1, m2)) { return m2; } return getDeepestCommonDependency(m1, m2); } /** Returns the deepest common dependency of the given modules. */ public JSModule getDeepestCommonDependencyInclusive( Collection modules) { Iterator iter = modules.iterator(); JSModule dep = iter.next(); while (iter.hasNext()) { dep = getDeepestCommonDependencyInclusive(dep, iter.next()); } return dep; } /** * Creates an iterable over the transitive dependencies of module {@code m} * in a non-increasing depth ordering. The result does not include the module * {@code m}. * * @param m A module in this graph * @return The transitive dependencies of module {@code m} */ @VisibleForTesting List getTransitiveDepsDeepestFirst(JSModule m) { return InverseDepthComparator.INSTANCE.sortedCopy(getTransitiveDeps(m)); } /** Returns the transitive dependencies of the module. */ private Set getTransitiveDeps(JSModule m) { Set deps = dependencyMap.get(m); if (deps == null) { deps = m.getAllDependencies(); dependencyMap.put(m, deps); } return deps; } /** * Apply the dependency options to the list of sources, returning a new source list re-ordering * and dropping files as necessary. This module graph will be updated to reflect the new list. * * @param inputs The original list of sources. Used to ensure that the sort is stable. * @throws MissingProvideException if an entry point was not provided by any of the inputs. * @see DependencyOptions for more info on how this works. */ public ImmutableList manageDependencies( DependencyOptions depOptions, List inputs) throws MissingProvideException, MissingModuleException { SortedDependencies sorter = new Es6SortedDependencies<>(inputs); Set entryPointInputs = createEntryPointInputs(depOptions, inputs, sorter); HashMap inputsByProvide = new HashMap<>(); for (CompilerInput input : inputs) { for (String provide : input.getKnownProvides()) { inputsByProvide.put(provide, input); } String moduleName = input.getPath().toModuleName(); if (!inputsByProvide.containsKey(moduleName)) { inputsByProvide.put(moduleName, input); } } // Dynamically imported files must be added to the module graph, but // they should not be ordered ahead of the files that import them. // We add them as entry points to ensure they get included. for (CompilerInput input : inputs) { for (String require : input.getDynamicRequires()) { if (inputsByProvide.containsKey(require)) { entryPointInputs.add(inputsByProvide.get(require)); } } } // The order of inputs, sorted independently of modules. List absoluteOrder = sorter.getDependenciesOf(inputs, depOptions.shouldSortDependencies()); // Figure out which sources *must* be in each module. ListMultimap entryPointInputsPerModule = LinkedListMultimap.create(); for (CompilerInput input : entryPointInputs) { JSModule module = input.getModule(); checkNotNull(module); entryPointInputsPerModule.put(module, input); } // Clear the modules of their inputs. This also nulls out // the input's reference to its module. for (JSModule module : getAllModules()) { module.removeAll(); } // Figure out which sources *must* be in each module, or in one // of that module's dependencies. List orderedInputs = new ArrayList<>(); Set reachedInputs = new HashSet<>(); for (JSModule module : entryPointInputsPerModule.keySet()) { List transitiveClosure; // Prefer a depth first ordering of dependencies from entry points. // Always orders in a deterministic fashion regardless of the order of provided inputs // given the same entry points in the same order. if (depOptions.shouldSortDependencies() && depOptions.shouldPruneDependencies()) { transitiveClosure = new ArrayList<>(); // We need the ful set of dependencies for each module, so start with the full input set Set inputsNotYetReached = new HashSet<>(inputs); for (CompilerInput entryPoint : entryPointInputsPerModule.get(module)) { transitiveClosure.addAll( getDepthFirstDependenciesOf(entryPoint, inputsNotYetReached, inputsByProvide)); } // For any input we have not yet reached, add them to the ordered list for (CompilerInput orderedInput : transitiveClosure) { if (reachedInputs.add(orderedInput)) { orderedInputs.add(orderedInput); } } } else { // Simply order inputs so that any required namespace comes before it's usage. // Ordered result varies based on the original order of inputs. transitiveClosure = sorter.getDependenciesOf( entryPointInputsPerModule.get(module), depOptions.shouldSortDependencies()); } for (CompilerInput input : transitiveClosure) { JSModule oldModule = input.getModule(); if (oldModule == null) { input.setModule(module); } else { input.setModule(null); input.setModule( getDeepestCommonDependencyInclusive(oldModule, module)); } } } if (!(depOptions.shouldSortDependencies() && depOptions.shouldPruneDependencies()) || entryPointInputsPerModule.isEmpty()) { orderedInputs = absoluteOrder; } // All the inputs are pointing to the modules that own them. Yeah! // Update the modules to reflect this. for (CompilerInput input : orderedInputs) { JSModule module = input.getModule(); if (module != null) { module.add(input); } } // Now, generate the sorted result. ImmutableList.Builder result = ImmutableList.builder(); for (JSModule module : getAllModules()) { result.addAll(module.getInputs()); } return result.build(); } /** * Given an input and set of unprocessed inputs, return the input and it's dependencies by * performing a recursive, depth-first traversal. */ private List getDepthFirstDependenciesOf( CompilerInput rootInput, Set unreachedInputs, Map inputsByProvide) { List orderedInputs = new ArrayList<>(); if (!unreachedInputs.remove(rootInput)) { return orderedInputs; } for (String importedNamespace : rootInput.getRequires()) { CompilerInput dependency = null; if (inputsByProvide.containsKey(importedNamespace) && unreachedInputs.contains(inputsByProvide.get(importedNamespace))) { dependency = inputsByProvide.get(importedNamespace); } if (dependency != null) { orderedInputs.addAll( getDepthFirstDependenciesOf(dependency, unreachedInputs, inputsByProvide)); } } orderedInputs.add(rootInput); return orderedInputs; } private Set createEntryPointInputs( DependencyOptions depOptions, List inputs, SortedDependencies sorter) throws MissingModuleException, MissingProvideException { Set entryPointInputs = new LinkedHashSet<>(); Map modulesByName = getModulesByName(); if (depOptions.shouldPruneDependencies()) { // Some files implicitly depend on base.js without actually requiring anything. // So we always treat it as the first entry point to ensure it's ordered correctly. CompilerInput baseJs = sorter.maybeGetInputProviding("goog"); if (baseJs != null) { entryPointInputs.add(baseJs); } if (!depOptions.shouldDropMoochers()) { entryPointInputs.addAll(sorter.getInputsWithoutProvides()); } for (ModuleIdentifier entryPoint : depOptions.getEntryPoints()) { CompilerInput entryPointInput = null; try { if (entryPoint.getClosureNamespace().equals(entryPoint.getModuleName())) { entryPointInput = sorter.maybeGetInputProviding(entryPoint.getClosureNamespace()); // Check to see if we can find the entry point as an ES6 and CommonJS module // ES6 and CommonJS entry points may not provide any symbols if (entryPointInput == null) { entryPointInput = sorter.getInputProviding(entryPoint.getName()); } } else { JSModule module = modulesByName.get(entryPoint.getModuleName()); if (module == null) { throw new MissingModuleException(entryPoint.getModuleName()); } else { entryPointInput = sorter.getInputProviding(entryPoint.getClosureNamespace()); entryPointInput.overrideModule(module); } } } catch (MissingProvideException e) { throw new MissingProvideException(entryPoint.getName(), e); } entryPointInputs.add(entryPointInput); } } else { entryPointInputs.addAll(inputs); } return entryPointInputs; } @SuppressWarnings("unused") LinkedDirectedGraph toGraphvizGraph() { LinkedDirectedGraph graphViz = LinkedDirectedGraph.create(); for (JSModule module : getAllModules()) { graphViz.createNode(module); for (JSModule dep : module.getDependencies()) { graphViz.createNode(dep); graphViz.connect(module, "->", dep); } } return graphViz; } /** * A module depth comparator that considers a deeper module to be "less than" * a shallower module. Uses module names to consistently break ties. */ private static final class InverseDepthComparator extends Ordering { static final InverseDepthComparator INSTANCE = new InverseDepthComparator(); @Override public int compare(JSModule m1, JSModule m2) { return depthCompare(m2, m1); } } private static int depthCompare(JSModule m1, JSModule m2) { if (m1 == m2) { return 0; } int d1 = m1.getDepth(); int d2 = m2.getDepth(); return d1 < d2 ? -1 : d2 == d1 ? m1.getName().compareTo(m2.getName()) : 1; } /** * Exception class for declaring when the modules being fed into a * JSModuleGraph as input aren't in dependence order, and so can't be * processed for caching of various dependency-related queries. */ protected static class ModuleDependenceException extends IllegalArgumentException { private static final long serialVersionUID = 1; private final JSModule module; private final JSModule dependentModule; protected ModuleDependenceException(String message, JSModule module, JSModule dependentModule) { super(message); this.module = module; this.dependentModule = dependentModule; } public JSModule getModule() { return module; } public JSModule getDependentModule() { return dependentModule; } } /** Another exception class */ public static class MissingModuleException extends Exception { MissingModuleException(String moduleName) { super(moduleName); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy