com.google.javascript.jscomp.deps.DepsGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.google.javascript.jscomp.deps.DepsGenerator.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.deps;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.javascript.jscomp.CheckLevel;
import com.google.javascript.jscomp.DiagnosticType;
import com.google.javascript.jscomp.ErrorManager;
import com.google.javascript.jscomp.JSError;
import com.google.javascript.jscomp.SourceFile;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

/**
 * Generates deps.js files by scanning JavaScript files for
 * calls to goog.provide(), goog.require() and goog.addDependency().
 *
 * @author agrieve@google.com (Andrew Grieve)
 */
public class DepsGenerator {

    public static enum InclusionStrategy {
        ALWAYS, WHEN_IN_SRCS, DO_NOT_DUPLICATE
    }

    private static Logger logger = Logger.getLogger(DepsGenerator.class.getName());

    // See the Flags in MakeJsDeps for descriptions of these.
    private final Collection<SourceFile> srcs;
    private final Collection<SourceFile> deps;
    private final String closurePathAbs;
    private final InclusionStrategy mergeStrategy;
    final ErrorManager errorManager;

    static final DiagnosticType SAME_FILE_WARNING = DiagnosticType.warning("DEPS_SAME_FILE",
            "Namespace \"{0}\" is both required and provided in the same file.");

    static final DiagnosticType NEVER_PROVIDED_ERROR = DiagnosticType.error("DEPS_NEVER_PROVIDED",
            "Namespace \"{0}\" is required but never provided.");

    static final DiagnosticType DUPE_PROVIDES_WARNING = DiagnosticType.warning("DEPS_DUPE_PROVIDES",
            "Multiple calls to goog.provide(\"{0}\")");

    static final DiagnosticType MULTIPLE_PROVIDES_ERROR = DiagnosticType.error("DEPS_DUPE_PROVIDES",
            "Namespace \"{0}\" is already provided in other file {1}");

    static final DiagnosticType DUPE_REQUIRE_WARNING = DiagnosticType.warning("DEPS_DUPE_REQUIRES",
            "Namespace \"{0}\" is required multiple times");

    static final DiagnosticType NO_DEPS_WARNING = DiagnosticType.warning("DEPS_NO_DEPS",
            "No dependencies found in file");

    /**
     * Creates a new DepsGenerator.
     */
    public DepsGenerator(Collection<SourceFile> deps, Collection<SourceFile> srcs, InclusionStrategy mergeStrategy,
            String closurePathAbs, ErrorManager errorManager) {
        this.deps = deps;
        this.srcs = srcs;
        this.mergeStrategy = mergeStrategy;
        this.closurePathAbs = closurePathAbs;
        this.errorManager = errorManager;
    }

    /**
     * Performs the parsing inputs and writing of outputs.
     * @throws IOException Occurs upon an IO error.
     * @return Returns a String of goog.addDependency calls that will build
     *     the dependency graph. Returns null if there was an error.
     */
    public String computeDependencyCalls() throws IOException {
        // Build a map of closure-relative path -> DepInfo.
        Map<String, DependencyInfo> depsFiles = parseDepsFiles();
        logger.fine("preparsedFiles: " + depsFiles);

        // Find all goog.provides & goog.requires in src files
        Map<String, DependencyInfo> jsFiles = parseSources(depsFiles.keySet());

        // Check if there were any parse errors.
        if (errorManager.getErrorCount() > 0) {
            return null;
        }

        cleanUpDuplicatedFiles(depsFiles, jsFiles);

        // Check for missing provides or other semantic inconsistencies.
        validateDependencies(depsFiles.values(), jsFiles.values());

        if (errorManager.getErrorCount() > 0) {
            return null;
        }

        ByteArrayOutputStream output = new ByteArrayOutputStream();
        writeDepsContent(depsFiles, jsFiles, new PrintStream(output));
        return new String(output.toByteArray(), UTF_8);
    }

    /**
     * Removes duplicated depsInfo from jsFiles if this info already present in
     * some of the parsed deps.js
     *
     * @param depsFiles DepsInfo from deps.js dependencies
     * @param jsFiles DepsInfo from some of jsSources
     */
    protected void cleanUpDuplicatedFiles(Map<String, DependencyInfo> depsFiles,
            Map<String, DependencyInfo> jsFiles) {
        Set<String> depsPathsCopy = new HashSet<>(depsFiles.keySet());
        for (String path : depsPathsCopy) {
            if (mergeStrategy != InclusionStrategy.WHEN_IN_SRCS) {
                jsFiles.remove(path);
            }
        }

        for (String path : jsFiles.keySet()) {
            // If a generated file appears in both the jsFiles and in depsFiles, then
            // remove it from depsFiles in order to get the full path the generated
            // file.
            depsFiles.remove(path);
        }
    }

    /**
     * Reports if there are any dependency problems with the given dependency
     * information. Reported problems include:
     *     - A namespace being provided more than once
     *     - A namespace being required multiple times from within one file
     *     - A namespace being provided and required in the same file
     *     - A namespace being required that is never provided
     * @param preparsedFileDepedencies Dependency information from existing
     *     deps.js files.
     * @param parsedFileDependencies Dependency information from parsed .js files.
     */
    private void validateDependencies(Iterable<DependencyInfo> preparsedFileDepedencies,
            Iterable<DependencyInfo> parsedFileDependencies) {
        // Create a map of namespace -> file providing it.
        // Also report any duplicate provides.
        Map<String, DependencyInfo> providesMap = new HashMap<>();
        addToProvideMap(preparsedFileDepedencies, providesMap);
        addToProvideMap(parsedFileDependencies, providesMap);
        // For each require in the parsed sources:
        for (DependencyInfo depInfo : parsedFileDependencies) {
            List<String> requires = new ArrayList<>(depInfo.getRequires());
            for (int i = 0, l = requires.size(); i < l; ++i) {
                String namespace = requires.get(i);
                // Check for multiple requires.
                if (requires.subList(i + 1, l).contains(namespace)) {
                    reportDuplicateRequire(namespace, depInfo);
                }
                // Check for missing provides.
                DependencyInfo provider = providesMap.get(namespace);
                if (provider == null) {
                    reportUndefinedNamespace(namespace, depInfo);
                } else if (provider == depInfo) {
                    reportSameFile(namespace, depInfo);
                }
            }
        }
    }

    private void reportSameFile(String namespace, DependencyInfo depInfo) {
        errorManager.report(CheckLevel.WARNING,
                JSError.make(depInfo.getName(), -1, -1, SAME_FILE_WARNING, namespace));
    }

    private void reportUndefinedNamespace(String namespace, DependencyInfo depInfo) {
        errorManager.report(CheckLevel.ERROR,
                JSError.make(depInfo.getName(), -1, -1, NEVER_PROVIDED_ERROR, namespace));
    }

    private void reportDuplicateProvide(String namespace, DependencyInfo firstDep, DependencyInfo secondDep) {
        if (firstDep == secondDep) {
            errorManager.report(CheckLevel.WARNING,
                    JSError.make(firstDep.getName(), -1, -1, DUPE_PROVIDES_WARNING, namespace));
        } else {
            errorManager.report(CheckLevel.ERROR, JSError.make(secondDep.getName(), -1, -1, MULTIPLE_PROVIDES_ERROR,
                    namespace, firstDep.getName()));
        }
    }

    private void reportDuplicateRequire(String namespace, DependencyInfo depInfo) {
        errorManager.report(CheckLevel.WARNING,
                JSError.make(depInfo.getName(), -1, -1, DUPE_REQUIRE_WARNING, namespace));
    }

    private void reportNoDepsInDepsFile(String filePath) {
        errorManager.report(CheckLevel.WARNING, JSError.make(filePath, -1, -1, NO_DEPS_WARNING));
    }

    /**
     * Adds the given DependencyInfos to the given providesMap. Also checks for
     * and reports duplicate provides.
     */
    private void addToProvideMap(Iterable<DependencyInfo> depInfos, Map<String, DependencyInfo> providesMap) {
        for (DependencyInfo depInfo : depInfos) {
            for (String provide : depInfo.getProvides()) {
                DependencyInfo prevValue = providesMap.put(provide, depInfo);
                // Check for duplicate provides.
                if (prevValue != null) {
                    reportDuplicateProvide(provide, prevValue, depInfo);
                }
            }
        }
    }

    protected DepsFileParser createDepsFileParser() {
        DepsFileParser depsParser = new DepsFileParser(errorManager);
        depsParser.setShortcutMode(true);
        return depsParser;
    }

    /**
     * Returns whether we should ignore dependency info in the given deps file.
     */
    protected boolean shouldSkipDepsFile(SourceFile file) {
        return false;
    }

    /**
     * Parses all deps.js files in the deps list and creates a map of
     * closure-relative path -> DependencyInfo.
     */
    private Map<String, DependencyInfo> parseDepsFiles() throws IOException {
        DepsFileParser depsParser = createDepsFileParser();
        Map<String, DependencyInfo> depsFiles = new HashMap<>();
        for (SourceFile file : deps) {
            if (!shouldSkipDepsFile(file)) {
                List<DependencyInfo> depInfos = depsParser.parseFileReader(file.getName(), file.getCodeReader());
                if (depInfos.isEmpty()) {
                    reportNoDepsInDepsFile(file.getName());
                } else {
                    for (DependencyInfo info : depInfos) {
                        depsFiles.put(info.getPathRelativeToClosureBase(), info);
                    }
                }
            }
        }

        // If a deps file also appears in srcs, our build tools will move it
        // into srcs.  So we need to scan all the src files for addDependency
        // calls as well.
        for (SourceFile src : srcs) {
            if ((new File(src.getName())).exists() && !shouldSkipDepsFile(src)) {
                List<DependencyInfo> srcInfos = depsParser.parseFileReader(src.getName(), src.getCodeReader());
                for (DependencyInfo info : srcInfos) {
                    depsFiles.put(info.getPathRelativeToClosureBase(), info);
                }
            }
        }

        return depsFiles;
    }

    /**
     * Parses all source files for dependency information.
     * @param preparsedFiles A set of closure-relative paths.
     *     Files in this set are not parsed if they are encountered in srcs.
     * @return Returns a map of closure-relative paths -> DependencyInfo for the
     *     newly parsed files.
     * @throws IOException Occurs upon an IO error.
     */
    private Map<String, DependencyInfo> parseSources(Set<String> preparsedFiles) throws IOException {
        Map<String, DependencyInfo> parsedFiles = new HashMap<>();
        JsFileParser jsParser = new JsFileParser(errorManager);

        for (SourceFile file : srcs) {
            String closureRelativePath = PathUtil.makeRelative(closurePathAbs,
                    PathUtil.makeAbsolute(file.getName()));
            logger.fine("Closure-relative path: " + closureRelativePath);

            if (InclusionStrategy.WHEN_IN_SRCS == mergeStrategy || !preparsedFiles.contains(closureRelativePath)) {
                DependencyInfo depInfo = jsParser.parseFile(file.getName(), closureRelativePath, file.getCode());

                // Kick the source out of memory.
                file.clearCachedSource();
                parsedFiles.put(closureRelativePath, depInfo);
            }
        }

        return parsedFiles;
    }

    /**
     * Creates the content to put into the output deps.js file. If mergeDeps is
     * true, then all of the dependency information in the providedDeps will be
     * included in the output.
     * @throws IOException Occurs upon an IO error.
     */
    private void writeDepsContent(Map<String, DependencyInfo> depsFiles, Map<String, DependencyInfo> jsFiles,
            PrintStream out) throws IOException {
        // Print all dependencies extracted from srcs.
        writeDepInfos(out, jsFiles.values());

        // Print all dependencies extracted from deps.
        if (mergeStrategy == InclusionStrategy.ALWAYS) {
            // This multimap is just for splitting DepsInfo objects by
            // it's definition deps.js file
            Multimap<String, DependencyInfo> infosIndex = Multimaps.index(depsFiles.values(),
                    new Function<DependencyInfo, String>() {
                        @Override
                        public String apply(DependencyInfo from) {
                            return from.getName();
                        }
                    });

            for (String depsPath : infosIndex.keySet()) {
                String path = formatPathToDepsFile(depsPath);
                out.println("\n// Included from: " + path);
                writeDepInfos(out, infosIndex.get(depsPath));
            }
        }
    }

    /**
     * Format the deps file path so that it can be included in the output file.
     */
    protected String formatPathToDepsFile(String path) {
        return path;
    }

    /**
     * Writes goog.addDependency() lines for each DependencyInfo in depInfos.
     */
    private void writeDepInfos(PrintStream out, Collection<DependencyInfo> depInfos) {
        // Print dependencies.
        // Lines look like this:
        // goog.addDependency('../../path/to/file.js', ['goog.Delay'],
        //     ['goog.Disposable', 'goog.Timer']);
        for (DependencyInfo depInfo : depInfos) {
            Collection<String> provides = depInfo.getProvides();
            Collection<String> requires = depInfo.getRequires();

            out.print("goog.addDependency('" + depInfo.getPathRelativeToClosureBase() + "', ");
            writeJsArray(out, provides);
            out.print(", ");
            writeJsArray(out, requires);
            // While transitioning, only write "module" for goog.module 
            if (depInfo.isModule()) {
                out.print(", ");
                writeJsBoolean(out, depInfo.isModule());
            }
            out.println(");");
        }
    }

    private void writeJsBoolean(PrintStream out, boolean value) {
        out.print(value ? "true" : "false");
    }

    /**
     * Prints a list of strings formatted as a JavaScript array of string
     * literals.
     */
    private static void writeJsArray(PrintStream out, Collection<String> values) {
        if (values.isEmpty()) {
            out.print("[]");
        } else {
            out.print("['");
            out.print(Joiner.on("', '").join(values));
            out.print("']");
        }
    }

    static List<SourceFile> createSourceFilesFromPaths(Collection<String> paths) {
        List<SourceFile> files = new ArrayList<>();
        for (String path : paths) {
            files.add(SourceFile.fromFile(path));
        }
        return files;
    }

    static List<SourceFile> createSourceFilesFromPaths(String... paths) {
        return createSourceFilesFromPaths(Arrays.asList(paths));
    }
}