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

com.google.javascript.jscomp.deps.DepsGenerator Maven / Gradle / Ivy

/*
 * 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.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.javascript.jscomp.CheckLevel;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.DiagnosticType;
import com.google.javascript.jscomp.ErrorManager;
import com.google.javascript.jscomp.JSError;
import com.google.javascript.jscomp.JsAst;
import com.google.javascript.jscomp.LazyParsedDependencyInfo;
import com.google.javascript.jscomp.SourceFile;
import com.google.javascript.jscomp.deps.DependencyInfo.Require;
import com.google.javascript.jscomp.deps.ModuleLoader.ModulePath;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
 * Generates deps.js files by scanning JavaScript files for
 * calls to goog.provide(), goog.require() and goog.addDependency().
 */
public class DepsGenerator {

  public static enum InclusionStrategy {
    ALWAYS,
    WHEN_IN_SRCS,
    DO_NOT_DUPLICATE
  }

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

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

  static final DiagnosticType ES6_IMPORT_FOR_NON_ES6_MODULE =
      DiagnosticType.warning(
          "DEPS_ES6_IMPORT_FOR_NON_ES6_MODULE",
          "Cannot import file \"{0}\" because it is not an ES6 module.");

  static final DiagnosticType UNKNOWN_PATH_IMPORT =
      DiagnosticType.warning("DEPS_UNKNOWN_PATH_IMPORT", "Could not find file \"{0}\".");

  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.\nYou "
              + "need to pass a library that has it in srcs or exports to your target''s deps.");

  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 NO_DEPS_WARNING = DiagnosticType.warning(
      "DEPS_NO_DEPS",
      "No dependencies found in file");

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

  /**
   * 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 depsFiles = parseDepsFiles();
    if (logger.isLoggable(Level.FINE)) {
      logger.fine("preparsedFiles: " + depsFiles);
    }
    // Find all goog.provides & goog.requires in src files
    Map jsFiles = parseSources(depsFiles.keySet());

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

    cleanUpDuplicatedFiles(depsFiles, jsFiles);

    jsFiles = removeMungedSymbols(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 depsFiles,
      Map jsFiles) {
    Set 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);
    }
  }

  /**
   * Removes munged symbols in requires and provides. These munged symbols are from ES6 modules
   * and are generated by {@link ModulePath#toModuleName()}. We do not wish to write these munged
   * symbols to the dependency file.
   *
   * 
    *
  • Makes any require'd munged symbol the require'd file's relative path to Closure.
  • *
  • Removes munged symbols from the provides list.
  • *
*/ private Map removeMungedSymbols( Map depFiles, Map jsFiles) { Map newJsFiles = new LinkedHashMap<>(); Map providesMap = new LinkedHashMap<>(); addToProvideMap(depFiles.values(), providesMap, true); addToProvideMap(jsFiles.values(), providesMap, false); for (DependencyInfo dependencyInfo : jsFiles.values()) { ArrayList newRequires = new ArrayList<>(); for (Require require : dependencyInfo.getRequires()) { if (require.getType() == Require.Type.ES6_IMPORT) { // Symbols are unique per file and have nothing to do with paths so map lookups are safe // here. DependencyInfo provider = providesMap.get(require.getSymbol()); if (provider == null) { reportMissingFile(dependencyInfo, require.getRawText()); } else { // If this is an ES6 module then set the symbol to be its relative path to Closure. // ES6 modules in a dependency file do not "provide" anything. Requires can match // a provided symbol or a relative path to Closure. newRequires.add(require.withSymbol(provider.getPathRelativeToClosureBase())); } } else { // Require is by symbol already so no need to change it. newRequires.add(require); } } ImmutableList provides = dependencyInfo.getProvides(); if ("es6".equals(dependencyInfo.getLoadFlags().get("module"))) { String mungedProvide = loader.resolve(dependencyInfo.getName()).toModuleName(); // Filter out the munged symbol. // Note that at the moment ES6 modules should not have any other provides! In the future // we may have additional mechanisms to add goog symbols. But for not nothing is officially // supported. ImmutableList.Builder builder = ImmutableList.builder(); for (String provide : provides) { if (!provide.equals(mungedProvide) && !provide.equals(dependencyInfo.getPathRelativeToClosureBase())) { builder.add(provide); } } provides = builder.build(); } newJsFiles.put( dependencyInfo.getPathRelativeToClosureBase(), SimpleDependencyInfo.builder( dependencyInfo.getPathRelativeToClosureBase(), dependencyInfo.getName()) .setProvides(provides) .setRequires(newRequires) .setLoadFlags(dependencyInfo.getLoadFlags()) .build()); } return newJsFiles; } /** * 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 preparsedFileDependencies Dependency information from existing * deps.js files. * @param parsedFileDependencies Dependency information from parsed .js files. */ private void validateDependencies(Iterable preparsedFileDependencies, Iterable parsedFileDependencies) { // Create a map of namespace -> file providing it. // Also report any duplicate provides. Map providesMap = new LinkedHashMap<>(); addToProvideMap(preparsedFileDependencies, providesMap, true); addToProvideMap(parsedFileDependencies, providesMap, false); // For each require in the parsed sources: for (DependencyInfo depInfo : parsedFileDependencies) { for (Require require : depInfo.getRequires()) { String namespace = require.getSymbol(); // Check for missing provides. DependencyInfo provider = providesMap.get(namespace); if (provider == null) { reportUndefinedNamespace(namespace, depInfo); } else if (provider == depInfo) { reportSameFile(namespace, depInfo); } else { depInfo.isModule(); boolean providerIsEs6Module = "es6".equals(provider.getLoadFlags().get("module")); switch (require.getType()) { case ES6_IMPORT: if (!providerIsEs6Module) { reportEs6ImportForNonEs6Module(provider, depInfo); } break; case GOOG_REQUIRE_SYMBOL: case PARSED_FROM_DEPS: break; case COMMON_JS: case COMPILER_MODULE: default: throw new IllegalStateException("Unexpected import type: " + require.getType()); } } } } } private void reportMissingFile(DependencyInfo depInfo, String path) { errorManager.report( CheckLevel.ERROR, JSError.make(depInfo.getName(), -1, -1, UNKNOWN_PATH_IMPORT, path)); } private void reportEs6ImportForNonEs6Module(DependencyInfo provider, DependencyInfo depInfo) { errorManager.report( CheckLevel.ERROR, JSError.make(depInfo.getName(), -1, -1, ES6_IMPORT_FOR_NON_ES6_MODULE, provider.getName())); } 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) { if (!firstDep.getPathRelativeToClosureBase().equals(namespace)) { 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 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 depInfos, Map providesMap, boolean isFromDepsFile) { for (DependencyInfo depInfo : depInfos) { List provides = new ArrayList<>(depInfo.getProvides()); // Add a munged symbol to the provides map so that lookups by path requires work as intended. if (isFromDepsFile) { // Don't add the dependency file itself but every file it says exists instead. provides.add( loader .resolve( PathUtil.makeAbsolute(depInfo.getPathRelativeToClosureBase(), closurePathAbs)) .toModuleName()); } else { // ES6 modules already provide these munged symbols. if (!"es6".equals(depInfo.getLoadFlags().get("module"))) { provides.add(loader.resolve(depInfo.getName()).toModuleName()); } } // Also add the relative closure path as a provide. At some point we'll swap out the munged // symbols for these relative paths. So looks ups by either need to work. provides.add(depInfo.getPathRelativeToClosureBase()); for (String provide : provides) { DependencyInfo prevValue = providesMap.put(provide, depInfo); // Check for duplicate provides. if (prevValue != null) { reportDuplicateProvide(provide, prevValue, depInfo); } } } } protected DepsFileRegexParser createDepsFileParser() { DepsFileRegexParser depsParser = new DepsFileRegexParser(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 parseDepsFiles() throws IOException { DepsFileRegexParser depsParser = createDepsFileParser(); Map depsFiles = new LinkedHashMap<>(); for (SourceFile file : deps) { if (!shouldSkipDepsFile(file)) { List depInfos = depsParser.parseFileReader( file.getName(), file.getCodeReader()); if (depInfos.isEmpty()) { reportNoDepsInDepsFile(file.getName()); } else { for (DependencyInfo info : depInfos) { depsFiles.put(info.getPathRelativeToClosureBase(), removeRelativePathProvide(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 (!shouldSkipDepsFile(src)) { List srcInfos = depsParser.parseFileReader(src.getName(), src.getCodeReader()); for (DependencyInfo info : srcInfos) { depsFiles.put(info.getPathRelativeToClosureBase(), removeRelativePathProvide(info)); } } } return depsFiles; } private DependencyInfo removeRelativePathProvide(DependencyInfo info) { // DepsFileRegexParser adds an ES6 module's relative path to closure as a provide so that // the resulting depgraph is valid. But we don't want to write this "fake" provide // back out, so remove it here. return SimpleDependencyInfo.Builder.from(info) .setProvides( info.getProvides().stream() .filter(p -> !p.equals(info.getPathRelativeToClosureBase())) .collect(Collectors.toList())) .build(); } /** * 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 parseSources( Set preparsedFiles) throws IOException { Map parsedFiles = new LinkedHashMap<>(); JsFileRegexParser jsParser = new JsFileRegexParser(errorManager).setModuleLoader(loader); Compiler compiler = new Compiler(); compiler.init(ImmutableList.of(), ImmutableList.of(), new CompilerOptions()); for (SourceFile file : srcs) { String closureRelativePath = PathUtil.makeRelative( closurePathAbs, PathUtil.makeAbsolute(file.getName())); if (logger.isLoggable(Level.FINE)) { logger.fine("Closure-relative path: " + closureRelativePath); } if (InclusionStrategy.WHEN_IN_SRCS == mergeStrategy || !preparsedFiles.contains(closureRelativePath)) { DependencyInfo depInfo = jsParser.parseFile( file.getName(), closureRelativePath, file.getCode()); depInfo = new LazyParsedDependencyInfo(depInfo, new JsAst(file), compiler); // 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 depsFiles, Map 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 infosIndex = Multimaps.index(depsFiles.values(), DependencyInfo::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 static void writeDepInfos(PrintStream out, Collection depInfos) throws IOException { // Print dependencies. // Lines look like this: // goog.addDependency('../../path/to/file.js', ['goog.Delay'], // ['goog.Disposable', 'goog.Timer']); for (DependencyInfo depInfo : depInfos) { DependencyInfo.Util.writeAddDependency(out, depInfo); } } static List createSourceFilesFromPaths( Collection paths) { List files = new ArrayList<>(); for (String path : paths) { files.add(SourceFile.fromFile(path)); } return files; } static List createSourceFilesFromPaths(String... paths) { return createSourceFilesFromPaths(Arrays.asList(paths)); } static List createSourceFilesFromZipPaths( Collection paths) throws IOException { List zipSourceFiles = new ArrayList<>(); for (String path : paths) { zipSourceFiles.addAll(SourceFile.fromZipFile(path, UTF_8)); } return zipSourceFiles; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy