com.google.javascript.jscomp.JSModuleGraph.java Source code

Java tutorial

Introduction

Here is the source code for com.google.javascript.jscomp.JSModuleGraph.java

Source

/*
 * 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 com.google.common.annotations.GwtIncompatible;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.javascript.jscomp.deps.ClosureSortedDependencies;
import com.google.javascript.jscomp.deps.Es6SortedDependencies;
import com.google.javascript.jscomp.deps.SortedDependencies;
import com.google.javascript.jscomp.deps.SortedDependencies.CircularDependencyException;
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.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

/**
 * 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 {

    private List<JSModule> modules;

    /**
     * Lists of modules at each depth. <code>modulesByDepth.get(3)</code> is a
     * list of the modules at depth 3, for example.
     */
    private List<List<JSModule>> 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.
     *
     * dependencyMap should be filled from leaf to root so that
     * getTransitiveDepsDeepestFirst can use its results directly.
     */
    private Map<JSModule, Set<JSModule>> dependencyMap = new HashMap<>();

    /**
     * Creates a module graph from a list of modules in dependency order.
     */
    public JSModuleGraph(JSModule[] modulesInDepOrder) {
        this(ImmutableList.copyOf(modulesInDepOrder));
    }

    /**
     * Creates a module graph from a list of modules in dependency order.
     */
    public JSModuleGraph(List<JSModule> modulesInDepOrder) {
        Preconditions.checkState(modulesInDepOrder.size() == new HashSet<>(modulesInDepOrder).size(),
                "Found duplicate modules");
        modules = ImmutableList.copyOf(modulesInDepOrder);
        modulesByDepth = new ArrayList<>();

        for (JSModule module : modulesInDepOrder) {
            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 == modulesByDepth.size()) {
                modulesByDepth.add(new ArrayList<JSModule>());
            }
            modulesByDepth.get(depth).add(module);
        }
    }

    /**
     * Gets an iterable over all modules in dependency order.
     */
    Iterable<JSModule> getAllModules() {
        return modules;
    }

    /**
     * Gets all modules indexed by name.
     */
    Map<String, JSModule> getModulesByName() {
        Map<String, JSModule> result = new HashMap<>();
        for (JSModule m : modules) {
            result.put(m.getName(), m);
        }
        return result;
    }

    /**
     * Gets the total number of modules.
     */
    int getModuleCount() {
        return modules.size();
    }

    /**
     * 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();
            try {
                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);
            } catch (JsonParseException e) {
                Throwables.propagate(e);
            }
        }
        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) {
        Set<JSModule> deps = dependencyMap.get(src);
        if (deps == null) {
            deps = getTransitiveDepsDeepestFirst(src);
            dependencyMap.put(src, deps);
        }

        return deps.contains(m);
    }

    /**
     * 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<JSModule> 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<JSModule> modules) {
        Iterator<JSModule> 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}
     */
    Set<JSModule> getTransitiveDepsDeepestFirst(JSModule m) {
        Set<JSModule> deps = dependencyMap.get(m);
        if (deps != null) {
            return deps;
        }
        deps = new TreeSet<>(new InverseDepthComparator());
        addDeps(deps, m);
        dependencyMap.put(m, deps);
        return deps;
    }

    /**
     * Adds a module's transitive dependencies to a set.
     */
    private static void addDeps(Set<JSModule> deps, JSModule m) {
        for (JSModule dep : m.getDependencies()) {
            deps.add(dep);
            addDeps(deps, dep);
        }
    }

    /**
     * Replaces any files that are found multiple times with a single instance in
     * the closest parent module that is common to all modules where it appears.
     *
     * JSCompiler normally errors if you attempt to compile modules containing the
     * same file.  This method can be used to remove duplicates before compiling
     * to avoid such an error.
     */
    public void coalesceDuplicateFiles() {
        Multimap<String, JSModule> fileRefs = LinkedHashMultimap.create();
        for (JSModule module : modules) {
            for (CompilerInput jsFile : module.getInputs()) {
                fileRefs.put(jsFile.getName(), module);
            }
        }

        for (String path : fileRefs.keySet()) {
            Collection<JSModule> refModules = fileRefs.get(path);
            if (refModules.size() > 1) {
                JSModule depModule = getDeepestCommonDependencyInclusive(refModules);
                CompilerInput file = refModules.iterator().next().getByName(path);
                for (JSModule module : refModules) {
                    if (module != depModule) {
                        module.removeByName(path);
                    }
                }
                if (!refModules.contains(depModule)) {
                    depModule.add(file);
                }
            }
        }
    }

    /**
     * Applies a DependencyOptions in "dependency sorting" and "dependency pruning"
     * mode to the given list of inputs. Returns a new list with the files sorted
     * and removed. This module graph will be updated to reflect the new list.
     *
     * If you need more fine-grained dependency management, you should create your
     * own DependencyOptions and call
     * {@code manageDependencies(DependencyOptions, List<CompilerInput>)}.
     *
     * @param entryPoints The entry points into the program.
     *     Expressed as JS symbols.
     * @param inputs The original list of sources. Used to ensure that the sort
     *     is stable.
     * @throws CircularDependencyException if there is a circular dependency
     *     between the provides and requires.
     * @throws MissingProvideException if an entry point was not provided
     *     by any of the inputs.
     * @see DependencyOptions for more info on how this works.
     */
    public List<CompilerInput> manageDependencies(List<String> entryPoints, List<CompilerInput> inputs)
            throws CircularDependencyException, MissingModuleException, MissingProvideException {
        DependencyOptions depOptions = new DependencyOptions();
        depOptions.setDependencySorting(true);
        depOptions.setDependencyPruning(true);
        depOptions.setEntryPoints(entryPoints);
        return manageDependencies(depOptions, inputs);
    }

    /**
     * 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 CircularDependencyException if there is a circular dependency
     *     between the provides and requires.
     * @throws MissingProvideException if an entry point was not provided
     *     by any of the inputs.
     * @see DependencyOptions for more info on how this works.
     */
    public List<CompilerInput> manageDependencies(DependencyOptions depOptions, List<CompilerInput> inputs)
            throws CircularDependencyException, MissingProvideException, MissingModuleException {

        SortedDependencies<CompilerInput> sorter = depOptions.isEs6ModuleOrder()
                ? new Es6SortedDependencies<>(inputs)
                : new ClosureSortedDependencies<>(inputs);

        Iterable<CompilerInput> entryPointInputs = createEntryPointInputs(depOptions, inputs, sorter);

        // The order of inputs, sorted independently of modules.
        List<CompilerInput> absoluteOrder = sorter.getDependenciesOf(inputs, depOptions.shouldSortDependencies());

        // Figure out which sources *must* be in each module.
        ListMultimap<JSModule, CompilerInput> entryPointInputsPerModule = LinkedListMultimap.create();
        for (CompilerInput input : entryPointInputs) {
            JSModule module = input.getModule();
            Preconditions.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.
        for (JSModule module : entryPointInputsPerModule.keySet()) {
            List<CompilerInput> 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));
                }
            }
        }

        // All the inputs are pointing to the modules that own them. Yeah!
        // Update the modules to reflect this.
        for (CompilerInput input : absoluteOrder) {
            JSModule module = input.getModule();
            if (module != null) {
                module.add(input);
            }
        }

        // Now, generate the sorted result.
        ImmutableList.Builder<CompilerInput> result = ImmutableList.builder();
        for (JSModule module : getAllModules()) {
            result.addAll(module.getInputs());
        }

        return result.build();
    }

    private Collection<CompilerInput> createEntryPointInputs(DependencyOptions depOptions,
            List<CompilerInput> inputs, SortedDependencies<CompilerInput> sorter)
            throws MissingModuleException, MissingProvideException {
        Set<CompilerInput> entryPointInputs = new LinkedHashSet<>();
        Map<String, JSModule> modulesByName = getModulesByName();

        if (depOptions.shouldPruneDependencies()) {
            if (!depOptions.shouldDropMoochers()) {
                entryPointInputs.addAll(sorter.getInputsWithoutProvides());
            }

            for (String entryPoint : depOptions.getEntryPoints()) {
                // An entry point is either formatted as:
                // 'foo.bar' - peg foo.bar to its current module
                // 'modC:foo.bar' - peg foo.bar to modC
                String inputName = entryPoint;
                int splitPoint = entryPoint.indexOf(':');
                CompilerInput entryPointInput = null;
                if (splitPoint != -1) {
                    String moduleName = entryPoint.substring(0, splitPoint);
                    inputName = entryPoint.substring(Math.min(splitPoint + 1, entryPoint.length() - 1));
                    JSModule module = modulesByName.get(moduleName);
                    if (module == null) {
                        throw new MissingModuleException(moduleName);
                    } else {
                        entryPointInput = sorter.getInputProviding(inputName);
                        entryPointInput.overrideModule(module);
                    }
                } else {
                    entryPointInput = sorter.getInputProviding(inputName);
                }

                entryPointInputs.add(entryPointInput);
            }

            CompilerInput baseJs = sorter.maybeGetInputProviding("goog");
            if (baseJs != null) {
                entryPointInputs.add(baseJs);
            }
        } else {
            entryPointInputs.addAll(inputs);
        }
        return entryPointInputs;
    }

    @SuppressWarnings("unused")
    LinkedDirectedGraph<JSModule, String> toGraphvizGraph() {
        LinkedDirectedGraph<JSModule, String> 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 class InverseDepthComparator implements Comparator<JSModule> {
        @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);
        }
    }

}