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 com.google.common.base.Preconditions;
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.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 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.
 * goog.provide and goog.require are emitted for closure compiler automatic
 * ordering.
 */
public final class ProcessCommonJSModules implements CompilerPass {
  private static final String EXPORTS = "exports";
  private static final String MODULE = "module";

  public static final DiagnosticType COMMON_JS_MODULE_LOAD_ERROR = DiagnosticType.error(
      "JSC_COMMONJS_MODULE_LOAD_ERROR",
      "Failed to load module \"{0}\"");

  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 Compiler compiler;
  private final boolean reportDependencies;

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

  /**
   * Creates a new ProcessCommonJSModules instance which can be used to
   * rewrite CommonJS modules to a concatenable form.
   *
   * @param compiler The compiler
   * @param reportDependencies Whether the rewriter should report dependency
   *     information to the Closure dependency manager. This needs to be true
   *     if we want to sort CommonJS module inputs correctly. Note that goog.provide
   *     and goog.require calls will still be generated if this argument is
   *     false.
   */
  public ProcessCommonJSModules(Compiler compiler, boolean reportDependencies) {
    this.compiler = compiler;
    this.reportDependencies = reportDependencies;
  }


  /**
   * Module rewriting is done a on per-file basis prior to main compilation. The pass must handle
   * ES6+ syntax and the root node for each file is a SCRIPT - not the typical jsRoot of other
   * passes.
   */
  @Override
  public void process(Node externs, Node root) {
    Preconditions.checkState(root.isScript());
    FindImportsAndExports finder = new FindImportsAndExports();
    NodeTraversal.traverseEs6(compiler, root, finder);

    ImmutableList.Builder exports = ImmutableList.builder();
    if (finder.isCommonJsModule()) {
      finder.reportModuleErrors();

      if (!finder.umdPatterns.isEmpty()) {
        finder.replaceUmdPatterns();

        // Removing the IIFE rewrites vars. We need to re-traverse
        // to get the new references.
        if (removeIIFEWrapper(root)) {
          finder = new FindImportsAndExports();
          NodeTraversal.traverseEs6(compiler, root, finder);
        }
      }

      //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);
        }
      }

      finder.addGoogProvide();
      compiler.reportCodeChange();
    }

    NodeTraversal.traverseEs6(
        compiler, root, new RewriteModule(finder.isCommonJsModule(), exports.build()));
  }

  /**
   * 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) {
    Preconditions.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;
    }

    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"
      if (!(call.getSecondChild() != null && call.getSecondChild().isThis())) {
        return false;
      }
    } else {
      return false;
    }

    if (NodeUtil.isVarArgsFunction(fnc)) {
      return false;
    }

    CompilerInput ci = compiler.getInput(root.getInputId());
    ModulePath modulePath = ci.getPath();
    if (modulePath == null) {
      return false;
    }

    String iifeLabel = modulePath.toModuleName() + "_iifeWrapper";

    FunctionToBlockMutator mutator =
        new FunctionToBlockMutator(compiler, compiler.getUniqueNameIdSupplier());
    Node block = mutator.unwrapIifeInModule(iifeLabel, fnc, call);
    root.removeChildren();
    root.addChildrenToFront(block.removeChildren());
    compiler.reportCodeChange();

    return true;
  }

  /**
   * Traverse the script. Find all references to CommonJS require (import) and module.exports or
   * export statements. Add goog.require statements for any require 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<>();
    Set imports = new HashSet<>();
    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()) {
        Preconditions.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")) {
        Var v = t.getScope().getVar(MODULE);
        // only rewrite "module.exports" if "module" is a free variable,
        // meaning it is not defined in the current scope as a local
        // variable or function parameter
        if (v == null) {
          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 && !umdPatternsContains(umdPatterns, ifAncestor)) {
            umdPatterns.add(new UmdPattern(ifAncestor, ifAncestor.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 && !umdPatternsContains(umdPatterns, ifAncestor)) {
          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
              && EXPORTS.equals(qNameRoot.getQualifiedName())
              && NodeUtil.isLValue(qNameRoot)) {
            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 && !umdPatternsContains(umdPatterns, ifAncestor)) {
              umdPatterns.add(new UmdPattern(ifAncestor, ifAncestor.getSecondChild()));
            }
          }
        }
      }

      if (n.isCall()
          && n.hasTwoChildren()
          && n.getFirstChild().matchesQualifiedName("require")
          && n.getSecondChild().isString()) {
        visitRequireCall(t, n, parent);
      }
    }

    /** Visit require calls. Emit corresponding goog.require call. */
    private void visitRequireCall(NodeTraversal t, Node require, Node parent) {
      String requireName = require.getSecondChild().getString();
      ModulePath modulePath = t.getInput().getPath().resolveCommonJsModule(requireName);
      if (modulePath == null) {
        compiler.report(t.makeError(require, COMMON_JS_MODULE_LOAD_ERROR, requireName));
        return;
      }


      String moduleName = modulePath.toModuleName();

      // 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 goog.require
      // call inserted will import the module.
      if (!NodeUtil.isExpressionResultUsed(require)
          && parent.isExprResult()
          && NodeUtil.isStatementBlock(parent.getParent())) {
        parent.getParent().removeChild(parent);
      }

      if (imports.add(moduleName)) {
        if (reportDependencies) {
          t.getInput().addRequire(moduleName);
        }
        this.script.addChildToFront(
            IR.exprResult(
                    IR.call(
                        IR.getprop(IR.name("goog"), IR.string("require")), IR.string(moduleName)))
                .useSourceInfoIfMissingFromForTree(require));
        compiler.reportCodeChange();
      }
    }

    /**
     * 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); compiler.reportCodeChange(); } void reportModuleErrors() { for (JSError error : errors) { compiler.report(error); } } /** * Add a goog.provide statement for the module. 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. */ void addGoogProvide() { CompilerInput ci = compiler.getInput(this.script.getInputId()); ModulePath modulePath = ci.getPath(); if (modulePath == null) { return; } String moduleName = modulePath.toModuleName(); // Add goog.provide calls. if (reportDependencies) { ci.addProvide(moduleName); } this.script.addChildToFront( IR.exprResult( IR.call(IR.getprop(IR.name("goog"), IR.string("provide")), IR.string(moduleName))) .useSourceInfoIfMissingFromForTree(this.script)); // The default declaration for the goog.provide is a constant so // we need to declare the variable if we have more than one // assignment to module.exports or those assignments are not // at the top level. // // If we assign to the variable more than once or all the assignments // are properties, initialize the variable as well. int directAssignmentsAtTopLevel = 0; 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++; if (export.node.getParent().getParent().isExprResult() && NodeUtil.isTopLevel(export.node.getParent().getParent().getParent())) { directAssignmentsAtTopLevel++; } } } } if (directAssignmentsAtTopLevel > 1 || (directAssignmentsAtTopLevel == 0 && directAssignments > 0) || directAssignments == 0) { int totalExportStatements = this.moduleExports.size() + this.exports.size(); Node initModule = IR.var(IR.name(moduleName)); if (directAssignments < totalExportStatements) { initModule.getFirstChild().addChildToFront(IR.objectlit()); // If all the assignments are property exports, initialize the // module as a namespace if (directAssignments == 0) { JSDocInfoBuilder builder = new JSDocInfoBuilder(true); builder.recordConstancy(); initModule.setJSDocInfo(builder.build()); } } initModule.useSourceInfoIfMissingFromForTree(this.script); Node refChild = this.script.getFirstChild(); while (refChild.getNext() != null && refChild.getNext().isExprResult() && refChild.getNext().getFirstChild().isCall() && (refChild.getNext().getFirstFirstChild().matchesQualifiedName("goog.require") || refChild.getNext().getFirstFirstChild().matchesQualifiedName("goog.provide"))) { refChild = refChild.getNext(); } this.script.addChildAfter(initModule, refChild); } } /** 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; } if (parent.isIf() && parent.getFirstChild() == n) { Node outerIf = getOutermostIfAncestor(parent); if (outerIf != null) { return outerIf; } return parent; } return getOutermostIfAncestor(parent); } /** Remove a Universal Module Definition and leave just the commonjs export statement */ void replaceUmdPatterns() { for (UmdPattern umdPattern : umdPatterns) { Node p = umdPattern.ifRoot.getParent(); Node newNode = umdPattern.activeBranch; if (newNode == null) { p.removeChild(umdPattern.ifRoot); return; } // Remove redundant block node. Not strictly necessary, but makes tests more legible. if (umdPattern.activeBranch.isBlock() && umdPattern.activeBranch.getChildCount() == 1) { newNode = umdPattern.activeBranch.getFirstChild(); umdPattern.activeBranch.detachChildren(); } else { umdPattern.ifRoot.detachChildren(); } p.replaceChild(umdPattern.ifRoot, newNode); } if (!umdPatterns.isEmpty()) { compiler.reportCodeChange(); } } } private static boolean umdPatternsContains(List umdPatterns, Node n) { for (UmdPattern umd : umdPatterns) { if (umd.ifRoot == n) { return true; } } return false; } /** * 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<>(); public RewriteModule(boolean allowFullRewrite, ImmutableCollection exports) { this.allowFullRewrite = allowFullRewrite; this.exports = exports; } @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())); compiler.reportCodeChange(); } for (ExportInfo export : exports) { visitExport(t, export.node); } for (Node require : imports) { visitRequireCall(t, require, require.getParent()); } break; case CALL: if (n.hasTwoChildren() && n.getFirstChild().matchesQualifiedName("require") && n.getSecondChild().isString()) { imports.add(n); } break; case NAME: { String qName = n.getQualifiedName(); if (qName == null) { break; } Var nameDeclaration = t.getScope().getVar(qName); if (nameDeclaration != null && nameDeclaration.getNode() != null && nameDeclaration.getNode().getInputId() == n.getInputId()) { maybeUpdateName(t, n, nameDeclaration); } break; } // ES6 object literal shorthand notation can refer to renamed variables case STRING_KEY: { if (n.hasChildren() || n.isQuotedString() || n.getParent().getParent().isDestructuringLhs()) { 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; } default: break; } 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 requireName = require.getSecondChild().getString(); ModulePath modulePath = t.getInput().getPath().resolveCommonJsModule(requireName); if (modulePath == null) { compiler.report(t.makeError(require, COMMON_JS_MODULE_LOAD_ERROR, requireName)); return; } String moduleName = modulePath.toModuleName(); Node moduleRef = IR.name(moduleName).srcref(require); parent.replaceChild(require, moduleRef); compiler.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, Node export) { Node root = getBaseQualifiedNameNode(export); 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 ("module.exports".equals(root.getQualifiedName())) { if (rValue != null && rValue.isObjectLit() && root.getParent().isAssign() && root.getParent().getParent().isExprResult()) { expandObjectLitAssignment(t, root); return; } } ModulePath modulePath = t.getInput().getPath(); String moduleName = modulePath.toModuleName(); // 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 = t.getScope().getVar(rValue.getQualifiedName()); // If the exported name is not found and this is a direct assignment // to modules.exports, look to see if the module name has a var definition if (rValueVar == null && root == export) { rValueVar = t.getScope().getVar(moduleName); if (rValueVar != null && rValueVar.getNode() == root) { rValueVar = null; } } } if (root.getParent().isAssign() && (root.getNext().isName() || root.getNext().isGetProp()) && root.getParent().getParent().isExprResult() && rValueVar != null) { root.getParent().getParent().detachFromParent(); compiler.reportCodeChange(); return; } Node updatedExport = NodeUtil.newName(compiler, moduleName, export, export.getQualifiedName()); if ("module.exports".equals(root.getQualifiedName()) && rValue != null && t.getScope().getVar("module.exports") == null && root.getParent().isAssign() && root.getParent().getParent().isExprResult()) { // Rewrite "module.exports = foo;" to "var moduleName = foo;" Node parent = root.getParent(); Node var = IR.var(updatedExport, rValue.detach()).useSourceInfoFrom(root.getParent()); parent.getParent().replaceWith(var); } else { // Other references to "module.exports" are just replaced with the module name. export.replaceWith(updatedExport); } compiler.reportCodeChange(); } /** * Since CommonJS modules may have only a single export, it's common to see the export be an * object literal. 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) { Preconditions.checkState(export.getParent().isAssign()); Node insertionRef = export.getParent().getParent(); Preconditions.checkState(insertionRef.isExprResult()); Node insertionParent = insertionRef.getParent(); Preconditions.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.getFirstChild().detachFromParent(); } else { value = IR.name(key.getString()); } } else if (key.isMemberFunctionDef()) { value = key.getFirstChild().detach(); } Node expr = IR.exprResult(IR.assign(lhs, value)).useSourceInfoIfMissingFromForTree(key); insertionParent.addChildAfter(expr, insertionRef); visitExport(t, lhs.getFirstChild()); // Export statements can be removed in visitExport if (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) { Preconditions.checkNotNull(var); Preconditions.checkState(n.isName() || n.isGetProp()); Preconditions.checkState(n.getParent() != null); String importedModuleName = getModuleImportName(t, var.getNode()); String originalName = n.getOriginalQualifiedName(); // 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, originalName, importedModuleName, 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(originalName)) { updateNameReference(t, n, originalName, exportedName, true); // If it's a global name, rename it to prevent conflicts with other scripts } else if (var.isGlobal()) { ModulePath modulePath = t.getInput().getPath(); String currentModuleName = modulePath.toModuleName(); if (currentModuleName.equals(originalName)) { return; } // refs to 'exports' are handled separately. if (EXPORTS.equals(originalName)) { return; } // closure_test_suite looks for test*() functions if (compiler.getOptions().exportTestFunctions && currentModuleName.startsWith("test")) { return; } String newName = originalName + "$$" + currentModuleName; updateNameReference(t, n, originalName, newName, 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) { Node parent = nameRef.getParent(); Preconditions.checkNotNull(parent); Preconditions.checkNotNull(newName); boolean newNameIsQualified = newName.indexOf('.') >= 0; 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); 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); } grandparent.replaceChild(parent, expr); if (expr.isLet()) { expr.getFirstChild().replaceChild(expr.getFirstChild().getFirstChild(), parent); } else { expr.getFirstChild().replaceChild(expr.getFirstChild().getSecondChild(), parent); } } else if (parent.getIndexOfChild(nameRef) == 1) { Node newNameRef = NodeUtil.newQName(compiler, newName, nameRef, originalName); 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 Node newNameRef = NodeUtil.newQName(compiler, newName, nameRef, originalName); 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.getFirstChild().getFirstChild(), parent); } else { expr.getFirstChild().replaceChild(expr.getFirstChild().getSecondChild(), parent); } } else { nameRef.setString(newName); nameRef.setOriginalName(originalName); } break; case VAR: case LET: case CONST: if (newNameIsQualified) { // Refactor a var declaration to a getprop assignment Node getProp = NodeUtil.newQName(compiler, newName, nameRef, originalName); JSDocInfo info = parent.getJSDocInfo(); if (info != null) { parent.setJSDocInfo(null); getProp.setJSDocInfo(info); } if (nameRef.hasChildren()) { Node expr = IR.exprResult(IR.assign(getProp, nameRef.getFirstChild().detachFromParent())) .useSourceInfoIfMissingFromForTree(nameRef); parent.replaceWith(expr); } else { parent.replaceWith(IR.exprResult(getProp).useSourceInfoFrom(getProp)); } } else if (newNameDeclaration != null) { // Variable is already defined. Convert this to an assignment. Node name = NodeUtil.newName(compiler, newName, nameRef, originalName); Node assign = IR.assign(name, nameRef.getFirstChild().detachFromParent()); 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); 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; } } compiler.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 || var.getNode().getInputId() != n.getInputId()) { return n.getQualifiedName(); } String moduleName = t.getInput().getPath().toModuleName(); 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.getLanguageMode(), 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 moduleName + "." + key.getString(); } } else { if (var.getNameNode() == exportedName) { String exportPrefix = exportBaseQName.startsWith(MODULE) ? "module.exports" : EXPORTS; if (exportBaseQName.length() == exportPrefix.length()) { return moduleName; } return moduleName + 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 = ""; if (n.isStringKey() && n.getParent().isObjectPattern() && n.getParent().getParent().isDestructuringLhs()) { rValue = n.getParent().getNext(); propSuffix = "." + n.getString(); } else if (n.getParent() != null) { rValue = NodeUtil.getRValueOfLValue(n); } if (rValue == null) { return null; } if (rValue.isCall()) { // var foo = require('bar'); if (rValue.hasTwoChildren() && rValue.getFirstChild().matchesQualifiedName("require") && rValue.getSecondChild().isString() && t.getScope().getVar(rValue.getFirstChild().getQualifiedName()) == null) { String requireName = rValue.getSecondChild().getString(); ModulePath modulePath = t.getInput().getPath().resolveCommonJsModule(requireName); if (modulePath == null) { return null; } return modulePath.toModuleName() + propSuffix; } return null; } else if (rValue.isGetProp()) { // var foo = require('bar').foo; String moduleName = getModuleImportName(t, rValue.getFirstChild()); if (moduleName != null) { return moduleName + "." + n.getSecondChild().getString() + 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); ModulePath modulePath = t.getInput().getPath().resolveCommonJsModule(moduleName); if (modulePath == null) { t.makeError(typeNode, COMMON_JS_MODULE_LOAD_ERROR, moduleName); return; } String globalModuleName = modulePath.toModuleName(); typeNode.setString( localTypeName == null ? globalModuleName : globalModuleName + 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().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 = t.getInput().getPath().toModuleName(); 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); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy