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.

There is a newer version: v20240317
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.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__";
  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.traverseEs6(compiler, root, 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();
      NodeTraversal.traverseEs6(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();
      boolean needsRetraverse = false;
      if (isCommonJsModule || forceModuleDetection) {
        finder.reportModuleErrors();

        if (!finder.umdPatterns.isEmpty()) {
          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();
            NodeTraversal.traverseEs6(compiler, n, finder);
          }
        }

        defaultExportIsConst = finder.initializeModule();

        //UMD pattern replacement can leave detached export references - don't include those
        for (ExportInfo export : finder.getModuleExports()) {
          if (NodeUtil.getEnclosingScript(export.node) != null) {
            exports.add(export);
          }
        }
        for (ExportInfo export : finder.getExports()) {
          if (NodeUtil.getEnclosingScript(export.node) != null) {
            exports.add(export);
          }
        }
      } else if (needsRetraverse) {
        finder = new FindImportsAndExports();
        NodeTraversal.traverseEs6(compiler, n, finder);
      }

      NodeTraversal.traverseEs6(
          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();
  }

  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 *
*/ 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.getSecondChild().isNumber() || requireCall.getSecondChild().isString())) { return true; } else if (requireCall.getFirstChild().matchesQualifiedName(REQUIRE) && requireCall.getSecondChild().isString()) { return true; } } else if (requireCall.isCall() && requireCall.getChildCount() == 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)) { 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);}) *
*/ public static boolean isCommonJsDynamicImportCallback( Node n, ModuleLoader.ResolutionMode resolutionMode) { if (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); }) */ 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() && callParent .getFirstFirstChild() .getFirstChild() .matchesQualifiedName(WEBPACK_REQUIRE + ".e") && callParent.getFirstChild().getSecondChild().isString() && callParent.getFirstChild().getSecondChild().getString().equals("then")) { return true; } 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; ExportInfo(Node node, Scope scope) { this.node = node; this.scope = scope; } } 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 fnc; if (call.getFirstChild().isFunction()) { fnc = n.getFirstFirstChild(); } else if (call.getFirstChild().isGetProp() && call.getFirstFirstChild().isFunction() && call.getFirstFirstChild().getNext().matchesQualifiedName("call")) { fnc = call.getFirstFirstChild(); } else { return; } 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; } if (arg.isCall() && arg.getFirstChild().isCall() && isCommonJsImport(arg.getFirstChild()) && arg.getSecondChild().isName() && arg.getSecondChild().getString().equals(MODULE)) { String importPath = getCommonJsImportPath(arg.getFirstChild()); 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 { 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<>(); 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) { if (t.inGlobalScope()) { // 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)) { if (isCommonJsExport(t, n)) { moduleExports.add(new ExportInfo(n, t.getScope())); // If the module.exports statement is nested in the then branch of an if statement, // assume the if statement is an UMD pattern with a common js export in the then branch // This seems fragile but has worked well for a long time. // TODO(ChadKillingsworth): Discover if there is a better way to detect these. Node ifAncestor = getOutermostIfAncestor(parent); if (ifAncestor != null && (NodeUtil.isLValue(n) || isInIfTest(n))) { UmdPattern existingPattern = findUmdPattern(umdPatterns, ifAncestor); if (existingPattern != null) { umdPatterns.remove(existingPattern); } Node enclosingIf = NodeUtil.getEnclosingNode( n, new Predicate() { @Override public boolean apply(Node node) { return node.isIf() || node.isHook(); } }); umdPatterns.add(new UmdPattern(ifAncestor, enclosingIf.getSecondChild())); } } } else if (n.matchesQualifiedName("define.amd")) { // If a define.amd statement is nested in the then branch of an if statement, // assume the if statement is an UMD pattern with a common js export // in the else branch // This seems fragile but has worked well for a long time. // TODO(ChadKillingsworth): Discover if there is a better way to detect these. Node ifAncestor = getOutermostIfAncestor(parent); if (ifAncestor != null && findUmdPattern(umdPatterns, ifAncestor) == null && (NodeUtil.isLValue(n) || isInIfTest(n))) { umdPatterns.add(new UmdPattern(ifAncestor, ifAncestor.getChildAtIndex(2))); } } 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())); } else if (!this.hasGoogProvideOrModule) { errors.add(t.makeError(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, // assume the if statement is an UMD pattern with a common js export in the then branch // This seems fragile but has worked well for a long time. // TODO(ChadKillingsworth): Discover if there is a better way to detect these. Node ifAncestor = getOutermostIfAncestor(parent); if (ifAncestor != null && findUmdPattern(umdPatterns, ifAncestor) == null && (NodeUtil.isLValue(n) || isInIfTest(n))) { umdPatterns.add(new UmdPattern(ifAncestor, ifAncestor.getSecondChild())); } } } } 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.getChildCount() != 3) { compiler.report( t.makeError( 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( t.makeError( 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( t.makeError( 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().getChildCount() == 1 && callback.getSecondChild().getFirstChild().isName() && "require".equals(callback.getSecondChild().getFirstChild().getString()))) { compiler.report( t.makeError( 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) { 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); } } } exports.removeAll(exportsToRemove); // 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++; } } } Node initModule = IR.var(IR.name(moduleName), IR.objectlit()); initModule.getFirstChild().putBooleanProp(Node.MODULE_EXPORT, true); JSDocInfoBuilder builder = new JSDocInfoBuilder(true); builder.recordConstancy(); initModule.setJSDocInfo(builder.build()); if (directAssignments == 0) { Node defaultProp = IR.stringKey(EXPORT_PROPERTY_NAME); defaultProp.putBooleanProp(Node.MODULE_EXPORT, true); defaultProp.addChildToFront(IR.objectlit()); initModule.getFirstFirstChild().addChildToFront(defaultProp); builder = new JSDocInfoBuilder(true); builder.recordConstancy(); defaultProp.setJSDocInfo(builder.build()); } this.script.addChildToFront(initModule.useSourceInfoFromForTree(this.script)); compiler.reportChangeToEnclosingScope(this.script); return directAssignments < 2; } /** Find the outermost if node ancestor for a node without leaving the function scope */ private Node getOutermostIfAncestor(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()) { Node outerIf = getOutermostIfAncestor(parent); if (outerIf != null) { return outerIf; } return parent; } return getOutermostIfAncestor(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.isNormalBlock() && umdPattern.activeBranch.getChildCount() == 1) { 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.isNormalBlock() && 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.isNormalBlock() && 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. if (n.hasMoreThanOneChild() && !NodeUtil.isAnyFor(parent)) { 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()) { 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); } } } 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")) { Var v = t.getScope().getVar(MODULE); if (v == null || v.isExtern()) { n.replaceWith(IR.string(t.getInput().getPath().toString()).useSourceInfoFrom(n)); } } 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)) .useSourceInfoFromForTree(require); 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()) { 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.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()))) { 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()) { if (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()) { // 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` root.getParent().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); } } 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 void 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(); while (key != null) { Node lhs; if (key.isQuotedString()) { lhs = IR.getelem(export.cloneTree(), IR.string(key.getString())); } else { 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; if (!key.isGetterDef()) { expr = IR.exprResult(IR.assign(lhs, value)).useSourceInfoIfMissingFromForTree(key); insertionParent.addChildAfter(expr, insertionRef); ExportInfo newExport = new ExportInfo(lhs.getFirstChild(), scope); visitExport(t, newExport); } else { String moduleName = getModuleName(t.getInput()); Var moduleVar = t.getScope().getVar(moduleName + "." + EXPORT_PROPERTY_NAME); Node defaultProp = null; if (moduleVar == null) { moduleVar = t.getScope().getVar(moduleName); if (moduleVar != null && moduleVar.getNode().getFirstChild() != null && moduleVar.getNode().getFirstChild().isObjectLit()) { defaultProp = NodeUtil.getFirstPropMatchingKey( moduleVar.getNode().getFirstChild(), EXPORT_PROPERTY_NAME); } } else if (moduleVar.getNode().getFirstChild() != null && moduleVar.getNode().getFirstChild().isObjectLit()) { defaultProp = moduleVar.getNode().getFirstChild(); } if (defaultProp != null) { Node getter = key.detach(); defaultProp.addChildToBack(getter); } } // Export statements can be removed in visitExport if (expr != null && expr.getParent() != null) { insertionRef = expr; } key = key.getNext(); } export.getParent().getParent().detach(); } /** * 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) { // 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()) { 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.getChildCount() > 1) { 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: { 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); } 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 (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) { return baseExportName + "." + key.getString(); } } else { 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) || NodeUtil.isClassExpression(rValue)) { return rValue.getFirstChild(); } Var var = info.scope.getVar(rValue.getQualifiedName()); 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)) + propSuffix; } else if (rValue.isGetProp() && isCommonJsImport(rValue.getFirstChild())) { // var foo = require('bar').foo; String importName = getBasePropertyImport(getImportedModuleName(t, 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 && 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