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

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

There is a newer version: 9.0.8
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.collect.ImmutableSet;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.AbstractShallowStatementCallback;
import com.google.javascript.jscomp.NodeTraversal.Callback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Finds all references to global symbols and rewrites them to be property
 * accesses to a special object with the same name as the global symbol.
 *
 * Given the name of the global object is NS
 * 
 var a = 1; function b() { return a }
* becomes *
 NS.a = 1; NS.b = function b() { return NS.a }
* * This allows splitting code into modules that depend on each other's * global symbols, without using polluting JavaScript's global scope with those * symbols. You typically define just a single global symbol, wrap each module * in a function wrapper, and pass the global symbol around, eg, *
 var uniqueNs = uniqueNs || {}; 
*
 (function (NS) { ...your module code here... })(uniqueNs); 
* * *

This compile step requires moveFunctionDeclarations to be turned on * to guarantee semantics. * *

For lots of examples, see the unit test. * * */ final class RescopeGlobalSymbols implements CompilerPass { // Appended to variables names that conflict with globalSymbolNamespace. private static final String DISAMBIGUATION_SUFFIX = "$"; private static final String WINDOW = "window"; private static final ImmutableSet SPECIAL_EXTERNS = ImmutableSet.of( WINDOW, "eval", "arguments", "undefined", // The javascript built-in objects (listed in Ecma 262 section 4.2) "Object", "Function", "Array", "String", "Boolean", "Number", "Math", "Date", "RegExp", "JSON", "Error", "EvalError", "ReferenceError", "SyntaxError", "TypeError", "URIError"); private final AbstractCompiler compiler; private final String globalSymbolNamespace; private final boolean addExtern; private final boolean assumeCrossModuleNames; private final Set crossModuleNames = new HashSet<>(); /** Global identifiers that may be a non-arrow function referencing "this" */ private final Set maybeReferencesThis = new HashSet<>(); private Set externNames; /** * Constructor for the RescopeGlobalSymbols compiler pass. * * @param compiler The JSCompiler, for reporting code changes. * @param globalSymbolNamespace Name of namespace into which all global * symbols are transferred. * @param assumeCrossModuleNames If true, all global symbols will be assumed * cross module boundaries and thus require renaming. */ RescopeGlobalSymbols( AbstractCompiler compiler, String globalSymbolNamespace, boolean assumeCrossModuleNames) { this(compiler, globalSymbolNamespace, true, assumeCrossModuleNames); } /** * Constructor for the RescopeGlobalSymbols compiler pass for use in testing. * * @param compiler The JSCompiler, for reporting code changes. * @param globalSymbolNamespace Name of namespace into which all global * symbols are transferred. * @param addExtern If true, the compiler will consider the * globalSymbolNamespace an extern name. * @param assumeCrossModuleNames If true, all global symbols will be assumed * cross module boundaries and thus require renaming. * VisibleForTesting */ RescopeGlobalSymbols( AbstractCompiler compiler, String globalSymbolNamespace, boolean addExtern, boolean assumeCrossModuleNames) { this.compiler = compiler; this.globalSymbolNamespace = globalSymbolNamespace; this.addExtern = addExtern; this.assumeCrossModuleNames = assumeCrossModuleNames; } private boolean isCrossModuleName(String name) { return assumeCrossModuleNames || crossModuleNames.contains(name) || compiler.getCodingConvention().isExported(name, false); } private boolean isExternVar(String varname, NodeTraversal t) { if (varname.isEmpty()) { return false; } Var v = t.getScope().getVar(varname); return v == null || v.isExtern() || (v.scope.isGlobal() && this.externNames.contains(varname)); } private void addExternForGlobalSymbolNamespace() { Node varNode = IR.var(IR.name(globalSymbolNamespace)); CompilerInput input = compiler.getSynthesizedExternsInput(); input.getAstRoot(compiler).addChildToBack(varNode); compiler.reportChangeToEnclosingScope(varNode); } @Override public void process(Node externs, Node root) { // Collect variables in externs; they can be shadowed by the same names in global scope. this.externNames = NodeUtil.collectExternVariableNames(this.compiler, externs); // Make the name of the globalSymbolNamespace an extern. if (addExtern) { addExternForGlobalSymbolNamespace(); } // Rewrite all references to global symbols to properties of a single symbol: // Turn global named function statements into var assignments. NodeTraversal.traverseEs6( compiler, root, new RewriteGlobalClassFunctionDeclarationsToVarAssignmentsCallback()); // Find global names that are used in more than one module. Those that // are have to be rewritten. List nonMutatingPasses = new ArrayList<>(); nonMutatingPasses.add(new FindCrossModuleNamesCallback()); // And find names that may refer to functions that reference this. nonMutatingPasses.add(new FindNamesReferencingThis()); CombinedCompilerPass.traverse(compiler, root, nonMutatingPasses); // Rewrite all references to be property accesses of the single symbol. RewriteScopeCallback rewriteScope = new RewriteScopeCallback(); NodeTraversal.traverseEs6(compiler, root, rewriteScope); // Remove the var from statements in global scope if the declared names have been rewritten // in the previous pass. NodeTraversal.traverseEs6(compiler, root, new RemoveGlobalVarCallback()); rewriteScope.declareModuleGlobals(); } /** * Rewrites global function and class declarations to var statements + assignment. Ignores * non-global function and class declarations. * *

function test(){}
* * becomes * *
var test = function (){}
* *
class A {}
* * becomes * *
var A = class {}
* * After this traversal, the special case of global class and function statements can be ignored. * *

This is helpful when rewriting simple names to property accesses on the global symbol, since * {@code class A {}} cannot be rewritten directly to {@code class NS.A {}} */ private class RewriteGlobalClassFunctionDeclarationsToVarAssignmentsCallback extends AbstractShallowStatementCallback { @Override public void visit(NodeTraversal t, Node n, Node parent) { if (NodeUtil.isFunctionDeclaration(n) // Since class declarations are block-scoped, only handle them if in the global scope. || (NodeUtil.isClassDeclaration(n) && t.inGlobalScope())) { Node nameNode = NodeUtil.getNameNode(n); String name = nameNode.getString(); // Remove the class or function name. Anonymous classes have an EMPTY node, while anonymous // functions have a NAME node with an empty string. if (n.isClass()) { nameNode.replaceWith(IR.empty().srcref(nameNode)); } else { nameNode.setString(""); compiler.reportChangeToEnclosingScope(nameNode); } Node prev = n.getPrevious(); n.detach(); Node var = NodeUtil.newVarNode(name, n); if (prev == null) { parent.addChildToFront(var); } else { parent.addChildAfter(var, prev); } compiler.reportChangeToEnclosingScope(parent); } } } /** * Find all global names that are used in more than one module. The following * compiler transformations can ignore the globals that are not. */ private class FindCrossModuleNamesCallback extends AbstractPostOrderCallback { @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isName()) { String name = n.getString(); if ("".equals(name) || crossModuleNames.contains(name)) { return; } Scope s = t.getScope(); Var v = s.getVar(name); if (v == null || !v.isGlobal()) { return; } CompilerInput input = v.getInput(); if (input == null) { // We know nothing. Assume name is used across modules. crossModuleNames.add(name); return; } // Compare the module where the variable is declared to the current // module. If they are different, the variable is used across modules. JSModule module = input.getModule(); if (module != t.getModule()) { crossModuleNames.add(name); } } } } /** * Builds the maybeReferencesThis set of names that may reference a function * that references this. If the function a name references does not reference * this it can be called as a method call where the this value is not the * same as in a normal function call. */ private class FindNamesReferencingThis extends AbstractPostOrderCallback { @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isName()) { String name = n.getString(); if (name.isEmpty()) { return; } Node value = null; if (parent.isAssign() && n == parent.getFirstChild()) { value = parent.getLastChild(); } else if (NodeUtil.isNameDeclaration(parent)) { value = n.getFirstChild(); } else if (parent.isFunction()) { value = parent; } if (value == null && !NodeUtil.isLhsByDestructuring(n)) { // If n is assigned in a destructuring pattern, don't bother finding its value and just // assume it may reference this. return; } // We already added this symbol. Done after checks above because those // are comparatively cheap. if (maybeReferencesThis.contains(name)) { return; } Scope s = t.getScope(); Var v = s.getVar(name); if (v == null || !v.isGlobal()) { return; } // If anything but a function is assigned we assume that possibly // a function referencing this is being assigned. Otherwise we // check whether the function assigned is a) an arrow function, which has a // lexically-scoped this, or b) a non-arrow function that does not reference this. if (value == null || !value.isFunction() || (!value.isArrowFunction() && NodeUtil.referencesThis(value))) { maybeReferencesThis.add(name); } } } } /** * Visits each NAME token and checks whether it refers to a global variable. If yes, rewrites the * name to be a property access on the "globalSymbolNamespace". If the NAME is an extern variable, * it becomes a property access on window. * *

var a = 1, b = 2, c = 3;
* * becomes * *
var NS.a = 1, NS.b = 2, NS.c = 4
* * (The var token is removed in a later traversal.) * *
a + b
* * becomes * *
NS.a + NS.b
* *
a()
* * becomes * *
(0,NS.a)()
* * Notice the special syntax here to preserve the *this* semantics in the function call. * *
var {a: b} = {}
* * becomes * *
var {a: NS.b} = {}
* * (This is invalid syntax, but the VAR token is removed later). */ private class RewriteScopeCallback extends AbstractPostOrderCallback { List preDeclarations = new ArrayList<>(); @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isName() && !NodeUtil.isLhsByDestructuring(n)) { // NOTE: we visit names that are lhs by destructuring in {@code visitDestructuringPattern}. visitName(t, n, parent); } else if (n.isDestructuringPattern()) { visitDestructuringPattern(t, n, parent); } } /** * Rewrites all cross-module names inside destructuring patterns, and converts destructuring * declarations containing any cross-module names to assignments. */ private void visitDestructuringPattern(NodeTraversal t, Node n, Node parent) { if (!(parent.isAssign() || parent.isParamList() || parent.isDestructuringLhs())) { // Don't handle patterns that are nested within another pattern. return; } List lhsNodes = NodeUtil.findLhsNodesInNode(n.getParent()); boolean hasCrossModuleName = false; // Go through all lhs name nodes in the destructuring pattern, and call {@code visitName} // on them to rescope any cross-module globals. // e.g. after the loop finishes, [a, b] = [1, 2]; becomes [NS.a, NS.b] = [1, 2]; for (Node lhs : lhsNodes) { if (!lhs.isName()) { // The LHS could also be a GETPROP or GETELEM, which get handled when the traversal hits // their NAME nodes. continue; } visitName(t, lhs, lhs.getParent()); hasCrossModuleName = hasCrossModuleName || isCrossModuleName(lhs.getString()); } // If the parent is not a destructuring lhs, this is an assignment, not a declaration, and // there's nothing left to do. if (!parent.isDestructuringLhs()) { return; } Node nameDeclaration = parent.getParent(); // If this declaration is global and has any cross-module names, rewrite it to not be a // declaration. RemoveGlobalVarCallback will remove the actual var/let/const node. if (hasCrossModuleName && (t.inGlobalScope() || (nameDeclaration.isVar() && t.inGlobalHoistScope()))) { Node value = n.getNext(); if (value != null) { // If the destructuring pattern has an rhs, convert this to be an ASSIGN. parent.removeChild(n); parent.removeChild(value); Node assign = IR.assign(n, value).srcref(n); nameDeclaration.replaceChild(parent, assign); } else { // In a for-in or for-of loop initializer, the rhs value is null. // Move the destructuring pattern to be a direct child of the name declaration. parent.removeChild(n); nameDeclaration.replaceChild(parent, n); } compiler.reportChangeToEnclosingScope(nameDeclaration); // If there are any declared names that are not cross module, they need to be declared // before the destructuring pattern, since we converted their declaration to an assignment. CompilerInput input = t.getInput(); for (Node lhs : lhsNodes) { if (!lhs.isName()) { continue; } String name = lhs.getString(); if (!isCrossModuleName(name)) { preDeclarations.add( new ModuleGlobal(input.getAstRoot(compiler), IR.name(name).srcref(lhs))); } } } } private void visitName(NodeTraversal t, Node n, Node parent) { String name = n.getString(); // Ignore anonymous functions if (parent.isFunction() && name.isEmpty()) { return; } if (isExternVar(name, t)) { visitExtern(n, parent); return; } // When the globalSymbolNamespace is used as a local variable name // add suffix to avoid shadowing the namespace. Also add a suffix // if a name starts with the name of the globalSymbolNamespace and // the suffix. Var var = t.getScope().getVar(name); if (!var.isGlobal() && (name.equals(globalSymbolNamespace) || name.startsWith(globalSymbolNamespace + DISAMBIGUATION_SUFFIX))) { n.setString(name + DISAMBIGUATION_SUFFIX); compiler.reportChangeToEnclosingScope(n); } // We only care about global vars. if (!var.isGlobal()) { return; } Node nameNode = var.getNameNode(); // The exception variable (e in try{}catch(e){}) should not be rewritten. if (nameNode != null && nameNode.getParent() != null && nameNode.getParent().isCatch()) { return; } replaceSymbol(t, n, name, t.getInput()); } private void replaceSymbol(NodeTraversal t, Node node, String name, CompilerInput input) { Node parent = node.getParent(); boolean isCrossModule = isCrossModuleName(name); if (!isCrossModule) { // When a non cross module name appears outside a var declaration we // never have to do anything. // If it's inside a destructuring pattern declaration, then it's handled elsewhere. if (!NodeUtil.isNameDeclaration(parent)) { return; } boolean hasInterestingChildren = false; for (Node c : parent.children()) { // VAR child is no longer a name means it was transformed already. if (!c.isName() || isCrossModuleName(c.getString()) || isExternVar(c.getString(), t)) { hasInterestingChildren = true; break; } } if (!hasInterestingChildren) { return; } } Node replacement = isCrossModule ? IR.getprop( IR.name(globalSymbolNamespace).srcref(node), IR.string(name).srcref(node)) : IR.name(name).srcref(node); replacement.srcref(node); if (node.hasChildren()) { // var declaration list: var a = 1, b = 2; Node assign = IR.assign( replacement, node.removeFirstChild()); parent.replaceChild(node, assign); compiler.reportChangeToEnclosingScope(assign); } else if (isCrossModule) { parent.replaceChild(node, replacement); compiler.reportChangeToEnclosingScope(replacement); if (parent.isCall() && !maybeReferencesThis.contains(name)) { // Do not write calls like this: (0, _a)() but rather as _.a(). The // this inside the function will be wrong, but it doesn't matter // because the this is never read. parent.putBooleanProp(Node.FREE_CALL, false); } } // If we changed a non cross module name that was in a var declaration // we need to preserve that var declaration. Because it is global // anyway, we just put it at the beginning of the current input. // Example: // var crossModule = i++, notCrossModule = i++ // becomes // var notCrossModule;_.crossModule = i++, notCrossModule = i++ if (!isCrossModule && NodeUtil.isNameDeclaration(parent)) { preDeclarations.add(new ModuleGlobal( input.getAstRoot(compiler), IR.name(name).srcref(node))); } compiler.reportChangeToEnclosingScope(parent); } /** * Rewrites extern names to be explicit children of window instead of only implicitly * referencing it. This enables injecting window into a scope and make all global symbols * depend on the injected object. */ private void visitExtern(Node nameNode, Node parent) { String name = nameNode.getString(); if (globalSymbolNamespace.equals(name) || SPECIAL_EXTERNS.contains(name)) { return; } Node windowPropAccess = IR.getprop(IR.name(WINDOW), IR.string(name)); if (NodeUtil.isNameDeclaration(parent) && nameNode.hasOneChild()) { Node assign = IR.assign(windowPropAccess, nameNode.removeFirstChild()); assign.setJSDocInfo(parent.getJSDocInfo()); parent.replaceChild(nameNode, assign.srcrefTree(parent)); } else { parent.replaceChild(nameNode, windowPropAccess.srcrefTree(nameNode)); } compiler.reportChangeToEnclosingScope(parent); } /** * Adds back declarations for variables that do not cross module boundaries. * Must be called after RemoveGlobalVarCallback. */ void declareModuleGlobals() { for (ModuleGlobal global : preDeclarations) { if (global.root.getFirstChild() != null && global.root.getFirstChild().isVar()) { global.root.getFirstChild().addChildToBack(global.name); } else { global.root.addChildToFront(IR.var(global.name).srcref(global.name)); } compiler.reportChangeToEnclosingScope(global.root); } } /** * Variable that doesn't cross module boundaries. */ private class ModuleGlobal { final Node root; final Node name; ModuleGlobal(Node root, Node name) { this.root = root; this.name = name; } } } /** * Removes every occurrence of var/let/const that declares a global variable. * *
var NS.a = 1, NS.b = 2;
* * becomes * *
NS.a = 1; NS.b = 2;
* *
for (var a = 0, b = 0;;)
* * becomes * *
for (NS.a = 0, NS.b = 0;;)
* * Declarations without assignments are optimized away: * *
var a = 1, b;
* * becomes * *
NS.a = 1
*/ private class RemoveGlobalVarCallback extends AbstractShallowStatementCallback { @Override public void visit(NodeTraversal t, Node n, Node parent) { if (!NodeUtil.isNameDeclaration(n)) { return; } List commas = new ArrayList<>(); List interestingChildren = new ArrayList<>(); // Filter out declarations without assignments. // As opposed to regular var nodes, there are always assignments // because the previous traversal in RewriteScopeCallback creates // them. boolean allNameOrDestructuring = true; for (Node c : n.children()) { if (!c.isName() && !c.isDestructuringLhs()) { allNameOrDestructuring = false; } if (c.isAssign() || NodeUtil.isAnyFor(parent)) { interestingChildren.add(c); } } // If every child of a var declares a name, it must stay in place. // This is the case if none of the declared variables cross module // boundaries. if (allNameOrDestructuring) { return; } for (Node c : interestingChildren) { if (NodeUtil.isAnyFor(parent) && parent.getFirstChild() == n) { commas.add(c.cloneTree()); } else { // Var statement outside of for-loop. Node expr = IR.exprResult(c.cloneTree()).srcref(c); NodeUtil.markNewScopesChanged(expr, compiler); parent.addChildBefore(expr, n); } } if (!commas.isEmpty()) { Node comma = joinOnComma(commas, n); parent.addChildBefore(comma, n); } // Remove the var/const/let node. parent.removeChild(n); NodeUtil.markFunctionsDeleted(n, compiler); compiler.reportChangeToEnclosingScope(parent); } private Node joinOnComma(List commas, Node source) { Node comma = commas.get(0); for (int i = 1; i < commas.size(); i++) { Node nextComma = IR.comma(comma, commas.get(i)); nextComma.useSourceInfoIfMissingFrom(source); comma = nextComma; } return comma; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy