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

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

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs. This binary checks for style issues such as incorrect or missing JSDoc usage, and missing goog.require() statements. It does not do more advanced checks such as typechecking.

There is a newer version: v20200830
Show newest version
/*
 * Copyright 2011 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.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.deps.ModuleLoader;
import com.google.javascript.jscomp.deps.ModuleLoader.ModulePath;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;

/**
 * Rewrites a CommonJS module http://wiki.commonjs.org/wiki/Modules/1.1.1 into a form that can be
 * safely concatenated. Does not add a function around the module body but instead adds suffixes to
 * global variables to avoid conflicts. Calls to require are changed to reference the required
 * module directly.
 */
public final class ProcessCommonJSModules extends NodeTraversal.AbstractPreOrderCallback
    implements CompilerPass {
  private static final String EXPORTS = "exports";
  private static final String MODULE = "module";
  private static final String REQUIRE = "require";
  private static final String WEBPACK_REQUIRE = "__webpack_require__";
  // Webpack transpiles import() statements to __webpack_require__.t(modulePath)
  // Always imports the module namespace regardless of module type
  private static final String WEBPACK_REQUIRE_NAMESPACE = "__webpack_require__.t";
  private static final String EXPORT_PROPERTY_NAME = "default";

  public static final DiagnosticType UNKNOWN_REQUIRE_ENSURE =
      DiagnosticType.warning(
          "JSC_COMMONJS_UNKNOWN_REQUIRE_ENSURE_ERROR", "Unrecognized require.ensure call: {0}");

  public static final DiagnosticType SUSPICIOUS_EXPORTS_ASSIGNMENT =
      DiagnosticType.warning(
          "JSC_COMMONJS_SUSPICIOUS_EXPORTS_ASSIGNMENT",
          "Suspicious re-assignment of \"exports\" variable."
              + " Did you actually intend to export something?");

  private final AbstractCompiler compiler;

  /**
   * Creates a new ProcessCommonJSModules instance which can be used to rewrite CommonJS modules to
   * a concatenable form.
   *
   * @param compiler The compiler
   */
  public ProcessCommonJSModules(AbstractCompiler compiler) {
    this.compiler = compiler;
  }

  @Override
  public void process(Node externs, Node root) {
    NodeTraversal.traverse(compiler, root, this);
    NodeTraversal.traverse(compiler, externs, this);
  }

  @Override
  public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
    if (n.isRoot()) {
      return true;
    } else if (n.isScript()) {
      if (compiler.getOptions().getModuleResolutionMode() == ModuleLoader.ResolutionMode.WEBPACK) {
        removeWebpackModuleShim(n);
      }

      FindImportsAndExports finder = new FindImportsAndExports();
      ErrorHandler moduleLoaderErrorHandler = compiler.getModuleLoader().getErrorHandler();
      compiler.getModuleLoader().setErrorHandler(finder);
      NodeTraversal.traverse(compiler, n, finder);

      CompilerInput.ModuleType moduleType = compiler.getInput(n.getInputId()).getJsModuleType();

      boolean forceModuleDetection = moduleType == CompilerInput.ModuleType.IMPORTED_SCRIPT;
      boolean defaultExportIsConst = true;

      boolean isCommonJsModule = finder.isCommonJsModule();
      ImmutableList.Builder exports = ImmutableList.builder();
      if (isCommonJsModule || forceModuleDetection) {
        if (!finder.umdPatterns.isEmpty()) {
          boolean needsRetraverse = false;
          if (finder.replaceUmdPatterns()) {
            needsRetraverse = true;
          }
          // Removing the IIFE rewrites vars. We need to re-traverse
          // to get the new references.
          if (removeIIFEWrapper(n)) {
            needsRetraverse = true;
          }

          if (needsRetraverse) {
            finder = new FindImportsAndExports();
            compiler.getModuleLoader().setErrorHandler(finder);
            NodeTraversal.traverse(compiler, n, finder);
          }
        }

        defaultExportIsConst = finder.initializeModule();

        exports.addAll(finder.getModuleExports());
        exports.addAll(finder.getExports());
      }
      finder.reportModuleErrors();
      compiler.getModuleLoader().setErrorHandler(moduleLoaderErrorHandler);

      NodeTraversal.traverse(
          compiler,
          n,
          new RewriteModule(
              isCommonJsModule || forceModuleDetection, exports.build(), defaultExportIsConst));
    }
    return false;
  }

  public static String getModuleName(CompilerInput input) {
    ModulePath modulePath = input.getPath();
    if (modulePath == null) {
      return null;
    }

    return getModuleName(modulePath);
  }

  public static String getModuleName(ModulePath input) {
    return input.toModuleName();
  }

  String getBasePropertyImport(String moduleName, Node requireCall) {
    checkArgument(requireCall.isCall());
    if (compiler.getOptions().getModuleResolutionMode() == ModuleLoader.ResolutionMode.WEBPACK
        && requireCall.getFirstChild().matchesQualifiedName(WEBPACK_REQUIRE_NAMESPACE)) {
      return moduleName;
    }

    return getBasePropertyImport(moduleName);
  }

  public String getBasePropertyImport(String moduleName) {
    CompilerInput.ModuleType moduleType = compiler.getModuleTypeByName(moduleName);
    if (moduleType != null && moduleType != CompilerInput.ModuleType.COMMONJS) {
      return moduleName;
    }

    return moduleName + "." + EXPORT_PROPERTY_NAME;
  }

  public boolean isCommonJsImport(Node requireCall) {
    return isCommonJsImport(requireCall, compiler.getOptions().getModuleResolutionMode());
  }

  /**
   * Recognize if a node is a module import. We recognize two forms:
   *
   * 
    *
  • require("something"); *
  • __webpack_require__(4); // only when the module resolution is WEBPACK *
  • __webpack_require__.t(4); // only when the module resolution is WEBPACK *
*/ public static boolean isCommonJsImport( Node requireCall, ModuleLoader.ResolutionMode resolutionMode) { if (requireCall.isCall() && requireCall.hasTwoChildren()) { if (resolutionMode == ModuleLoader.ResolutionMode.WEBPACK && (requireCall.getFirstChild().matchesQualifiedName(WEBPACK_REQUIRE) || requireCall.getFirstChild().matchesQualifiedName(WEBPACK_REQUIRE_NAMESPACE)) && (requireCall.getSecondChild().isNumber() || requireCall.getSecondChild().isString())) { return true; } else if (requireCall.getFirstChild().matchesQualifiedName(REQUIRE) && requireCall.getSecondChild().isString()) { return true; } } else if (requireCall.isCall() && requireCall.hasXChildren(3) && resolutionMode == ModuleLoader.ResolutionMode.WEBPACK && requireCall.getFirstChild().matchesQualifiedName(WEBPACK_REQUIRE + ".bind") && requireCall.getSecondChild().isNull() && (requireCall.getLastChild().isNumber() || requireCall.getLastChild().isString())) { return true; } return false; } public String getCommonJsImportPath(Node requireCall) { return getCommonJsImportPath(requireCall, compiler.getOptions().getModuleResolutionMode()); } public static String getCommonJsImportPath( Node requireCall, ModuleLoader.ResolutionMode resolutionMode) { if (resolutionMode == ModuleLoader.ResolutionMode.WEBPACK) { Node pathArgument = requireCall.getChildCount() >= 3 ? requireCall.getChildAtIndex(2) : requireCall.getSecondChild(); if (pathArgument.isNumber()) { return String.valueOf((int) pathArgument.getDouble()); } else { return pathArgument.getString(); } } return requireCall.getSecondChild().getString(); } private String getImportedModuleName(NodeTraversal t, Node requireCall) { return getImportedModuleName(t, requireCall, getCommonJsImportPath(requireCall)); } private String getImportedModuleName(NodeTraversal t, Node n, String importPath) { ModulePath modulePath = t.getInput() .getPath() .resolveJsModule(importPath, n.getSourceFileName(), n.getLineno(), n.getCharno()); if (modulePath == null) { return ModuleIdentifier.forFile(importPath).getModuleName(); } return modulePath.toModuleName(); } /** * Recognize if a node is a module export. We recognize several forms: * *
    *
  • module.exports = something; *
  • module.exports.something = something; *
  • exports.something = something; *
* *

In addition, we only recognize an export if the base export object is not defined or is * defined in externs. */ public static boolean isCommonJsExport( NodeTraversal t, Node export, ModuleLoader.ResolutionMode resolutionMode) { if (export.matchesQualifiedName(MODULE + "." + EXPORTS) || (export.isGetElem() && export.getFirstChild().matchesQualifiedName(MODULE) && export.getSecondChild().isString() && export.getSecondChild().getString().equals(EXPORTS))) { Var v = t.getScope().getVar(MODULE); if (v == null || v.isExtern()) { return true; } } else if (export.isName() && EXPORTS.equals(export.getString())) { Var v = t.getScope().getVar(export.getString()); if (v == null || v.isGlobal()) { return true; } } return false; } private boolean isCommonJsExport(NodeTraversal t, Node export) { return ProcessCommonJSModules.isCommonJsExport( t, export, compiler.getOptions().getModuleResolutionMode()); } /** * Recognize if a node is a dynamic module import. Currently only the webpack dynamic import is * recognized: * *

    *
  • __webpack_require__.e(0).then(function() { return __webpack_require__(4);}) *
  • Promise.all([__webpack_require__.e(0)]).then(function() { return * __webpack_require__(4);}) *
*/ public static boolean isCommonJsDynamicImportCallback( Node n, ModuleLoader.ResolutionMode resolutionMode) { if (n == null || resolutionMode != ModuleLoader.ResolutionMode.WEBPACK) { return false; } if (n.isFunction() && isWebpackRequireEnsureCallback(n)) { return true; } return false; } /** * Recognizes __webpack_require__ calls that are the .then callback of a __webpack_require__.e * call. Example: * *
    *
  • __webpack_require__.e(0).then(function() { return __webpack_require__(4); }) *
  • Promise.all([__webpack_require__.e(0)]).then(function() { return * __webpack_require__(4);}) *
*/ private static boolean isWebpackRequireEnsureCallback(Node fnc) { checkArgument(fnc.isFunction()); if (fnc.getParent() == null) { return false; } Node callParent = fnc.getParent(); if (!callParent.isCall()) { return false; } if (!(callParent.hasChildren() && callParent.getFirstChild().isGetProp() && callParent.getFirstFirstChild().isCall())) { return false; } Node callParentTarget = callParent.getFirstFirstChild().getFirstChild(); if (callParentTarget.matchesQualifiedName(WEBPACK_REQUIRE + ".e") && callParent.getFirstChild().getSecondChild().isString() && callParent.getFirstChild().getSecondChild().getString().equals("then")) { return true; } else if (callParentTarget.matchesQualifiedName("Promise.all") && callParentTarget.getNext() != null && callParentTarget.getNext().isArrayLit()) { boolean allElementsAreDynamicImports = false; for (Node arrayItem = callParentTarget.getNext().getFirstChild(); arrayItem != null; arrayItem = arrayItem.getNext()) { if (!(arrayItem.isCall() && arrayItem.hasTwoChildren() && arrayItem.getFirstChild().matchesQualifiedName(WEBPACK_REQUIRE + ".e"))) { return false; } allElementsAreDynamicImports = true; } return allElementsAreDynamicImports; } return false; } /** * Information on a Universal Module Definition A UMD is an IF statement and a reference to which * branch contains the commonjs export */ static class UmdPattern { final Node ifRoot; final Node activeBranch; UmdPattern(Node ifRoot, Node activeBranch) { this.ifRoot = ifRoot; this.activeBranch = activeBranch; } } static class ExportInfo { final Node node; final Scope scope; final boolean isInSupportedScope; ExportInfo(Node node, Scope scope) { this.node = node; this.scope = scope; Node disqualifyingParent = NodeUtil.getEnclosingNode( node, (n) -> n.isIf() || n.isHook() || n.isFunction() || n.isArrowFunction()); this.isInSupportedScope = disqualifyingParent == null; } } private Node getBaseQualifiedNameNode(Node n) { Node refParent = n; while (refParent.getParent() != null && refParent.getParent().isQualifiedName()) { refParent = refParent.getParent(); } return refParent; } /** * UMD modules are often wrapped in an IIFE for cases where they are used as scripts instead of * modules. Remove the wrapper. * @return Whether an IIFE wrapper was found and removed. */ private boolean removeIIFEWrapper(Node root) { checkState(root.isScript()); Node n = root.getFirstChild(); // Sometimes scripts start with a semicolon for easy concatenation. // Skip any empty statements from those while (n != null && n.isEmpty()) { n = n.getNext(); } // An IIFE wrapper must be the only non-empty statement in the script, // and it must be an expression statement. if (n == null || !n.isExprResult() || n.getNext() != null) { return false; } // Function expression can be forced with !, just skip ! // TODO(ChadKillingsworth): // Expression could also be forced with: + - ~ void // ! ~ void can be repeated any number of times if (n != null && n.getFirstChild() != null && n.getFirstChild().isNot()) { n = n.getFirstChild(); } Node call = n.getFirstChild(); if (call == null || !call.isCall()) { return false; } // Find the IIFE call and function nodes Node fnc; if (call.getFirstChild().isFunction()) { fnc = n.getFirstFirstChild(); } else if (call.getFirstChild().isGetProp() && call.getFirstFirstChild().isFunction() && call.getFirstFirstChild().getNext().isString() && call.getFirstFirstChild().getNext().getString().equals("call")) { fnc = call.getFirstFirstChild(); // We only support explicitly binding "this" to the parent "this" or "exports" if (!(call.getSecondChild() != null && (call.getSecondChild().isThis() || call.getSecondChild().matchesQualifiedName(EXPORTS)))) { return false; } } else { return false; } if (NodeUtil.doesFunctionReferenceOwnArgumentsObject(fnc)) { return false; } CompilerInput ci = compiler.getInput(root.getInputId()); ModulePath modulePath = ci.getPath(); if (modulePath == null) { return false; } String iifeLabel = getModuleName(modulePath) + "_iifeWrapper"; FunctionToBlockMutator mutator = new FunctionToBlockMutator(compiler, compiler.getUniqueNameIdSupplier()); Node block = mutator.mutateWithoutRenaming(iifeLabel, fnc, call, null, false, false); root.removeChildren(); root.addChildrenToFront(block.removeChildren()); reportNestedScopesDeleted(fnc); compiler.reportChangeToEnclosingScope(root); return true; } /** * For AMD wrappers, webpack adds a shim for the "module" variable. We need that to be a free var * so we remove the shim. */ private void removeWebpackModuleShim(Node root) { checkState(root.isScript()); Node n = root.getFirstChild(); // Sometimes scripts start with a semicolon for easy concatenation. // Skip any empty statements from those while (n != null && n.isEmpty()) { n = n.getNext(); } // An IIFE wrapper must be the only non-empty statement in the script, // and it must be an expression statement. if (n == null || !n.isExprResult() || n.getNext() != null) { return; } Node call = n.getFirstChild(); if (call == null || !call.isCall()) { return; } // Find the IIFE call and function nodes Node callTarget = call.getFirstChild(); if (!callTarget.isFunction()) { return; } Node fnc = callTarget; Node params = NodeUtil.getFunctionParameters(fnc); Node moduleParam = null; Node param = params.getFirstChild(); int paramNumber = 0; while (param != null) { paramNumber++; if (param.isName() && param.getString().equals(MODULE)) { moduleParam = param; break; } param = param.getNext(); } if (moduleParam == null) { return; } boolean isFreeCall = call.getBooleanProp(Node.FREE_CALL); Node arg = call.getChildAtIndex(isFreeCall ? paramNumber : paramNumber + 1); if (arg == null) { return; } Node argCallTarget = arg.getFirstChild(); if (arg.isCall() && argCallTarget.isCall() && isCommonJsImport(argCallTarget) && argCallTarget.getNext().matchesName(MODULE)) { String importPath = getCommonJsImportPath(argCallTarget); ModulePath modulePath = compiler .getInput(root.getInputId()) .getPath() .resolveJsModule( importPath, arg.getSourceFileName(), arg.getLineno(), arg.getCharno()); if (modulePath == null) { // The module loader will issue an error return; } if (modulePath.toString().contains("/buildin/module.js")) { arg.detach(); param.detach(); compiler.reportChangeToChangeScope(fnc); compiler.reportChangeToEnclosingScope(fnc); } } } /** * Traverse the script. Find all references to CommonJS require (import) and module.exports or * export statements. Rewrites any require calls to reference the rewritten module name. */ class FindImportsAndExports implements NodeTraversal.Callback, ErrorHandler { private boolean hasGoogProvideOrModule = false; private Node script = null; boolean isCommonJsModule() { return (exports.size() > 0 || moduleExports.size() > 0) && !hasGoogProvideOrModule; } List umdPatterns = new ArrayList<>(); List moduleExports = new ArrayList<>(); List exports = new ArrayList<>(); List errors = new ArrayList<>(); @Override public void report(CheckLevel ignoredLevel, JSError error) { errors.add(error); } public List getModuleExports() { return ImmutableList.copyOf(moduleExports); } public List getExports() { return ImmutableList.copyOf(exports); } @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { if (n.isScript()) { checkState(this.script == null); this.script = n; } return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { // Check for goog.provide or goog.module statements if (parent == null || NodeUtil.isControlStructure(parent) || NodeUtil.isStatementBlock(parent)) { if (n.isExprResult()) { Node maybeGetProp = n.getFirstFirstChild(); if (maybeGetProp != null && (maybeGetProp.matchesQualifiedName("goog.provide") || maybeGetProp.matchesQualifiedName("goog.module"))) { hasGoogProvideOrModule = true; } } } // Find require.ensure calls if (n.isCall() && n.getFirstChild().matchesQualifiedName("require.ensure")) { visitRequireEnsureCall(t, n); } if (n.matchesQualifiedName(MODULE + "." + EXPORTS) || (n.isGetElem() && n.getFirstChild().matchesQualifiedName(MODULE) && n.getSecondChild().isString() && n.getSecondChild().getString().equals(EXPORTS))) { if (isCommonJsExport(t, n)) { moduleExports.add(new ExportInfo(n, t.getScope())); // If the module.exports statement is nested in an if statement, // and the test of the if checks for "module" or "define, // assume the if statement is an UMD pattern with a common js export in the then branch UmdTestInfo umdTestAncestor = getOutermostUmdTest(parent); if (umdTestAncestor != null && !isInIfTest(n)) { UmdPattern existingPattern = findUmdPattern(umdPatterns, umdTestAncestor.enclosingIf); if (existingPattern == null) { umdPatterns.add( new UmdPattern(umdTestAncestor.enclosingIf, umdTestAncestor.activeBranch)); } } } } else if (n.matchesQualifiedName("define.amd") || n.matchesQualifiedName("window.define.amd")) { // If a define.amd statement is in the test of an if statement, // and the statement has no "else" block, we simply want to remove // the entire if statement. UmdTestInfo umdTestAncestor = getOutermostUmdTest(parent); if (umdTestAncestor != null && isInIfTest(n)) { UmdPattern existingPattern = findUmdPattern(umdPatterns, umdTestAncestor.enclosingIf); if (existingPattern == null && umdTestAncestor.enclosingIf.getChildAtIndex(2) == null) { umdPatterns.add(new UmdPattern(umdTestAncestor.enclosingIf, null)); } } } if (n.isName() && EXPORTS.equals(n.getString())) { Var v = t.getScope().getVar(EXPORTS); if (v == null || v.isGlobal()) { Node qNameRoot = getBaseQualifiedNameNode(n); if (qNameRoot != null && qNameRoot.matchesQualifiedName(EXPORTS) && NodeUtil.isLValue(qNameRoot)) { // Match the special assignment // exports = module.exports if (n.getGrandparent().isExprResult() && n.getNext() != null && ((n.getNext().isGetProp() && n.getNext().matchesQualifiedName(MODULE + "." + EXPORTS)) || (n.getNext().isAssign() && n.getNext() .getFirstChild() .matchesQualifiedName(MODULE + "." + EXPORTS)))) { exports.add(new ExportInfo(n, t.getScope())); // Ignore inlining created identity vars // var exports = exports } else if (!this.hasGoogProvideOrModule && (v == null || (v.getNameNode() == null && v.getNameNode().getFirstChild() != n))) { errors.add(JSError.make(qNameRoot, SUSPICIOUS_EXPORTS_ASSIGNMENT)); } } else { exports.add(new ExportInfo(n, t.getScope())); // If the exports statement is nested in the then branch of an if statement, // and the test of the if checks for "module" or "define, // assume the if statement is an UMD pattern with a common js export in the then branch UmdTestInfo umdTestAncestor = getOutermostUmdTest(parent); if (umdTestAncestor != null && !isInIfTest(n)) { UmdPattern existingPattern = findUmdPattern(umdPatterns, umdTestAncestor.enclosingIf); if (existingPattern == null) { umdPatterns.add( new UmdPattern(umdTestAncestor.enclosingIf, umdTestAncestor.activeBranch)); } } } } } else if (n.isThis() && n.getParent().isGetProp() && t.inGlobalScope()) { exports.add(new ExportInfo(n, t.getScope())); } if (isCommonJsImport(n)) { visitRequireCall(t, n, parent); } } /** Visit require calls. */ private void visitRequireCall(NodeTraversal t, Node require, Node parent) { // When require("name") is used as a standalone statement (the result isn't used) // it indicates that a module is being loaded for the side effects it produces. // In this case the require statement should just be removed as the dependency // sorting will insert the file for us. if (!NodeUtil.isExpressionResultUsed(require) && parent.isExprResult() && NodeUtil.isStatementBlock(parent.getParent())) { // Attempt to resolve the module so that load warnings are issued t.getInput() .getPath() .resolveJsModule( getCommonJsImportPath(require), require.getSourceFileName(), require.getLineno(), require.getCharno()); Node grandparent = parent.getParent(); parent.detach(); compiler.reportChangeToEnclosingScope(grandparent); } } /** * Visit require.ensure calls. Replace the call with an IIFE. Require.ensure must always be of * the form: * *

require.ensure(['module1', ...], function(require) {}) */ private void visitRequireEnsureCall(NodeTraversal t, Node call) { if (!call.hasXChildren(3)) { compiler.report( JSError.make( call, UNKNOWN_REQUIRE_ENSURE, "Expected the function to have 2 arguments but instead found {0}", "" + call.getChildCount())); return; } Node dependencies = call.getSecondChild(); if (!dependencies.isArrayLit()) { compiler.report( JSError.make( dependencies, UNKNOWN_REQUIRE_ENSURE, "The first argument must be an array literal of string literals.")); return; } for (Node dep : dependencies.children()) { if (!dep.isString()) { compiler.report( JSError.make( dep, UNKNOWN_REQUIRE_ENSURE, "The first argument must be an array literal of string literals.")); return; } } Node callback = dependencies.getNext(); if (!(callback.isFunction() && callback.getSecondChild().hasOneChild() && callback.getSecondChild().getFirstChild().isName() && "require".equals(callback.getSecondChild().getFirstChild().getString()))) { compiler.report( JSError.make( callback, UNKNOWN_REQUIRE_ENSURE, "The second argument must be a function" + " whose first argument is named \"require\".")); return; } callback.detach(); // Remove the "require" argument from the parameter list. callback.getSecondChild().removeChildren(); call.removeChildren(); call.putBooleanProp(Node.FREE_CALL, true); call.addChildToFront(callback); t.reportCodeChange(); } void reportModuleErrors() { for (JSError error : errors) { compiler.report(error); } } /** * If the export is directly assigned more than once, or the assignments are not global, declare * the module name variable. * *

If all of the assignments are simply property assignments, initialize the module name * variable as a namespace. * *

Returns whether the default export can be declared constant */ boolean initializeModule() { CompilerInput ci = compiler.getInput(this.script.getInputId()); ModulePath modulePath = ci.getPath(); if (modulePath == null) { return true; } String moduleName = getModuleName(ci); List exportsToRemove = new ArrayList<>(); for (ExportInfo export : exports) { if (NodeUtil.getEnclosingScript(export.node) == null) { exportsToRemove.add(export); continue; } Node qNameBase = getBaseQualifiedNameNode(export.node); if (export.node == qNameBase && export.node.getParent().isAssign() && export.node.getGrandparent().isExprResult() && export.node.getPrevious() == null && export.node.getNext() != null) { // Find any identity assignments and just remove them // exports = module.exports; if (export.node.getNext().isGetProp() && export.node.getNext().matchesQualifiedName(MODULE + "." + EXPORTS)) { for (ExportInfo moduleExport : moduleExports) { if (moduleExport.node == export.node.getNext()) { moduleExports.remove(moduleExport); break; } } Node changeRoot = export.node.getGrandparent().getParent(); export.node.getGrandparent().detach(); exportsToRemove.add(export); compiler.reportChangeToEnclosingScope(changeRoot); // Find compound identity assignments and remove the exports = portion // exports = module.exports = foo; } else if (export.node.getNext().isAssign() && export .node .getNext() .getFirstChild() .matchesQualifiedName(MODULE + "." + EXPORTS)) { Node assign = export.node.getNext(); export.node.getParent().replaceWith(assign.detach()); exportsToRemove.add(export); compiler.reportChangeToEnclosingScope(assign); } // Find babel transpiled interop assignment // module.exports = exports['default']; } else if (export.node.getParent().isGetElem() && export.node.getNext().isString() && export.node.getNext().getString().equals(EXPORT_PROPERTY_NAME) && export.node.getGrandparent().isAssign() && export.node.getParent().getPrevious() != null && export.node.getParent().getPrevious().matchesQualifiedName(MODULE + "." + EXPORTS)) { Node parent = export.node.getParent(); Node grandparent = parent.getParent(); Node prop = export.node.getNext(); parent.replaceWith( IR.getprop(export.node.detach(), prop.detach()).useSourceInfoFrom(parent)); compiler.reportChangeToEnclosingScope(grandparent); } } exports.removeAll(exportsToRemove); exportsToRemove.clear(); HashMap exportsToReplace = new HashMap<>(); for (ExportInfo export : moduleExports) { if (NodeUtil.getEnclosingScript(export.node) == null) { exportsToRemove.add(export); } else if (export.node.isGetElem()) { Node prop = export.node.getSecondChild().detach(); ExportInfo newExport = new ExportInfo(IR.getprop(export.node.getFirstChild().detach(), prop), export.scope); export.node.replaceWith(newExport.node); compiler.reportChangeToEnclosingScope(newExport.node); exportsToReplace.put(export, newExport); } } moduleExports.removeAll(exportsToRemove); for (ExportInfo oldExport : exportsToReplace.keySet()) { int oldIndex = moduleExports.indexOf(oldExport); moduleExports.remove(oldIndex); moduleExports.add(oldIndex, exportsToReplace.get(oldExport)); } // If we assign to the variable more than once or all the assignments // are properties, initialize the variable as well. int directAssignments = 0; for (ExportInfo export : moduleExports) { if (NodeUtil.getEnclosingScript(export.node) == null) { continue; } Node base = getBaseQualifiedNameNode(export.node); if (base == export.node && export.node.getParent().isAssign()) { Node rValue = NodeUtil.getRValueOfLValue(export.node); if (rValue == null || !rValue.isObjectLit()) { directAssignments++; } else if (rValue.isObjectLit()) { for (Node key = rValue.getFirstChild(); key != null; key = key.getNext()) { if ((!key.isStringKey() || key.isQuotedString()) && !key.isMemberFunctionDef()) { directAssignments++; break; } } } } } Node initModule = IR.var(IR.name(moduleName), IR.objectlit()); initModule.getFirstChild().putBooleanProp(Node.MODULE_EXPORT, true); initModule.getFirstChild().makeNonIndexable(); JSDocInfoBuilder builder = new JSDocInfoBuilder(true); builder.recordConstancy(); initModule.setJSDocInfo(builder.build()); if (directAssignments == 0 || (!exports.isEmpty() && !moduleExports.isEmpty())) { Node defaultProp = IR.stringKey(EXPORT_PROPERTY_NAME); defaultProp.putBooleanProp(Node.MODULE_EXPORT, true); defaultProp.addChildToFront(IR.objectlit()); initModule.getFirstFirstChild().addChildToFront(defaultProp); if (exports.isEmpty() || moduleExports.isEmpty()) { builder = new JSDocInfoBuilder(true); builder.recordConstancy(); defaultProp.setJSDocInfo(builder.build()); } } this.script.addChildToFront(initModule.useSourceInfoFromForTree(this.script)); compiler.reportChangeToEnclosingScope(this.script); return directAssignments < 2 && (exports.isEmpty() || moduleExports.isEmpty()); } private class UmdTestInfo { public Node enclosingIf; public Node activeBranch; UmdTestInfo(Node enclosingIf, Node activeBranch) { this.enclosingIf = enclosingIf; this.activeBranch = activeBranch; } } /** * Find the outermost if node ancestor for a node without leaving the function scope. To match, * the test class of the "if" statement must reference "module" or "define" names. */ private UmdTestInfo getOutermostUmdTest(Node n) { if (n == null || NodeUtil.isTopLevel(n) || n.isFunction()) { return null; } Node parent = n.getParent(); if (parent == null) { return null; } // When walking up ternary operations (hook), don't check if parent is the condition, // because one ternary operation can be then/else branch of another. if ((parent.isIf() || parent.isHook())) { UmdTestInfo umdTestInfo = getOutermostUmdTest(parent); if (umdTestInfo != null) { // If this is the then block of an else-if statement, set the active branch to the // then block rather than the "if" itself. if (parent.isIf() && parent.getSecondChild() == n && parent.getParent() != null && parent.getParent().isBlock() && parent.getNext() == null && umdTestInfo.activeBranch == parent.getParent()) { return new UmdTestInfo(umdTestInfo.enclosingIf, n); } return umdTestInfo; } final List umdTests = new ArrayList<>(); NodeUtil.visitPreOrder( parent.getFirstChild(), new NodeUtil.Visitor() { @Override public void visit(Node node) { if ((node.isName() && (node.getString().equals(MODULE) || node.getString().equals("define"))) || (node.isGetProp() && node.matchesQualifiedName("window.define")) || (node.isString() && node.getString().equals("amd") && node.getParent().isIn())) { umdTests.add(node); } } }); // Webpack replaces tests of `typeof module !== 'undefined'` with `true` if (umdTests.isEmpty() && parent.getFirstChild().isTrue()) { umdTests.add(parent.getFirstChild()); } if (!umdTests.isEmpty()) { return new UmdTestInfo(parent, n); } return null; } return getOutermostUmdTest(parent); } /** Return whether the node is within the test portion of an if statement */ private boolean isInIfTest(Node n) { if (n == null || NodeUtil.isTopLevel(n) || n.isFunction()) { return false; } Node parent = n.getParent(); if (parent == null) { return false; } if ((parent.isIf() || parent.isHook()) && parent.getFirstChild() == n) { return true; } return isInIfTest(parent); } /** Remove a Universal Module Definition and leave just the commonjs export statement */ boolean replaceUmdPatterns() { boolean needsRetraverse = false; Node changeScope; for (UmdPattern umdPattern : umdPatterns) { if (NodeUtil.getEnclosingScript(umdPattern.ifRoot) == null) { reportNestedScopesDeleted(umdPattern.ifRoot); continue; } Node parent = umdPattern.ifRoot.getParent(); Node newNode = umdPattern.activeBranch; if (newNode == null) { parent.removeChild(umdPattern.ifRoot); reportNestedScopesDeleted(umdPattern.ifRoot); compiler.reportChangeToEnclosingScope(parent); needsRetraverse = true; continue; } // Remove redundant block node. Not strictly necessary, but makes tests more legible. if (umdPattern.activeBranch.isBlock() && umdPattern.activeBranch.hasOneChild()) { newNode = umdPattern.activeBranch.removeFirstChild(); } else { newNode.detach(); } needsRetraverse = true; parent.replaceChild(umdPattern.ifRoot, newNode); reportNestedScopesDeleted(umdPattern.ifRoot); changeScope = NodeUtil.getEnclosingChangeScopeRoot(newNode); if (changeScope != null) { compiler.reportChangeToEnclosingScope(newNode); } Node block = parent; if (block.isExprResult()) { block = block.getParent(); } // Detect UMD Factory Patterns and inline the functions if (block.isBlock() && block.getParent().isFunction() && block.getGrandparent().isCall() && parent.hasOneChild()) { Node enclosingFnCall = block.getGrandparent(); Node fn = block.getParent(); Node enclosingScript = NodeUtil.getEnclosingScript(enclosingFnCall); if (enclosingScript == null) { continue; } CompilerInput ci = compiler.getInput( NodeUtil.getEnclosingScript(enclosingFnCall).getInputId()); ModulePath modulePath = ci.getPath(); if (modulePath == null) { continue; } needsRetraverse = true; String factoryLabel = modulePath.toModuleName() + "_factory" + compiler.getUniqueNameIdSupplier().get(); FunctionToBlockMutator mutator = new FunctionToBlockMutator(compiler, compiler.getUniqueNameIdSupplier()); Node newStatements = mutator.mutateWithoutRenaming(factoryLabel, fn, enclosingFnCall, null, false, false); // Check to see if the returned block is of the form: // { // var jscomp$inline = function() {}; // jscomp$inline(); // } // // or // // { // var jscomp$inline = function() {}; // module.exports = jscomp$inline(); // } // // If so, inline again if (newStatements.isBlock() && newStatements.hasTwoChildren() && newStatements.getFirstChild().isVar() && newStatements.getFirstFirstChild().hasOneChild() && newStatements.getFirstFirstChild().getFirstChild().isFunction() && newStatements.getSecondChild().isExprResult()) { Node inlinedFn = newStatements.getFirstFirstChild().getFirstChild(); Node expr = newStatements.getSecondChild().getFirstChild(); Node call = null; String assignedName = null; if (expr.isAssign() && expr.getSecondChild().isCall()) { call = expr.getSecondChild(); assignedName = modulePath.toModuleName() + "_iife" + compiler.getUniqueNameIdSupplier().get(); } else if (expr.isCall()) { call = expr; } if (call != null) { newStatements = mutator.mutateWithoutRenaming( factoryLabel, inlinedFn, call, assignedName, false, false); if (assignedName != null) { Node newName = IR.var( NodeUtil.newName( compiler, assignedName, fn, expr.getFirstChild().getQualifiedName())) .useSourceInfoFromForTree(fn); if (newStatements.hasChildren() && newStatements.getFirstChild().isExprResult() && newStatements.getFirstFirstChild().isAssign() && newStatements.getFirstFirstChild().getFirstChild().isName() && newStatements .getFirstFirstChild() .getFirstChild() .getString() .equals(assignedName)) { newName .getFirstChild() .addChildToFront( newStatements.getFirstFirstChild().getSecondChild().detach()); newStatements.replaceChild(newStatements.getFirstChild(), newName); } else { newStatements.addChildToFront(newName); } expr.replaceChild(expr.getSecondChild(), newName.getFirstChild().cloneNode()); newStatements.addChildToBack(expr.getParent().detach()); } } } Node callRoot = enclosingFnCall.getParent(); if (callRoot.isNot()) { callRoot = callRoot.getParent(); } if (callRoot.isExprResult()) { Node callRootParent = callRoot.getParent(); callRootParent.addChildrenAfter(newStatements.removeChildren(), callRoot); callRoot.detach(); reportNestedScopesChanged(callRootParent); compiler.reportChangeToEnclosingScope(callRootParent); reportNestedScopesDeleted(enclosingFnCall); } else { parent.replaceChild(umdPattern.ifRoot, newNode); compiler.reportChangeToEnclosingScope(newNode); reportNestedScopesDeleted(umdPattern.ifRoot); } } } return needsRetraverse; } } private void reportNestedScopesDeleted(Node n) { NodeUtil.visitPreOrder( n, new NodeUtil.Visitor() { @Override public void visit(Node n) { if (n.isFunction()) { compiler.reportFunctionDeleted(n); } } }); } private void reportNestedScopesChanged(Node n) { NodeUtil.visitPreOrder( n, new NodeUtil.Visitor() { @Override public void visit(Node n) { if (n.isFunction()) { compiler.reportChangeToChangeScope(n); } } }); } private static UmdPattern findUmdPattern(List umdPatterns, Node n) { for (UmdPattern umd : umdPatterns) { if (umd.ifRoot == n) { return umd; } } return null; } /** * Traverse a file and rewrite all references to imported names directly to the targeted module * name. * *

If a file is a CommonJS module, rewrite export statements. Typically exports create an alias * - the rewriting tries to avoid such aliases. */ private class RewriteModule extends AbstractPostOrderCallback { private final boolean allowFullRewrite; private final ImmutableCollection exports; private final List imports = new ArrayList<>(); private final List rewrittenClassExpressions = new ArrayList<>(); private final List functionsToHoist = new ArrayList<>(); private final boolean defaultExportIsConst; public RewriteModule( boolean allowFullRewrite, ImmutableCollection exports, boolean defaultExportIsConst) { this.allowFullRewrite = allowFullRewrite; this.exports = exports; this.defaultExportIsConst = defaultExportIsConst; } @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case SCRIPT: // Class names can't be changed during the middle of a traversal. Unlike functions, // the name can be the EMPTY token rather than just a zero length string. for (Node clazz : rewrittenClassExpressions) { clazz.replaceChild( clazz.getFirstChild(), IR.empty().useSourceInfoFrom(clazz.getFirstChild())); t.reportCodeChange(); } CompilerInput ci = compiler.getInput(n.getInputId()); String moduleName = getModuleName(ci); // If a function is the direct module export, move it to the top. for (int i = 1; i < functionsToHoist.size(); i++) { if (functionsToHoist .get(i) .getFirstFirstChild() .matchesQualifiedName(getBasePropertyImport(moduleName))) { Node fncVar = functionsToHoist.get(i); functionsToHoist.remove(i); functionsToHoist.add(0, fncVar); break; } } // Hoist functions in reverse order so that they maintain the same relative // order after hoisting. for (int i = functionsToHoist.size() - 1; i >= 0; i--) { Node functionExpr = functionsToHoist.get(i); Node scopeRoot = t.getClosestHoistScopeRoot(); Node insertionPoint = scopeRoot.getFirstChild(); if (insertionPoint == null || !(insertionPoint.isVar() && insertionPoint.getFirstChild().getString().equals(moduleName))) { insertionPoint = null; } if (insertionPoint == null) { if (scopeRoot.getFirstChild() != functionExpr) { scopeRoot.addChildToFront(functionExpr.detach()); } } else if (insertionPoint != functionExpr && insertionPoint.getNext() != functionExpr) { scopeRoot.addChildAfter(functionExpr.detach(), insertionPoint); } } for (ExportInfo export : exports) { visitExport(t, export); } for (Node require : imports) { visitRequireCall(t, require, require.getParent()); } break; case CALL: if (isCommonJsImport(n)) { imports.add(n); } break; case VAR: case LET: case CONST: // Multiple declarations need split apart so that they can be refactored into // property assignments or removed altogether. Don't split declarations for // ES module export calls as it breaks AST and ES modules shouldn't be affected at all // by this pass. if (n.hasMoreThanOneChild() && !NodeUtil.isAnyFor(parent) && !parent.isExport()) { List vars = splitMultipleDeclarations(n); t.reportCodeChange(); for (Node var : vars) { visit(t, var.getFirstChild(), var); } } // UMD Inlining can shadow global variables - these are just removed. // // var exports = exports; if (n.getFirstChild().hasChildren() && n.getFirstFirstChild().isName() && n.getFirstChild().getString().equals(n.getFirstFirstChild().getString())) { n.detach(); t.reportCodeChange(); return; } break; case NAME: { // If this is a name declaration with multiple names, it will be split apart when // the parent is visited and then revisit the children. if (NodeUtil.isNameDeclaration(n.getParent()) && n.getParent().hasMoreThanOneChild() && !NodeUtil.isAnyFor(n.getGrandparent())) { break; } String qName = n.getQualifiedName(); if (qName == null) { break; } final Var nameDeclaration = t.getScope().getVar(qName); if (nameDeclaration != null) { if (NodeUtil.isLhsByDestructuring(n)) { maybeUpdateName(t, n, nameDeclaration); } else if (nameDeclaration.getNode() != null && Objects.equals(nameDeclaration.getNode().getInputId(), n.getInputId())) { // Avoid renaming a shadowed global // // var angular = angular; // value is global ref Node enclosingDeclaration = NodeUtil.getEnclosingNode( n, new Predicate() { @Override public boolean apply(Node node) { return node == nameDeclaration.getNameNode(); } }); if (enclosingDeclaration == null || enclosingDeclaration == n || nameDeclaration.getScope() != t.getScope()) { maybeUpdateName(t, n, nameDeclaration); } } // Replace loose "module" references with an object literal } else if (allowFullRewrite && n.getString().equals(MODULE) && !(n.getParent().isGetProp() || n.getParent().isGetElem() || n.getParent().isTypeOf())) { Node objectLit = IR.objectlit().useSourceInfoFrom(n); n.replaceWith(objectLit); t.reportCodeChange(objectLit); } break; } // ES6 object literal shorthand notation can refer to renamed variables case STRING_KEY: { if (n.hasChildren() || n.isQuotedString() || NodeUtil.isLhsByDestructuring(n)) { break; } Var nameDeclaration = t.getScope().getVar(n.getString()); if (nameDeclaration == null) { break; } String importedName = getModuleImportName(t, nameDeclaration.getNode()); if (nameDeclaration.isGlobal() || importedName != null) { Node value = IR.name(n.getString()).useSourceInfoFrom(n); n.addChildToBack(value); maybeUpdateName(t, value, nameDeclaration); } break; } case GETPROP: if (n.matchesQualifiedName(MODULE + ".id") || n.matchesQualifiedName(MODULE + ".filename")) { Var v = t.getScope().getVar(MODULE); if (v == null || v.isExtern()) { n.replaceWith(IR.string(t.getInput().getPath().toString()).useSourceInfoFrom(n)); } } else if (allowFullRewrite && n.getFirstChild().isName() && n.getFirstChild().getString().equals(MODULE) && !n.matchesQualifiedName(MODULE + "." + EXPORTS)) { Var v = t.getScope().getVar(MODULE); if (v == null || v.isExtern()) { n.getFirstChild().replaceWith(IR.objectlit().useSourceInfoFrom(n.getFirstChild())); } } break; case TYPEOF: if (allowFullRewrite && n.getFirstChild().isName() && (n.getFirstChild().getString().equals(MODULE) || n.getFirstChild().getString().equals(EXPORTS))) { Var v = t.getScope().getVar(n.getFirstChild().getString()); if (v == null || v.isExtern()) { n.replaceWith(IR.string("object")); } } break; default: break; } fixTypeAnnotationsForNode(t, n); } private void fixTypeAnnotationsForNode(NodeTraversal t, Node n) { JSDocInfo info = n.getJSDocInfo(); if (info != null) { for (Node typeNode : info.getTypeNodes()) { fixTypeNode(t, typeNode); } } } /** * Visit require calls. Rewrite require statements to be a direct reference to name of require * module. By this point all references to the import alias should have already been renamed. */ private void visitRequireCall(NodeTraversal t, Node require, Node parent) { String moduleName = getImportedModuleName(t, require); Node moduleRef = NodeUtil.newQName(compiler, getBasePropertyImport(moduleName, require)) .useSourceInfoFromForTree(require.getSecondChild()); parent.replaceChild(require, moduleRef); t.reportCodeChange(); } /** * Visit export statements. Export statements can be either a direct assignment: module.exports * = foo or a property assignment: module.exports.foo = foo; exports.foo = foo; */ private void visitExport(NodeTraversal t, ExportInfo export) { Node root = getBaseQualifiedNameNode(export.node); Node rValue = NodeUtil.getRValueOfLValue(root); // For object literal assignments to module.exports, convert them to // individual property assignments. // // module.exports = { foo: bar}; // // becomes // // module.exports = {}; // module.exports.foo = bar; if (root.matchesQualifiedName("module.exports")) { if (rValue != null && rValue.isObjectLit() && root.getParent().isAssign() && root.getParent().getParent().isExprResult()) { if (expandObjectLitAssignment(t, root, export.scope)) { return; } } } String moduleName = getModuleName(t.getInput()); Var moduleInitialization = t.getScope().getVar(moduleName); // If this is an assignment to module.exports or exports, renaming // has already handled this case. Remove the export. Var rValueVar = null; if (rValue != null && rValue.isQualifiedName()) { rValueVar = export.scope.getVar(rValue.getQualifiedName()); } if (root.getParent().isAssign() && root.getGrandparent().isExprResult() && (root.getNext() != null && (root.getNext().isName() || root.getNext().isGetProp())) && root.getParent().getParent().isExprResult() && rValueVar != null && (NodeUtil.getEnclosingScript(rValueVar.nameNode) == null || (rValueVar.nameNode.getParent() != null && !rValueVar.isParam())) && export.isInSupportedScope && (rValueVar.getNameNode().getParent() == null || !NodeUtil.isLhsByDestructuring(rValueVar.getNameNode()))) { root.getParent().getParent().detach(); t.reportCodeChange(); return; } moduleName = moduleName + "." + EXPORT_PROPERTY_NAME; Node updatedExport = NodeUtil.newQName(compiler, moduleName, export.node, export.node.getQualifiedName()); updatedExport.putBooleanProp(Node.MODULE_EXPORT, true); boolean exportIsConst = defaultExportIsConst && updatedExport.matchesQualifiedName( getBasePropertyImport(getModuleName(t.getInput()))) && root == export.node && NodeUtil.isLValue(export.node); Node changeScope = null; if (root.matchesQualifiedName("module.exports") && rValue != null && export.scope.getVar("module.exports") == null && root.getParent().isAssign() && root.getGrandparent().isExprResult() && moduleInitialization == null) { // Rewrite "module.exports = foo;" to "var moduleName = {default: foo};" Node parent = root.getParent(); Node exportName = IR.exprResult(IR.assign(updatedExport, rValue.detach())); if (exportIsConst) { JSDocInfoBuilder info = new JSDocInfoBuilder(false); info.recordConstancy(); exportName.getFirstChild().setJSDocInfo(info.build()); } parent.getParent().replaceWith(exportName.useSourceInfoFromForTree(root.getParent())); changeScope = NodeUtil.getEnclosingChangeScopeRoot(parent); } else if (root.getNext() != null && root.getNext().isName() && rValueVar != null && rValueVar.isGlobal() && export.isInSupportedScope && (rValueVar.getNameNode().getParent() == null || !NodeUtil.isLhsByDestructuring(rValueVar.getNameNode()))) { // This is a where a module export assignment is used in a complex expression. // Before: `SOME_VALUE !== undefined && module.exports = SOME_VALUE` // After: `SOME_VALUE !== undefined && module$name` Node parent = root.getParent(); root.detach(); parent.replaceWith(root); if (root == export.node) { root = updatedExport; } export.node.replaceWith(updatedExport); changeScope = NodeUtil.getEnclosingChangeScopeRoot(root); } else { // Other references to "module.exports" are just replaced with the module name. export.node.replaceWith(updatedExport); if (updatedExport.getParent().isAssign() && exportIsConst) { JSDocInfoBuilder infoBuilder = JSDocInfoBuilder.maybeCopyFrom(updatedExport.getParent().getJSDocInfo()); infoBuilder.recordConstancy(); updatedExport.getParent().setJSDocInfo(infoBuilder.build()); } changeScope = NodeUtil.getEnclosingChangeScopeRoot(updatedExport); } if (changeScope != null) { compiler.reportChangeToChangeScope(changeScope); } } /** * Since CommonJS modules may have only a single export, it's common to see the export be an * object pattern. We want to expand this to individual property assignments. If any individual * property assignment has been renamed, it will be removed. * *

We need to keep assignments which aren't names * *

module.exports = { foo: bar, baz: function() {} } * *

becomes * *

module.exports.foo = bar; // removed later module.exports.baz = function() {}; */ private boolean expandObjectLitAssignment(NodeTraversal t, Node export, Scope scope) { checkState(export.getParent().isAssign()); Node insertionRef = export.getParent().getParent(); checkState(insertionRef.isExprResult()); Node insertionParent = insertionRef.getParent(); checkNotNull(insertionParent); Node rValue = NodeUtil.getRValueOfLValue(export); Node key = rValue.getFirstChild(); boolean removedNodes = false; while (key != null) { Node lhs; if ((!key.isStringKey() || key.isQuotedString()) && !key.isMemberFunctionDef()) { key = key.getNext(); continue; } lhs = IR.getprop(export.cloneTree(), IR.string(key.getString())); Node value = null; if (key.isStringKey()) { if (key.hasChildren()) { value = key.removeFirstChild(); } else { value = IR.name(key.getString()); } } else if (key.isMemberFunctionDef()) { value = key.getFirstChild().detach(); } Node expr = null; expr = IR.exprResult(IR.assign(lhs, value)).useSourceInfoIfMissingFromForTree(key); insertionParent.addChildAfter(expr, insertionRef); ExportInfo newExport = new ExportInfo(lhs.getFirstChild(), scope); visitExport(t, newExport); // Export statements can be removed in visitExport if (expr != null && expr.getParent() != null) { insertionRef = expr; } Node currentKey = key; key = key.getNext(); currentKey.detach(); removedNodes = true; } if (!rValue.hasChildren()) { export.getGrandparent().detach(); return true; } if (removedNodes) { t.reportCodeChange(rValue); } return false; } /** * Given a name reference, check to see if it needs renamed. * *

We handle 3 main cases: 1. References to an import alias. These are replaced with a direct * reference to the imported module. 2. Names which are exported. These are rewritten to be the * export assignment directly. 3. Global names: If a name is global to the script, add a suffix * so it doesn't collide with any other global. * *

Rewriting case 1 is safe to perform on all files. Cases 2 and 3 can only be done if this * file is a commonjs module. */ private void maybeUpdateName(NodeTraversal t, Node n, Var var) { checkNotNull(var); checkState(n.isName() || n.isGetProp()); checkState(n.getParent() != null); String importedModuleName = getModuleImportName(t, var.getNode()); String name = n.getQualifiedName(); // Check if the name refers to a alias for a require('foo') import. if (importedModuleName != null && n != var.getNode()) { // Reference the imported name directly, rather than the alias updateNameReference(t, n, name, importedModuleName, false, false); } else if (allowFullRewrite) { String exportedName = getExportedName(t, n, var); // We need to exclude the alias created by the require import. We assume dead // code elimination will remove these later. if ((n != var.getNode() || n.getParent().isClass()) && exportedName == null && (var == null || var.getNameNode().getParent() == null || !NodeUtil.isLhsByDestructuring(var.getNameNode()))) { // The name is actually the export reference itself. // This will be handled later by visitExports. if (n.getParent().isClass() && n.getParent().getFirstChild() == n) { rewrittenClassExpressions.add(n.getParent()); } return; } // Check if the name is used as an export if (importedModuleName == null && exportedName != null && !exportedName.equals(name) && !var.isParam() && (var.getNameNode().getParent() == null || !NodeUtil.isLhsByDestructuring(var.getNameNode()))) { boolean exportPropIsConst = defaultExportIsConst && getBasePropertyImport(getModuleName(t.getInput())).equals(exportedName) && getBaseQualifiedNameNode(n) == n && NodeUtil.isLValue(n); updateNameReference(t, n, name, exportedName, true, exportPropIsConst); // If it's a global name, rename it to prevent conflicts with other scripts } else if (var.isGlobal()) { String currentModuleName = getModuleName(t.getInput()); if (currentModuleName.equals(name)) { return; } // refs to 'exports' are handled separately. if (EXPORTS.equals(name)) { return; } // closure_test_suite looks for test*() functions if (compiler.getOptions().exportTestFunctions && currentModuleName.startsWith("test")) { return; } String newName = name + "$$" + currentModuleName; updateNameReference(t, n, name, newName, false, false); } } } /** * @param nameRef the qualified name node * @param originalName of nameRef * @param newName for nameRef * @param requireFunctionExpressions Whether named class or functions should be rewritten to * variable assignments */ private void updateNameReference( NodeTraversal t, Node nameRef, String originalName, String newName, boolean requireFunctionExpressions, boolean qualifiedNameIsConst) { Node parent = nameRef.getParent(); checkNotNull(parent); checkNotNull(newName); boolean newNameIsQualified = newName.indexOf('.') >= 0; boolean newNameIsModuleExport = newName.equals(getBasePropertyImport(getModuleName(t.getInput()))); Var newNameDeclaration = t.getScope().getVar(newName); switch (parent.getToken()) { case CLASS: if (parent.getIndexOfChild(nameRef) == 0 && (newNameIsQualified || requireFunctionExpressions)) { // Refactor a named class to a class expression // We can't remove the class name during a traversal, so save it for later rewrittenClassExpressions.add(parent); Node newNameRef = NodeUtil.newQName(compiler, newName, nameRef, originalName); if (newNameIsModuleExport) { newNameRef.putBooleanProp(Node.MODULE_EXPORT, true); } Node grandparent = parent.getParent(); Node expr; if (!newNameIsQualified && newNameDeclaration == null) { expr = IR.let(newNameRef, IR.nullNode()).useSourceInfoIfMissingFromForTree(nameRef); } else { expr = IR.exprResult(IR.assign(newNameRef, IR.nullNode())) .useSourceInfoIfMissingFromForTree(nameRef); JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(parent.getJSDocInfo()); parent.setJSDocInfo(null); if (qualifiedNameIsConst) { info.recordConstancy(); } expr.getFirstChild().setJSDocInfo(info.build()); fixTypeAnnotationsForNode(t, expr.getFirstChild()); } grandparent.replaceChild(parent, expr); if (expr.isLet()) { expr.getFirstChild().replaceChild(expr.getFirstFirstChild(), parent); } else { expr.getFirstChild().replaceChild(expr.getFirstChild().getSecondChild(), parent); } } else if (parent.getIndexOfChild(nameRef) == 1) { Node newNameRef = NodeUtil.newQName(compiler, newName, nameRef, originalName); if (newNameIsModuleExport) { newNameRef.putBooleanProp(Node.MODULE_EXPORT, true); } parent.replaceChild(nameRef, newNameRef); } else { nameRef.setString(newName); nameRef.setOriginalName(originalName); } break; case FUNCTION: if (newNameIsQualified || requireFunctionExpressions) { // Refactor a named function to a function expression if (NodeUtil.isFunctionExpression(parent)) { // Don't refactor if the parent is a named function expression. // e.g. var foo = function foo() {}; return; } Node newNameRef = NodeUtil.newQName(compiler, newName, nameRef, originalName); if (newNameIsModuleExport) { newNameRef.putBooleanProp(Node.MODULE_EXPORT, true); } Node grandparent = parent.getParent(); nameRef.setString(""); Node expr; if (!newNameIsQualified && newNameDeclaration == null) { expr = IR.var(newNameRef, IR.nullNode()).useSourceInfoIfMissingFromForTree(nameRef); } else { expr = IR.exprResult(IR.assign(newNameRef, IR.nullNode())) .useSourceInfoIfMissingFromForTree(nameRef); } grandparent.replaceChild(parent, expr); if (expr.isVar()) { expr.getFirstChild().replaceChild(expr.getFirstFirstChild(), parent); } else { expr.getFirstChild().replaceChild(expr.getFirstChild().getSecondChild(), parent); JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(parent.getJSDocInfo()); parent.setJSDocInfo(null); if (qualifiedNameIsConst) { info.recordConstancy(); } expr.getFirstChild().setJSDocInfo(info.build()); fixTypeAnnotationsForNode(t, expr.getFirstChild()); } functionsToHoist.add(expr); } else { nameRef.setString(newName); nameRef.setOriginalName(originalName); } break; case VAR: case LET: case CONST: // Multiple declaration - needs split apart. if (parent.hasMoreThanOneChild() && !NodeUtil.isAnyFor(parent.getParent())) { splitMultipleDeclarations(parent); parent = nameRef.getParent(); newNameDeclaration = t.getScope().getVar(newName); } if (newNameIsQualified) { // Var declarations without initialization can simply // be removed if they are being converted to a property. if (!nameRef.hasChildren() && parent.getJSDocInfo() == null) { parent.detach(); break; } // Refactor a var declaration to a getprop assignment Node getProp = NodeUtil.newQName(compiler, newName, nameRef, originalName); if (newNameIsModuleExport) { getProp.putBooleanProp(Node.MODULE_EXPORT, true); } JSDocInfo info = parent.getJSDocInfo(); parent.setJSDocInfo(null); if (nameRef.hasChildren()) { Node assign = IR.assign(getProp, nameRef.removeFirstChild()); assign.setJSDocInfo(info); Node expr = IR.exprResult(assign).useSourceInfoIfMissingFromForTree(nameRef); parent.replaceWith(expr); JSDocInfoBuilder infoBuilder = JSDocInfoBuilder.maybeCopyFrom(info); parent.setJSDocInfo(null); if (qualifiedNameIsConst) { infoBuilder.recordConstancy(); } assign.setJSDocInfo(infoBuilder.build()); fixTypeAnnotationsForNode(t, assign); } else { getProp.setJSDocInfo(info); parent.replaceWith(IR.exprResult(getProp).useSourceInfoFrom(getProp)); } } else if (newNameDeclaration != null && newNameDeclaration.getNameNode() != nameRef) { // Variable is already defined. Convert this to an assignment. // If the variable declaration has no initialization, we simply // remove the node. This can occur when the variable which is exported // is declared in an outer scope but assigned in an inner one. if (!nameRef.hasChildren()) { parent.detachFromParent(); break; } Node name = NodeUtil.newName(compiler, newName, nameRef, originalName); Node assign = IR.assign(name, nameRef.removeFirstChild()); JSDocInfo info = parent.getJSDocInfo(); if (info != null) { parent.setJSDocInfo(null); assign.setJSDocInfo(info); } parent.replaceWith(IR.exprResult(assign).useSourceInfoFromForTree(nameRef)); } else { nameRef.setString(newName); nameRef.setOriginalName(originalName); } break; default: // Whenever possible, reuse the existing reference if (!newNameIsQualified && nameRef.isName()) { nameRef.setString(newName); nameRef.setOriginalName(originalName); } else { Node name = newNameIsQualified ? NodeUtil.newQName(compiler, newName, nameRef, originalName) : NodeUtil.newName(compiler, newName, nameRef, originalName); if (newNameIsModuleExport) { name.putBooleanProp(Node.MODULE_EXPORT, true); } JSDocInfo info = nameRef.getJSDocInfo(); if (info != null) { nameRef.setJSDocInfo(null); name.setJSDocInfo(info); } name.useSourceInfoFromForTree(nameRef); parent.replaceChild(nameRef, name); if (nameRef.hasChildren()) { name.addChildrenToFront(nameRef.removeChildren()); } } break; } t.reportCodeChange(); } /** * Determine whether the given name Node n is referenced in an export * * @return string - If the name is not used in an export, return it's own name If the name node * is actually the export target itself, return null; */ private String getExportedName(NodeTraversal t, Node n, Var var) { if (var == null || !Objects.equals(var.getNode().getInputId(), n.getInputId())) { return n.getQualifiedName(); } String baseExportName = getBasePropertyImport(getModuleName(t.getInput())); for (ExportInfo export : this.exports) { Node exportBase = getBaseQualifiedNameNode(export.node); Node exportRValue = NodeUtil.getRValueOfLValue(exportBase); if (exportRValue == null) { continue; } Node exportedName = getExportedNameNode(export); // We don't want to handle the export itself if (export.isInSupportedScope && (exportRValue == n || ((NodeUtil.isClassExpression(exportRValue) || NodeUtil.isFunctionExpression(exportRValue)) && exportedName == n))) { return null; } String exportBaseQName = exportBase.getQualifiedName(); if (exportRValue.isObjectLit()) { if (!"module.exports".equals(exportBaseQName)) { return n.getQualifiedName(); } Node key = exportRValue.getFirstChild(); boolean keyIsExport = false; while (key != null) { if (key.isStringKey() && !key.isQuotedString() && NodeUtil.isValidPropertyName( compiler.getOptions().getLanguageIn().toFeatureSet(), key.getString())) { if (key.hasChildren()) { if (key.getFirstChild().isQualifiedName()) { if (key.getFirstChild() == n) { return null; } Var valVar = t.getScope().getVar(key.getFirstChild().getQualifiedName()); if (valVar != null && valVar.getNameNode() == var.getNameNode()) { keyIsExport = true; break; } } } else { if (key == n) { return null; } // Handle ES6 object lit shorthand assignments Var valVar = t.getScope().getVar(key.getString()); if (valVar != null && valVar.getNameNode() == var.getNameNode()) { keyIsExport = true; break; } } } key = key.getNext(); } if (key != null && keyIsExport) { if (export.isInSupportedScope) { return baseExportName + "." + key.getString(); } else { return n.getQualifiedName(); } } } else { if (!export.isInSupportedScope) { return n.getQualifiedName(); } if (var.getNameNode() == exportedName) { String exportPrefix; if (exportBaseQName.startsWith(MODULE)) { exportPrefix = MODULE + "." + EXPORTS; } else { exportPrefix = EXPORTS; } if (exportBaseQName.length() == exportPrefix.length()) { return baseExportName; } return baseExportName + exportBaseQName.substring(exportPrefix.length()); } } } return n.getQualifiedName(); } private Node getExportedNameNode(ExportInfo info) { Node qNameBase = getBaseQualifiedNameNode(info.node); Node rValue = NodeUtil.getRValueOfLValue(qNameBase); if (rValue == null) { return null; } if (NodeUtil.isFunctionExpression(rValue)) { return rValue.getFirstChild(); } if (NodeUtil.isClassExpression(rValue) && rValue.getFirstChild() == qNameBase) { return rValue.getFirstChild(); } if (!rValue.isName()) { return null; } Var var = info.scope.getVar(rValue.getString()); if (var == null) { return null; } return var.getNameNode(); } /** * Determine if the given Node n is an alias created by a module import. * * @return null if it's not an alias or the imported module name */ private String getModuleImportName(NodeTraversal t, Node n) { Node rValue = null; String propSuffix = ""; Node parent = n.getParent(); if (parent != null && parent.isStringKey()) { Node grandparent = parent.getParent(); if (grandparent.isObjectPattern() && grandparent.getParent().isDestructuringLhs()) { rValue = grandparent.getNext(); propSuffix = "." + parent.getString(); } } if (propSuffix.isEmpty() && parent != null) { rValue = NodeUtil.getRValueOfLValue(n); } if (rValue == null) { return null; } if (rValue.isCall() && isCommonJsImport(rValue)) { return getBasePropertyImport(getImportedModuleName(t, rValue), rValue) + propSuffix; } else if (rValue.isGetProp() && isCommonJsImport(rValue.getFirstChild())) { // var foo = require('bar').foo; String importName = getBasePropertyImport( getImportedModuleName(t, rValue.getFirstChild()), rValue.getFirstChild()); String suffix = rValue.getSecondChild().isGetProp() ? rValue.getSecondChild().getQualifiedName() : rValue.getSecondChild().getString(); return importName + "." + suffix + propSuffix; } return null; } /** * Update any type references in JSDoc annotations to account for all the rewriting we've done. */ private void fixTypeNode(NodeTraversal t, Node typeNode) { if (typeNode.isString()) { String name = typeNode.getString(); // Type nodes can be module paths. if (ModuleLoader.isPathIdentifier(name)) { int lastSlash = name.lastIndexOf('/'); int endIndex = name.indexOf('.', lastSlash); String localTypeName = null; if (endIndex == -1) { endIndex = name.length(); } else { localTypeName = name.substring(endIndex); } String moduleName = name.substring(0, endIndex); String globalModuleName = getImportedModuleName(t, typeNode, moduleName); String baseImportProperty = getBasePropertyImport(globalModuleName); typeNode.setString( localTypeName == null ? baseImportProperty : baseImportProperty + localTypeName); } else { // A type node can be a getprop. Any portion of the getprop // can be either an import alias or export alias. Check each // segment. boolean wasRewritten = false; int endIndex = -1; while (endIndex < name.length()) { endIndex = name.indexOf('.', endIndex + 1); if (endIndex == -1) { endIndex = name.length(); } String baseName = name.substring(0, endIndex); String suffix = endIndex < name.length() ? name.substring(endIndex) : ""; Var typeDeclaration = t.getScope().getVar(baseName); // Make sure we can find a variable declaration (and it's in this file) if (typeDeclaration != null && typeDeclaration.getNode() != null && Objects.equals(typeDeclaration.getNode().getInputId(), typeNode.getInputId())) { String importedModuleName = getModuleImportName(t, typeDeclaration.getNode()); // If the name is an import alias, rewrite it to be a reference to the // module name directly if (importedModuleName != null) { typeNode.setString(importedModuleName + suffix); typeNode.setOriginalName(name); wasRewritten = true; break; } else if (this.allowFullRewrite) { // Names referenced in export statements can only be rewritten in // commonjs modules. String exportedName = getExportedName(t, typeNode, typeDeclaration); if (exportedName != null && !exportedName.equals(name)) { typeNode.setString(exportedName + suffix); typeNode.setOriginalName(name); wasRewritten = true; break; } } } } // If the name was neither an import alias or referenced in an export, // We still may need to rename it if it's global if (!wasRewritten && this.allowFullRewrite) { endIndex = name.indexOf('.'); if (endIndex == -1) { endIndex = name.length(); } String baseName = name.substring(0, endIndex); Var typeDeclaration = t.getScope().getVar(baseName); if (typeDeclaration != null && typeDeclaration.isGlobal()) { String moduleName = getModuleName(t.getInput()); String newName = baseName + "$$" + moduleName; if (endIndex < name.length()) { newName += name.substring(endIndex); } typeNode.setString(newName); typeNode.setOriginalName(name); } } } } for (Node child = typeNode.getFirstChild(); child != null; child = child.getNext()) { fixTypeNode(t, child); } } private List splitMultipleDeclarations(Node var) { checkState(NodeUtil.isNameDeclaration(var)); List vars = new ArrayList<>(); JSDocInfo info = var.getJSDocInfo(); while (var.getSecondChild() != null) { Node newVar = new Node(var.getToken(), var.removeFirstChild()); if (info != null) { newVar.setJSDocInfo(info.clone()); } newVar.useSourceInfoFrom(var); var.getParent().addChildBefore(newVar, var); vars.add(newVar); } vars.add(var); return vars; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy