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

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

/*
 * 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.JSDocInfo;
import com.google.javascript.rhino.JSDocInfoBuilder;
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;
import java.util.Set;

/**
 * Process aliases in goog.modules.
 * 
 * goog.module('namespace');
 * var foo = goog.require('another.namespace');
 * ...
 * 
* * becomes * *
 * goog.provide('namespace');
 * goog.require('another.namespace');
 * goog.scope(function() {
 *   var foo = another.namespace;
 *   ...
 * });
 * 
* * @author [email protected] (John Lenz) */ final 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."); static final DiagnosticType INVALID_GET_ALIAS = DiagnosticType.error( "JSC_GOOG_MODULE_INVALID_GET_ALIAS", "goog.module.get should not be aliased."); static final DiagnosticType INVALID_EXPORT_COMPUTED_PROPERTY = DiagnosticType.error( "JSC_GOOG_MODULE_INVALID_EXPORT_COMPUTED_PROPERTY", "Computed properties are not yet supported in goog.module exports."); static final DiagnosticType USELESS_USE_STRICT_DIRECTIVE = DiagnosticType.warning( "JSC_USELESS_USE_STRICT_DIRECTIVE", "'use strict' is unnecessary in goog.module files."); private static final ImmutableSet USE_STRICT_ONLY = ImmutableSet.of("use strict"); private final AbstractCompiler compiler; private class ModuleDescription { Node moduleDecl; String moduleNamespace = ""; Node requireInsertNode = null; final Node moduleScopeRoot; final Node moduleStatementRoot; final List requires = new ArrayList<>(); final List provides = new ArrayList<>(); final List 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.traverseEs6(compiler, scriptRoot, this); } @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { boolean isModuleFile = isModuleFile(n); if (isModuleFile) { checkStrictModeDirective(t, n); } if (isModuleFile || isLoadModuleCall(n)) { enterModule(n); } if (isGetModuleCall(n)) { rewriteGetModuleCall(t, n); } if (inModule()) { switch (n.getType()) { case Token.SCRIPT: current.moduleScope = t.getScope(); break; case Token.BLOCK: if (current.moduleScopeRoot == parent && parent.isFunction()) { current.moduleScope = t.getScope(); } break; case Token.ASSIGN: if (isGetModuleCallAlias(n)) { rewriteGetModuleCallAlias(t, n); } break; default: if (current.moduleScopeRoot == parent && parent.isBlock()) { current.moduleScope = t.getScope(); } break; } } return true; } private static void checkStrictModeDirective(NodeTraversal t, Node n) { Preconditions.checkState(n.isScript(), n); Set directives = n.getDirectives(); if (directives != null && directives.contains("use strict")) { t.report(n, USELESS_USE_STRICT_DIRECTIVE); } else { if (directives == null) { n.setDirectives(USE_STRICT_ONLY); } else { ImmutableSet.Builder builder = new ImmutableSet.Builder().add("use strict"); builder.addAll(directives); n.setDirectives(builder.build()); } } } private static boolean isCallTo(Node n, String qname) { return n.isCall() && n.getFirstChild().matchesQualifiedName(qname); } private static boolean isLoadModuleCall(Node n) { return isCallTo(n, "goog.loadModule"); } private static boolean isGetModuleCall(Node n) { return isCallTo(n, "goog.module.get"); } private void rewriteGetModuleCall(NodeTraversal t, Node n) { // "use(goog.module.get('a.namespace'))" to "use(a.namespace)" Node namespace = n.getSecondChild(); 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 void rewriteGetModuleCallAlias(NodeTraversal t, Node n) { // x = goog.module.get('a.namespace'); Preconditions.checkArgument(NodeUtil.isExprAssign(n.getParent())); Preconditions.checkArgument(n.getFirstChild().isName()); Preconditions.checkArgument(isGetModuleCall(n.getLastChild())); rewriteGetModuleCall(t, n.getLastChild()); String aliasName = n.getFirstChild().getQualifiedName(); Var alias = t.getScope().getVar(aliasName); if (alias == null) { t.report(n, INVALID_GET_ALIAS); return; } // Only rewrite if original definition was of the form: // let x = goog.forwardDeclare('a.namespace'); Node forwardDeclareCall = NodeUtil.getRValueOfLValue(alias.getNode()); if (forwardDeclareCall == null || !isCallTo(forwardDeclareCall, "goog.forwardDeclare") || forwardDeclareCall.getChildCount() != 2) { t.report(n, INVALID_GET_ALIAS); return; } Node argument = forwardDeclareCall.getLastChild(); if (!argument.isString() || !n.getLastChild().matchesQualifiedName(argument.getString())) { t.report(n, INVALID_GET_ALIAS); return; } Node replacement = NodeUtil.newQName(compiler, argument.getString()); replacement.srcrefTree(forwardDeclareCall); // Rewrite goog.forwardDeclare forwardDeclareCall.getParent().replaceChild(forwardDeclareCall, replacement); // and remove goog.module.get n.getParent().detachFromParent(); 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.getFirstFirstChild(); return (target.matchesQualifiedName("goog.module")); } return false; } private static boolean isGetModuleCallAlias(Node n) { return NodeUtil.isExprAssign(n.getParent()) && n.getFirstChild().isName() && isGetModuleCall(n.getLastChild()); } /** * 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.EXPR_RESULT: // Handle "goog.module.declareLegacyNamespace". Currently, we simply // need to remove it. if (isCallTo(n.getFirstChild(), "goog.module.declareLegacyNamespace")) { n.detachFromParent(); } break; case Token.CALL: if (isCallTo(n, "goog.module")) { recordAndUpdateModule(t, n); } else if (isCallTo(n, "goog.require")) { recordRequire(t, n); } else if (isLoadModuleCall(n)) { rewriteModuleAsScope(n); } break; case Token.GETPROP: if (isExportPropAssign(n)) { Node rhs = parent.getLastChild(); maybeUpdateExportDeclToNode(t, parent, rhs); } break; case Token.NAME: if (n.getString().equals("exports")) { current.exports.add(n); if (isAssignTarget(n)) { maybeUpdateExportObjectDecl(t, n); } } break; case Token.SCRIPT: // Exiting the script, fixup everything else; rewriteModuleAsScope(n); break; case Token.RETURN: // Remove the "return exports" for bundled goog.module files. if (parent == current.moduleStatementRoot) { n.detachFromParent(); } break; } } /** * For exports like "exports = {prop: value}" update the declarations to enforce * @const ness (and typedef exports). */ private void maybeUpdateExportObjectDecl(NodeTraversal t, Node n) { Node parent = n.getParent(); Node rhs = parent.getLastChild(); // The export declaration itself maybeUpdateExportDeclToNode(t, parent, rhs); if (rhs.isObjectLit()) { for (Node c = rhs.getFirstChild(); c != null; c = c.getNext()) { if (c.isComputedProp()) { t.report(c, INVALID_EXPORT_COMPUTED_PROPERTY); } else if (c.isStringKey()) { Node value = c.hasChildren() ? c.getFirstChild() : IR.name(c.getString()); maybeUpdateExportDeclToNode(t, c, value); } } } } private void maybeUpdateExportDeclToNode( NodeTraversal t, Node target, Node value) { // If the RHS is a typedef, clone the declaration. // Hack alert: clone the typedef declaration if one exists // this is a simple attempt that covers the common case of the // exports being in the same scope as the typedef declaration. // Otherwise the type name might be invalid. if (value.isName()) { Scope currentScope = t.getScope(); Var v = t.getScope().getVar(value.getString()); if (v != null) { Scope varScope = v.getScope(); if (varScope.getDepth() == currentScope.getDepth()) { JSDocInfo info = v.getJSDocInfo(); if (info != null && info.hasTypedefType()) { JSDocInfoBuilder builder = JSDocInfoBuilder.copyFrom(info); target.setJSDocInfo(builder.build()); return; } } } } // Don't add @const on class declarations, @const on classes has a // different meaning (it means "not subclassable"). // "goog.defineClass" hasn't been rewritten yet, so check for that // explicitly. JSDocInfo info = target.getJSDocInfo(); if ((info != null && info.isConstructorOrInterface() || isCallTo(value, "goog.defineClass"))) { return; } // Not a known typedef export, simple declare the props to be @const, // this is valid because we freeze module export objects. JSDocInfoBuilder builder = JSDocInfoBuilder.maybeCopyFrom(info); builder.recordConstancy(); target.setJSDocInfo(builder.build()); } /** * @return Whether the getprop is used as an assignment target, and that * target represents a module export. * Note: that "export.name = value" is an export, while "export.name.foo = value" * is not (it is an assignment to a property of an exported value). */ private static boolean isExportPropAssign(Node n) { Preconditions.checkState(n.isGetProp(), n); Node target = n.getFirstChild(); return isAssignTarget(n) && target.isName() && target.getString().equals("exports"); } private static boolean isAssignTarget(Node n) { Node parent = n.getParent(); return parent.isAssign() && parent.getFirstChild() == n; } 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 requires) { for (Node node : requires) { updateRequire(node); } } private void updateRequire(Node call) { if (call.getParent().isExprResult()) { // The goog.require is the entire statement. There is no var, so there's nothing to do. return; } String namespace = call.getLastChild().getString(); if (current.requireInsertNode == null) { current.requireInsertNode = getInsertRoot(call); } // rewrite: // var foo = goog.require('ns.foo') // to // goog.require('ns.foo'); // var foo = ns.foo; // replace the goog.require statement 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 collectRoots(ModuleDescription module) { List result = new ArrayList<>(); for (Node n : module.provides) { result.add(getRootName(n.getSecondChild())); } for (Node n : module.requires) { result.add(getRootName(n.getSecondChild())); } 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 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 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 roots) { final Map 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; } NodeTraversal.traverseEs6(compiler, s.getRootNode(), 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); } } } }); } private Node getModuleScopeRootForLoadModuleCall(Node n) { Preconditions.checkState(n.isCall(), n); Node fn = n.getLastChild(); Preconditions.checkState(fn.isFunction()); return fn.getLastChild(); } private Node getModuleStatementRootForLoadModuleCall(Node n) { Node scopeRoot = getModuleScopeRootForLoadModuleCall(n); if (scopeRoot.isFunction()) { return scopeRoot.getLastChild(); } else { return scopeRoot; } } 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.getFirstFirstChild(); 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; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy