Package com.google.javascript.jscomp

Source Code of com.google.javascript.jscomp.ClosureRewriteModule$ModuleDescription

/*
* Copyright 2014 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.ImmutableSet;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* Process aliases in goog.scope blocks.
* <pre>
* goog.module('namespace');
* var foo = goog.require('another.namespace');
*
* should become
*
* goog.provide('namespace');
* goog.require('another.namespace');
* etc
* </pre>
*
* @author johnlenz@google.com (John Lenz)
*/
public class ClosureRewriteModule
    implements NodeTraversal.Callback, HotSwapCompilerPass {

  // TODO(johnlenz): Don't use goog.scope as an intermediary add type checker
  // support instead.
  // TODO(johnlenz): harden this class to warn about misuse
  // TODO(johnlenz): handle non-namespace module identifiers aka 'foo/bar'

  static final DiagnosticType INVALID_MODULE_IDENTIFIER =
      DiagnosticType.error(
          "JSC_GOOG_MODULE_INVALID_MODULE_IDENTIFIER",
          "Module idenifiers must be string literals");

  static final DiagnosticType INVALID_REQUIRE_IDENTIFIER =
      DiagnosticType.error(
          "JSC_GOOG_MODULE_INVALID_REQUIRE_IDENTIFIER",
          "goog.require parameter must be a string literal.");

  static final DiagnosticType INVALID_GET_IDENTIFIER =
      DiagnosticType.error(
          "JSC_GOOG_MODULE_INVALID_GET_IDENTIFIER",
          "goog.module.get parameter must be a string literal.");

  static final DiagnosticType INVALID_GET_CALL_SCOPE =
      DiagnosticType.error(
          "JSC_GOOG_MODULE_INVALID_GET_CALL_SCOPE",
          "goog.module.get can not be called in global scope.");

  private final AbstractCompiler compiler;

  private static class ModuleDescription {
    Node moduleDecl;
    String moduleNamespace = "";
    Node requireInsertNode = null;
    final Node moduleScopeRoot;
    final Node moduleStatementRoot;
    final List<Node> requires = new ArrayList<>();
    final List<Node> provides = new ArrayList<>();
    final List<Node> exports = new ArrayList<>();
    public Scope moduleScope = null;

    ModuleDescription(Node n) {
      if (isLoadModuleCall(n)) {
        this.moduleScopeRoot = getModuleScopeRootForLoadModuleCall(n);
        this.moduleStatementRoot = getModuleStatementRootForLoadModuleCall(n);
      } else {
        this.moduleScopeRoot = n;
        this.moduleStatementRoot = n;
      }
    }
  }

  // Per "goog.module" state need for rewriting.
  private ModuleDescription current = null;

  ClosureRewriteModule(AbstractCompiler compiler) {
    this.compiler = compiler;
  }

  @Override
  public void process(Node externs, Node root) {
    // Each module is its own scope, prevent building a global scope,
    // so we can use the scope for the file.
    // TODO(johnlenz): this is a little odd, rework this once we have
    // a concept of a module scope.
    for (Node c = root.getFirstChild(); c != null; c = c.getNext()) {
      Preconditions.checkState(c.isScript());
      hotSwapScript(c, null);
    }
  }

  @Override
  public void hotSwapScript(Node scriptRoot, Node originalRoot) {
    NodeTraversal.traverse(compiler, scriptRoot, this);
  }

  @Override
  public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
    if (isModuleFile(n) || isLoadModuleCall(n)) {
      enterModule(n);
    }
    if (isGetModuleCall(n)) {
      rewriteGetModuleCall(t, n);
    }
    return true;
  }

  private static boolean isLoadModuleCall(Node n) {
    return n.isCall()
        && n.getFirstChild().matchesQualifiedName("goog.loadModule");
  }

  private static boolean isGetModuleCall(Node n) {
    return n.isCall()
        && n.getFirstChild().matchesQualifiedName("goog.module.get");
  }

  private void rewriteGetModuleCall(NodeTraversal t, Node n) {
    // "use(goog.module.get('a.namespace'))" to "use(a.namespace)"
    Node namespace = n.getFirstChild().getNext();
    if (!namespace.isString()) {
      t.report(namespace, INVALID_GET_IDENTIFIER);
      return;
    }

    if (!inModule() && t.inGlobalScope()) {
      t.report(namespace, INVALID_GET_CALL_SCOPE);
      return;
    }

    Node replacement = NodeUtil.newQName(compiler, namespace.getString());
    replacement.srcrefTree(namespace);

    n.getParent().replaceChild(n, replacement);
    compiler.reportCodeChange();
  }


  private static boolean isModuleFile(Node n) {
    return n.isScript() && n.hasChildren()
        && isGoogModuleCall(n.getFirstChild());
  }

  private void enterModule(Node n) {
    current = new ModuleDescription(n);
  }

  private boolean inModule() {
    return current != null;
  }

  private static boolean isGoogModuleCall(Node n) {
    if (NodeUtil.isExprCall(n)) {
      Node target = n.getFirstChild().getFirstChild();
      return (target.matchesQualifiedName("goog.module"));
    }
    return false;
  }

  /**
   * Rewrite:
   *   goog.module('foo')
   *   var bar = goog.require('bar');
   *   exports = something;
   * to:
   *   goog.provide('foo');
   *   goog.require('ns.bar');
   *   goog.scope(function() {
   *     var bar = ns.bar;
   *     foo = something;
   *   });
   */
  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    if (!inModule()) {
      // Nothing to do if we aren't within a module file.
      return;
    }

    switch (n.getType()) {
      case Token.BLOCK:
        if (current.moduleScopeRoot == parent && parent.isFunction()) {
          current.moduleScope = t.getScope();
        }
        break;

      case Token.EXPR_RESULT:
        if (n.getFirstChild().isCall()) {
          Node target = n.getFirstChild().getFirstChild();
          if (target.matchesQualifiedName(
              "goog.module.declareLegacyNamespace")) {
            n.detachFromParent();
          }
        }
        break;

      case Token.CALL:
        Node first = n.getFirstChild();
        if (first.matchesQualifiedName("goog.module")) {
          recordAndUpdateModule(t, n);
        } else if (first.matchesQualifiedName("goog.require")) {
          recordRequire(t, n);
        } else if (isLoadModuleCall(n)) {
          rewriteModuleAsScope(n);
        }
        break;

      case Token.NAME:
        if (n.getString().equals("exports")) {
          current.exports.add(n);
        }
        break;

      case Token.SCRIPT:
        current.moduleScope = t.getScope();
        // Exiting the script, fixup everything else;
        rewriteModuleAsScope(n);
        break;

      case Token.RETURN:
        if (t.getScopeRoot() == current.moduleScopeRoot) {
          n.detachFromParent();
        }
        break;
    }
  }

  private void recordAndUpdateModule(NodeTraversal t, Node call) {
    Node idNode = call.getLastChild();
    if (!idNode.isString()) {
      t.report(idNode, INVALID_MODULE_IDENTIFIER);
      return;
    }

    current.moduleNamespace = idNode.getString();
    current.moduleDecl = call;

    // rewrite "goog.module('foo')" to "goog.provide('foo')"
    Node target = call.getFirstChild();
    target.getLastChild().setString("provide");

    current.provides.add(call);
  }

  private void recordRequire(NodeTraversal t, Node call) {
    Node idNode = call.getLastChild();
    if (!idNode.isString()) {
      t.report(idNode, INVALID_REQUIRE_IDENTIFIER);
      return;
    }
    current.requires.add(call);
  }

  private void updateRequires(List<Node> requires) {
    for (Node node : requires) {
      updateRequire(node);
    }
  }

  private void updateRequire(Node call) {
    Node idNode = call.getLastChild();
    String namespace = idNode.getString();
    if (current.requireInsertNode == null) {
      current.requireInsertNode = getInsertRoot(call);
    }

    // rewrite:
    //   var foo = goog.require('ns.foo')
    // to
    //   goog.provide('foo');
    //   var foo = ns.foo;

    // replace the goog.require statementment with a reference to the
    // namespace.
    Node replacement = NodeUtil.newQName(compiler, namespace).srcrefTree(call);
    call.getParent().replaceChild(call, replacement);

    // readd the goog.require statement
    Node require = IR.exprResult(call).srcref(call);
    Node insertAt = current.requireInsertNode;
    insertAt.getParent().addChildBefore(require, insertAt);
  }

  private List<String> collectRoots(ModuleDescription module) {
    List<String> result = new ArrayList<>();
    for (Node n : module.provides) {
      result.add(getRootName(n.getFirstChild().getNext()));
    }
    for (Node n : module.requires) {
      result.add(getRootName(n.getFirstChild().getNext()));
    }
    return result;
  }

  private String getRootName(Node n) {
    String qname = n.getString();
    int endPos = qname.indexOf('.');
    return (endPos == -1) ? qname : qname.substring(0, endPos);
  }

  private void rewriteModuleAsScope(Node root) {
    // Moving everything following the goog.module/goog.requires into a
    // goog.scope so that the aliases can be resolved.

    Node moduleRoot = current.moduleStatementRoot;

    // The moduleDecl will be null if it is invalid.
    Node srcref = current.moduleDecl != null ? current.moduleDecl : root;

    ImmutableSet<String> roots = ImmutableSet.copyOf(collectRoots(current));
    updateRootShadows(current.moduleScope, roots);
    updateRequires(current.requires);
    updateExports(current.exports);


    Node block = IR.block();
    Node scope = IR.exprResult(IR.call(
        IR.getprop(IR.name("goog"), IR.string("scope")),
        IR.function(IR.name(""), IR.paramList(), block)))
        .srcrefTree(srcref);

    // Skip goog.module, etc.
    Node fromNode = skipHeaderNodes(moduleRoot);
    Preconditions.checkNotNull(fromNode);
    moveChildrenAfter(fromNode, block);
    moduleRoot.addChildAfter(scope, fromNode);

    if (root.isCall()) {
      Node expr = root.getParent();
      Preconditions.checkState(expr.isExprResult(), expr);
      expr.getParent().addChildrenAfter(moduleRoot.removeChildren(), expr);
      expr.detachFromParent();
    }
    compiler.reportCodeChange();

    // reset the module.
    current = null;
  }

  private void updateExports(List<Node> exports) {
    for (Node n : exports) {
      Node replacement = NodeUtil.newQName(compiler, current.moduleNamespace);
      replacement.srcrefTree(n);
      n.getParent().replaceChild(n, replacement);
    }
  }

  private void updateRootShadows(Scope s, ImmutableSet<String> roots) {
    final Map<String, String> nameMap = new HashMap<>();
    for (String root : roots) {
      if (s.getOwnSlot(root) != null) {
        nameMap.put(root, root + "_module");
      }
    }

    if (nameMap.isEmpty()) {
      // Don't traverse if there is nothing to do.
      return;
    }

    new NodeTraversal(compiler, new AbstractPostOrderCallback() {
      @Override
      public void visit(NodeTraversal t, Node n, Node parent) {
        if (n.isName()) {
          String rename = nameMap.get(n.getString());
          if (rename != null) {
            n.setString(rename);
          }
        }
      }
    }).traverseAtScope(s);
  }

  private static Node getModuleScopeRootForLoadModuleCall(Node n) {
    Preconditions.checkState(n.isCall());
    Node fn = n.getLastChild();
    Preconditions.checkState(fn.isFunction());
    return fn;
  }

  private static Node getModuleStatementRootForLoadModuleCall(Node n) {
    Node fn = getModuleScopeRootForLoadModuleCall(n);
    Node block = fn.getLastChild();
    Preconditions.checkState(block.isBlock());
    return block;
  }

  private Node skipHeaderNodes(Node script) {
    Node lastHeaderNode = null;
    Node child = script.getFirstChild();
    while (child != null && isHeaderNode(child)) {
      lastHeaderNode = child;
      child = child.getNext();
    }
    return lastHeaderNode;
  }

  private boolean isHeaderNode(Node n) {
    if (n.isEmpty()) {
      return true;
    }
    if (NodeUtil.isExprCall(n)) {
      Node target = n.getFirstChild().getFirstChild();
      return (
          target.matchesQualifiedName("goog.module")
          || target.matchesQualifiedName("goog.provide")
          || target.matchesQualifiedName("goog.require")
          || target.matchesQualifiedName("goog.setTestOnly"));
    }
    return false;
  }

  private void moveChildrenAfter(Node fromNode, Node targetBlock) {
    Node parent = fromNode.getParent();
    while (fromNode.getNext() != null) {
      Node child = parent.removeChildAfter(fromNode);
      targetBlock.addChildToBack(child);
    }
  }

  private Node getInsertRoot(Node n) {
    while (n.getParent() != current.moduleStatementRoot) {
      n = n.getParent();
    }
    return n;
  }
}
TOP

Related Classes of com.google.javascript.jscomp.ClosureRewriteModule$ModuleDescription

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.