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

com.google.javascript.jscomp.CollapseProperties 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. This binary checks for style issues such as incorrect or missing JSDoc usage, and missing goog.require() statements. It does not do more advanced checks such as typechecking.

There is a newer version: v20200830
Show newest version
/*
 * Copyright 2006 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.CompilerOptions.PropertyCollapseLevel;
import com.google.javascript.jscomp.GlobalNamespace.Name;
import com.google.javascript.jscomp.GlobalNamespace.Ref;
import com.google.javascript.jscomp.Normalize.PropagateConstantAnnotationsOverVars;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.TokenStream;
import com.google.javascript.rhino.jstype.JSType;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

/**
 * Flattens global objects/namespaces by replacing each '.' with '$' in their names.
 *
 * 

This reduces the number of property lookups the browser has to do and allows the {@link * RenameVars} pass to shorten namespaced names. For example, goog.events.handleEvent() -> * goog$events$handleEvent() -> Za(). * *

If a global object's name is assigned to more than once, or if a property is added to the * global object in a complex expression, then none of its properties will be collapsed (for * safety/correctness). * *

If, after a global object is declared, it is never referenced except when its properties are * read or set, then the object will be removed after its properties have been collapsed. * *

Uninitialized variable stubs are created at a global object's declaration site for any of its * properties that are added late in a local scope. * *

Static properties of constructors are always collapsed, unsafely! For other objects: if, after * an object is declared, it is referenced directly in a way that might create an alias for it, then * none of its properties will be collapsed. This behavior is a safeguard to prevent the values * associated with the flattened names from getting out of sync with the object's actual property * values. For example, in the following case, an alias a$b, if created, could easily keep the value * 0 even after a.b became 5: a = {b: 0}; c = a; c.b = 5; . * *

This pass may break code, but relies on {@link AggressiveInlineAliases} running before this * pass to make some common patterns safer. * *

This pass doesn't flatten property accesses of the form: a[b]. * *

For lots of examples, see the unit test. * */ class CollapseProperties implements CompilerPass { // Warnings static final DiagnosticType UNSAFE_NAMESPACE_WARNING = DiagnosticType.warning( "JSC_UNSAFE_NAMESPACE", "incomplete alias created for namespace {0}, possibly due to await/yield transpilation.\n" + "See https://github.com/google/closure-compiler/wiki/FAQ#i-got-an-incomplete-alias-created-for-namespace-error--what-do-i-do for more details."); static final DiagnosticType NAMESPACE_REDEFINED_WARNING = DiagnosticType.warning("JSC_NAMESPACE_REDEFINED", "namespace {0} should not be redefined"); static final DiagnosticType UNSAFE_THIS = DiagnosticType.warning( "JSC_UNSAFE_THIS", "dangerous use of ''this'' in static method {0}"); private final AbstractCompiler compiler; private final PropertyCollapseLevel propertyCollapseLevel; /** Global namespace tree */ private List globalNames; /** Maps names (e.g. "a.b.c") to nodes in the global namespace tree */ private Map nameMap; private final HashSet dynamicallyImportedModules = new HashSet<>(); CollapseProperties(AbstractCompiler compiler, PropertyCollapseLevel propertyCollapseLevel) { this.compiler = compiler; this.propertyCollapseLevel = propertyCollapseLevel; } @Override public void process(Node externs, Node root) { if (propertyCollapseLevel == PropertyCollapseLevel.MODULE_EXPORT) { gatherDynamicallyImportedModules(); } GlobalNamespace namespace = new GlobalNamespace(compiler, root); nameMap = namespace.getNameIndex(); globalNames = namespace.getNameForest(); checkNamespaces(); for (Name name : globalNames) { flattenReferencesToCollapsibleDescendantNames(name, name.getBaseName()); } // We collapse property definitions after collapsing property references // because this step can alter the parse tree above property references, // invalidating the node ancestry stored with each reference. for (Name name : globalNames) { collapseDeclarationOfNameAndDescendants(name, name.getBaseName()); } // This shouldn't be necessary, this pass should already be setting new constants as constant. // TODO(b/64256754): Investigate. (new PropagateConstantAnnotationsOverVars(compiler, false)).process(externs, root); } private boolean canCollapse(Name name) { if (!name.canCollapse()) { return false; } if (propertyCollapseLevel == PropertyCollapseLevel.MODULE_EXPORT && (!name.isModuleExport() || dynamicallyImportedModules.contains(name.getBaseName()))) { return false; } return true; } private boolean canEliminate(Name name) { if (!name.canEliminate()) { return false; } if (name.props == null || name.props.isEmpty() || propertyCollapseLevel != PropertyCollapseLevel.MODULE_EXPORT) { return true; } return false; } /** * Runs through all namespaces (prefixes of classes and enums), and checks if any of them have * been used in an unsafe way. */ private void checkNamespaces() { for (Name name : nameMap.values()) { if (name.isNamespaceObjectLit() && (name.getAliasingGets() > 0 || name.getLocalSets() + name.getGlobalSets() > 1 || name.getDeleteProps() > 0)) { boolean initialized = name.getDeclaration() != null; for (Ref ref : name.getRefs()) { if (ref == name.getDeclaration()) { continue; } if (ref.type == Ref.Type.DELETE_PROP) { if (initialized) { warnAboutNamespaceRedefinition(name, ref); } } else if ( ref.type == Ref.Type.SET_FROM_GLOBAL || ref.type == Ref.Type.SET_FROM_LOCAL) { if (initialized && !isSafeNamespaceReinit(ref)) { warnAboutNamespaceRedefinition(name, ref); } initialized = true; } else if (ref.type == Ref.Type.ALIASING_GET) { warnAboutNamespaceAliasing(name, ref); } } } } } static boolean isSafeNamespaceReinit(Ref ref) { // allow "a = a || {}" or "var a = a || {}" or "var a;" Node valParent = getValueParent(ref); Node val = valParent.getLastChild(); if (val != null && val.isOr()) { Node maybeName = val.getFirstChild(); if (ref.getNode().matchesQualifiedName(maybeName)) { return true; } } return false; } /** * Gets the parent node of the value for any assignment to a Name. * For example, in the assignment * {@code var x = 3;} * the parent would be the NAME node. */ private static Node getValueParent(Ref ref) { // there are four types of declarations: VARs, LETs, CONSTs, and ASSIGNs Node n = ref.getNode().getParent(); return (n != null && NodeUtil.isNameDeclaration(n)) ? ref.getNode() : ref.getNode().getParent(); } /** * Reports a warning because a namespace was aliased. * * @param nameObj A namespace that is being aliased * @param ref The reference that forced the alias */ private void warnAboutNamespaceAliasing(Name nameObj, Ref ref) { compiler.report(JSError.make(ref.getNode(), UNSAFE_NAMESPACE_WARNING, nameObj.getFullName())); } /** * Reports a warning because a namespace was redefined. * * @param nameObj A namespace that is being redefined * @param ref The reference that set the namespace */ private void warnAboutNamespaceRedefinition(Name nameObj, Ref ref) { compiler.report( JSError.make(ref.getNode(), NAMESPACE_REDEFINED_WARNING, nameObj.getFullName())); } /** * Flattens all references to collapsible properties of a global name except * their initial definitions. Recurs on subnames. * * @param n An object representing a global name * @param alias The flattened name for {@code n} */ private void flattenReferencesToCollapsibleDescendantNames( Name n, String alias) { if (n.props == null || n.isCollapsingExplicitlyDenied()) { return; } for (Name p : n.props) { String propAlias = appendPropForAlias(alias, p.getBaseName()); boolean isAllowedToCollapse = propertyCollapseLevel != PropertyCollapseLevel.MODULE_EXPORT || p.isModuleExport(); if (isAllowedToCollapse && p.canCollapse()) { flattenReferencesTo(p, propAlias); } else if (isAllowedToCollapse && p.isSimpleStubDeclaration() && !p.isCollapsingExplicitlyDenied()) { flattenSimpleStubDeclaration(p, propAlias); } flattenReferencesToCollapsibleDescendantNames(p, propAlias); } } /** * Flattens a stub declaration. * This is mostly a hack to support legacy users. */ private void flattenSimpleStubDeclaration(Name name, String alias) { Ref ref = Iterables.getOnlyElement(name.getRefs()); Node nameNode = NodeUtil.newName(compiler, alias, ref.getNode(), name.getFullName()); Node varNode = IR.var(nameNode).useSourceInfoIfMissingFrom(nameNode); checkState(ref.getNode().getParent().isExprResult()); Node parent = ref.getNode().getParent(); Node grandparent = parent.getParent(); grandparent.replaceChild(parent, varNode); compiler.reportChangeToEnclosingScope(varNode); } /** * Flattens all references to a collapsible property of a global name except * its initial definition. * * @param n A global property name (e.g. "a.b" or "a.b.c.d") * @param alias The flattened name (e.g. "a$b" or "a$b$c$d") */ private void flattenReferencesTo(Name n, String alias) { String originalName = n.getFullName(); for (Ref r : n.getRefs()) { if (r == n.getDeclaration()) { // Declarations are handled separately. continue; } Node rParent = r.getNode().getParent(); // There are two cases when we shouldn't flatten a reference: // 1) Object literal keys, because duplicate keys show up as refs. // 2) References inside a complex assign. (a = x.y = 0). These are // called TWIN references, because they show up twice in the // reference list. Only collapse the set, not the alias. if (!NodeUtil.mayBeObjectLitKey(r.getNode()) && (r.getTwin() == null || r.isSet())) { flattenNameRef(alias, r.getNode(), rParent, originalName); } else if (r.getNode().isStringKey() && r.getNode().getParent().isObjectPattern()) { Node newNode = IR.name(alias).srcref(r.getNode()); NodeUtil.copyNameAnnotations(r.getNode(), newNode); DestructuringGlobalNameExtractor.reassignDestructringLvalue( r.getNode(), newNode, null, r, compiler); } } // Flatten all occurrences of a name as a prefix of its subnames. For // example, if {@code n} corresponds to the name "a.b", then "a.b" will be // replaced with "a$b" in all occurrences of "a.b.c", "a.b.c.d", etc. if (n.props != null) { for (Name p : n.props) { flattenPrefixes(alias, p, 1); } } } /** * Flattens all occurrences of a name as a prefix of subnames beginning * with a particular subname. * * @param n A global property name (e.g. "a.b.c.d") * @param alias A flattened prefix name (e.g. "a$b") * @param depth The difference in depth between the property name and * the prefix name (e.g. 2) */ private void flattenPrefixes(String alias, Name n, int depth) { // Only flatten the prefix of a name declaration if the name being // initialized is fully qualified (i.e. not an object literal key). String originalName = n.getFullName(); Ref decl = n.getDeclaration(); if (decl != null && decl.getNode() != null && decl.getNode().isGetProp()) { flattenNameRefAtDepth(alias, decl.getNode(), depth, originalName); } for (Ref r : n.getRefs()) { if (r == decl) { // Declarations are handled separately. continue; } // References inside a complex assign (a = x.y = 0) // have twins. We should only flatten one of the twins. if (r.getTwin() == null || r.isSet()) { flattenNameRefAtDepth(alias, r.getNode(), depth, originalName); } } if (n.props != null) { for (Name p : n.props) { flattenPrefixes(alias, p, depth + 1); } } } /** * Flattens a particular prefix of a single name reference. * * @param alias A flattened prefix name (e.g. "a$b") * @param n The node corresponding to a subproperty name (e.g. "a.b.c.d") * @param depth The difference in depth between the property name and * the prefix name (e.g. 2) * @param originalName String version of the property name. */ private void flattenNameRefAtDepth(String alias, Node n, int depth, String originalName) { // This method has to work for both GETPROP chains and, in rare cases, // OBJLIT keys, possibly nested. That's why we check for children before // proceeding. In the OBJLIT case, we don't need to do anything. Token nType = n.getToken(); boolean isQName = nType == Token.NAME || nType == Token.GETPROP; boolean isObjKey = NodeUtil.mayBeObjectLitKey(n); checkState(isObjKey || isQName); if (isQName) { for (int i = 1; i < depth && n.hasChildren(); i++) { n = n.getFirstChild(); } if (n.isGetProp() && n.getFirstChild().isGetProp()) { flattenNameRef(alias, n.getFirstChild(), n, originalName); } } } /** * Replaces a GETPROP a.b.c with a NAME a$b$c. * * @param alias A flattened prefix name (e.g. "a$b") * @param n The GETPROP node corresponding to the original name (e.g. "a.b") * @param parent {@code n}'s parent * @param originalName String version of the property name. */ private void flattenNameRef(String alias, Node n, Node parent, String originalName) { Preconditions.checkArgument( n.isGetProp(), "Expected GETPROP, found %s. Node: %s", n.getToken(), n); // BEFORE: // getprop // getprop // name a // string b // string c // AFTER: // name a$b$c Node ref = NodeUtil.newName(compiler, alias, n, originalName); NodeUtil.copyNameAnnotations(n.getLastChild(), ref); if (parent.isCall() && n == parent.getFirstChild()) { // The node was a call target, we are deliberately flatten these as // we node the "this" isn't provided by the namespace. Mark it as such: parent.putBooleanProp(Node.FREE_CALL, true); } JSType type = n.getJSType(); if (type != null) { ref.setJSType(type); } parent.replaceChild(n, ref); compiler.reportChangeToEnclosingScope(ref); } /** * Collapses definitions of the collapsible properties of a global name. * Recurs on subnames that also represent JavaScript objects with * collapsible properties. * * @param n A node representing a global name * @param alias The flattened name for {@code n} */ private void collapseDeclarationOfNameAndDescendants(Name n, String alias) { boolean canCollapseChildNames = n.canCollapseUnannotatedChildNames(); // Handle this name first so that nested object literals get unrolled. if (canCollapse(n)) { updateGlobalNameDeclaration(n, alias, canCollapseChildNames); } if (n.props == null) { return; } for (Name p : n.props) { collapseDeclarationOfNameAndDescendants(p, appendPropForAlias(alias, p.getBaseName())); } } /** * Updates the initial assignment to a collapsible property at global scope * by adding a VAR stub and collapsing the property. e.g. c = a.b = 1; => var a$b; c = a$b = 1; * This specifically handles "twinned" assignments, which are those where the assignment is also * used as a reference and which need special handling. * * @param alias The flattened property name (e.g. "a$b") * @param refName The name for the reference being updated. * @param ref An object containing information about the assignment getting updated */ private void updateTwinnedDeclaration(String alias, Name refName, Ref ref) { checkNotNull(ref.getTwin()); // Don't handle declarations of an already flat name, just qualified names. if (!ref.getNode().isGetProp()) { return; } Node rvalue = ref.getNode().getNext(); Node parent = ref.getNode().getParent(); Node grandparent = parent.getParent(); if (rvalue != null && rvalue.isFunction()) { checkForHosedThisReferences(rvalue, refName.getJSDocInfo(), refName); } // Create the new alias node. Node nameNode = NodeUtil.newName(compiler, alias, grandparent.getFirstChild(), refName.getFullName()); NodeUtil.copyNameAnnotations(ref.getNode().getLastChild(), nameNode); // BEFORE: // ... (x.y = 3); // // AFTER: // var x$y; // ... (x$y = 3); Node current = grandparent; Node currentParent = grandparent.getParent(); for (; !currentParent.isScript() && !currentParent.isBlock(); current = currentParent, currentParent = currentParent.getParent()) {} // Create a stub variable declaration right // before the current statement. Node stubVar = IR.var(nameNode.cloneTree()).useSourceInfoIfMissingFrom(nameNode); currentParent.addChildBefore(stubVar, current); parent.replaceChild(ref.getNode(), nameNode); compiler.reportChangeToEnclosingScope(nameNode); } /** * Updates the first initialization (a.k.a "declaration") of a global name. * This involves flattening the global name (if it's not just a global * variable name already), collapsing object literal keys into global * variables, declaring stub global variables for properties added later * in a local scope. * * It may seem odd that this function also takes care of declaring stubs * for direct children. The ultimate goal of this function is to eliminate * the global name entirely (when possible), so that "middlemen" namespaces * disappear, and to do that we need to make sure that all the direct children * will be collapsed as well. * * @param n An object representing a global name (e.g. "a", "a.b.c") * @param alias The flattened name for {@code n} (e.g. "a", "a$b$c") * @param canCollapseChildNames Whether it's possible to collapse children of * this name. (This is mostly passed for convenience; it's equivalent to * n.canCollapseChildNames()). */ private void updateGlobalNameDeclaration( Name n, String alias, boolean canCollapseChildNames) { Ref decl = n.getDeclaration(); if (decl == null) { // Some names do not have declarations, because they // are only defined in local scopes. return; } switch (decl.getNode().getParent().getToken()) { case ASSIGN: updateGlobalNameDeclarationAtAssignNode( n, alias, canCollapseChildNames); break; case VAR: case LET: case CONST: updateGlobalNameDeclarationAtVariableNode(n, canCollapseChildNames); break; case FUNCTION: updateGlobalNameDeclarationAtFunctionNode(n, canCollapseChildNames); break; case CLASS: updateGlobalNameDeclarationAtClassNode(n, canCollapseChildNames); break; case CLASS_MEMBERS: updateGlobalNameDeclarationAtStaticMemberNode(n, alias, canCollapseChildNames); break; default: break; } } /** * Updates the first initialization (a.k.a "declaration") of a global name * that occurs at an ASSIGN node. See comment for * {@link #updateGlobalNameDeclaration}. * * @param n An object representing a global name (e.g. "a", "a.b.c") * @param alias The flattened name for {@code n} (e.g. "a", "a$b$c") */ private void updateGlobalNameDeclarationAtAssignNode( Name n, String alias, boolean canCollapseChildNames) { // NOTE: It's important that we don't add additional nodes // (e.g. a var node before the exprstmt) because the exprstmt might be // the child of an if statement that's not inside a block). // All qualified names - even for variables that are initially declared as LETS and CONSTS - // are being declared as VAR statements, but this is not incorrect because // we are only collapsing for global names. Ref ref = n.getDeclaration(); Node rvalue = ref.getNode().getNext(); if (ref.getTwin() != null) { updateTwinnedDeclaration(alias, ref.name, ref); return; } Node varNode = new Node(Token.VAR); Node varParent = ref.getNode().getAncestor(3); Node grandparent = ref.getNode().getAncestor(2); boolean isObjLit = rvalue.isObjectLit(); boolean insertedVarNode = false; if (isObjLit && canEliminate(n)) { // Eliminate the object literal altogether. varParent.replaceChild(grandparent, varNode); n.updateRefNode(ref, null); insertedVarNode = true; compiler.reportChangeToEnclosingScope(varNode); } else if (!n.isSimpleName()) { // Create a VAR node to declare the name. if (rvalue.isFunction()) { checkForHosedThisReferences(rvalue, n.getJSDocInfo(), n); } compiler.reportChangeToEnclosingScope(rvalue); ref.getNode().getParent().removeChild(rvalue); Node nameNode = NodeUtil.newName(compiler, alias, ref.getNode().getAncestor(2), n.getFullName()); JSDocInfo info = NodeUtil.getBestJSDocInfo(ref.getNode().getParent()); if (ref.getNode().getLastChild().getBooleanProp(Node.IS_CONSTANT_NAME) || (info != null && info.isConstant())) { nameNode.putBooleanProp(Node.IS_CONSTANT_NAME, true); } if (info != null) { varNode.setJSDocInfo(info); } varNode.addChildToBack(nameNode); nameNode.addChildToFront(rvalue); varParent.replaceChild(grandparent, varNode); // Update the node ancestry stored in the reference. n.updateRefNode(ref, nameNode); insertedVarNode = true; compiler.reportChangeToEnclosingScope(varNode); } if (canCollapseChildNames) { if (isObjLit) { declareVariablesForObjLitValues( n, alias, rvalue, varNode, varNode.getPrevious(), varParent); } addStubsForUndeclaredProperties(n, alias, varParent, varNode); } if (insertedVarNode) { if (!varNode.hasChildren()) { varParent.removeChild(varNode); } } } /** * Warns about any references to "this" in the given FUNCTION. The function * is getting collapsed, so the references will change. */ private void checkForHosedThisReferences(Node function, JSDocInfo docInfo, final Name name) { // A function is getting collapsed. Make sure that if it refers to "this", // it must be a constructor, interface, record, arrow function, or documented with @this. boolean isAllowedToReferenceThis = (docInfo != null && (docInfo.isConstructorOrInterface() || docInfo.hasThisType())) || function.isArrowFunction(); if (!isAllowedToReferenceThis) { NodeTraversal.traverse(compiler, function.getLastChild(), new NodeTraversal.AbstractShallowCallback() { @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isThis()) { compiler.report( JSError.make(n, UNSAFE_THIS, name.getFullName())); } } }); } } /** * Updates the first initialization (a.k.a "declaration") of a global name that occurs at a VAR * node. See comment for {@link #updateGlobalNameDeclaration}. * * @param n An object representing a global name (e.g. "a") */ private void updateGlobalNameDeclarationAtVariableNode( Name n, boolean canCollapseChildNames) { if (!canCollapseChildNames) { return; } Ref ref = n.getDeclaration(); String name = ref.getNode().getString(); Node rvalue = ref.getNode().getFirstChild(); Node variableNode = ref.getNode().getParent(); Node grandparent = variableNode.getParent(); boolean isObjLit = rvalue.isObjectLit(); if (isObjLit) { declareVariablesForObjLitValues( n, name, rvalue, variableNode, variableNode.getPrevious(), grandparent); } addStubsForUndeclaredProperties(n, name, grandparent, variableNode); if (isObjLit && canEliminate(n)) { variableNode.removeChild(ref.getNode()); compiler.reportChangeToEnclosingScope(variableNode); if (!variableNode.hasChildren()) { grandparent.removeChild(variableNode); } // Clear out the object reference, since we've eliminated it from the // parse tree. n.updateRefNode(ref, null); } } /** * Updates the first initialization (a.k.a "declaration") of a global name * that occurs at a FUNCTION node. See comment for * {@link #updateGlobalNameDeclaration}. * * @param n An object representing a global name (e.g. "a") */ private void updateGlobalNameDeclarationAtFunctionNode( Name n, boolean canCollapseChildNames) { if (!canCollapseChildNames || !canCollapse(n)) { return; } Ref ref = n.getDeclaration(); String fnName = ref.getNode().getString(); addStubsForUndeclaredProperties( n, fnName, ref.getNode().getAncestor(2), ref.getNode().getParent()); } /** * Updates the first initialization (a.k.a "declaration") of a global name that occurs at a CLASS * node. See comment for {@link #updateGlobalNameDeclaration}. * * @param n An object representing a global name (e.g. "a") */ private void updateGlobalNameDeclarationAtClassNode(Name n, boolean canCollapseChildNames) { if (!canCollapseChildNames || !canCollapse(n)) { return; } Ref ref = n.getDeclaration(); String className = ref.getNode().getString(); addStubsForUndeclaredProperties( n, className, ref.getNode().getAncestor(2), ref.getNode().getParent()); } /** * Updates the first initialization (a.k.a "declaration") of a global name that occurs in a static * MEMBER_FUNCTION_DEF in a class. See comment for {@link #updateGlobalNameDeclaration}. * * @param n A static MEMBER_FUNCTION_DEF in a class assigned to a global name (e.g. `a.b`) * @param alias The new flattened name for `n` (e.g. "a$b") * @param canCollapseChildNames whether properties of `n` are also collapsible, meaning that any * properties only assigned locally need stub declarations */ private void updateGlobalNameDeclarationAtStaticMemberNode( Name n, String alias, boolean canCollapseChildNames) { Ref declaration = n.getDeclaration(); Node classNode = declaration.getNode().getGrandparent(); checkState(classNode.isClass(), classNode); Node enclosingStatement = NodeUtil.getEnclosingStatement(classNode); if (canCollapseChildNames) { addStubsForUndeclaredProperties(n, alias, enclosingStatement.getParent(), classNode); } // detach `static m() {}` from `class Foo { static m() {} }` Node memberFn = declaration.getNode().detach(); Node fnNode = memberFn.getOnlyChild().detach(); checkForHosedThisReferences(fnNode, memberFn.getJSDocInfo(), n); // add a var declaration, creating `var Foo$m = function() {}; class Foo {}` Node varDecl = IR.var(NodeUtil.newName(compiler, alias, memberFn), fnNode).srcref(memberFn); enclosingStatement.getParent().addChildBefore(varDecl, enclosingStatement); compiler.reportChangeToEnclosingScope(varDecl); // collapsing this name's properties requires updating this Ref n.updateRefNode(declaration, varDecl.getFirstChild()); } /** * Declares global variables to serve as aliases for the values in an object literal, optionally * removing all of the object literal's keys and values. * * @param alias The object literal's flattened name (e.g. "a$b$c") * @param objlit The OBJLIT node * @param varNode The VAR node to which new global variables should be added as children * @param nameToAddAfter The child of {@code varNode} after which new variables should be added * (may be null) * @param varParent {@code varNode}'s parent */ private void declareVariablesForObjLitValues( Name objlitName, String alias, Node objlit, Node varNode, Node nameToAddAfter, Node varParent) { int arbitraryNameCounter = 0; boolean discardKeys = !objlitName.shouldKeepKeys(); for (Node key = objlit.getFirstChild(), nextKey; key != null; key = nextKey) { Node value = key.getFirstChild(); nextKey = key.getNext(); // A computed property, or a get or a set can not be rewritten as a VAR. We don't know what // properties will be generated by a spread. switch (key.getToken()) { case GETTER_DEF: case SETTER_DEF: case COMPUTED_PROP: case SPREAD: continue; case STRING_KEY: case MEMBER_FUNCTION_DEF: break; default: throw new IllegalStateException("Unexpected child of OBJECTLIT: " + key.toStringTree()); } // We generate arbitrary names for keys that aren't valid JavaScript // identifiers, since those keys are never referenced. (If they were, // this object literal's child names wouldn't be collapsible.) The only // reason that we don't eliminate them entirely is the off chance that // their values are expressions that have side effects. boolean isJsIdentifier = !key.isNumber() && TokenStream.isJSIdentifier(key.getString()); String propName = isJsIdentifier ? key.getString() : String.valueOf(++arbitraryNameCounter); // If the name cannot be collapsed, skip it. String qName = objlitName.getFullName() + '.' + propName; Name p = nameMap.get(qName); if (p != null && !canCollapse(p)) { continue; } String propAlias = appendPropForAlias(alias, propName); Node refNode = null; if (discardKeys) { objlit.removeChild(key); value.detach(); // Don't report a change here because the objlit has already been removed from the tree. } else { // Substitute a reference for the value. refNode = IR.name(propAlias); if (key.getBooleanProp(Node.IS_CONSTANT_NAME)) { refNode.putBooleanProp(Node.IS_CONSTANT_NAME, true); } key.replaceChild(value, refNode); compiler.reportChangeToEnclosingScope(refNode); } // Declare the collapsed name as a variable with the original value. Node nameNode = IR.name(propAlias); nameNode.addChildToFront(value); if (key.getBooleanProp(Node.IS_CONSTANT_NAME)) { nameNode.putBooleanProp(Node.IS_CONSTANT_NAME, true); } Node newVar = IR.var(nameNode).useSourceInfoIfMissingFromForTree(key); if (nameToAddAfter != null) { varParent.addChildAfter(newVar, nameToAddAfter); } else { varParent.addChildBefore(newVar, varNode); } compiler.reportChangeToEnclosingScope(newVar); nameToAddAfter = newVar; // Update the global name's node ancestry if it hasn't already been // done. (Duplicate keys in an object literal can bring us here twice // for the same global name.) if (isJsIdentifier && p != null) { if (!discardKeys) { p.addAliasingGetClonedFromDeclaration(refNode); } p.updateRefNode(p.getDeclaration(), nameNode); if (value.isFunction()) { checkForHosedThisReferences(value, key.getJSDocInfo(), p); } } } } /** * Adds global variable "stubs" for any properties of a global name that are only set in a local * scope or read but never set. * * @param n An object representing a global name (e.g. "a", "a.b.c") * @param alias The flattened name of the object whose properties we are adding stubs for (e.g. * "a$b$c") * @param parent The node to which new global variables should be added as children * @param addAfter The child of after which new variables should be added */ private void addStubsForUndeclaredProperties(Name n, String alias, Node parent, Node addAfter) { checkState(n.canCollapseUnannotatedChildNames(), n); checkArgument(NodeUtil.isStatementBlock(parent), parent); checkNotNull(addAfter); if (n.props == null) { return; } for (Name p : n.props) { if (p.needsToBeStubbed()) { String propAlias = appendPropForAlias(alias, p.getBaseName()); Node nameNode = IR.name(propAlias); Node newVar = IR.var(nameNode).useSourceInfoIfMissingFromForTree(addAfter); parent.addChildAfter(newVar, addAfter); addAfter = newVar; compiler.reportChangeToEnclosingScope(newVar); // Determine if this is a constant var by checking the first // reference to it. Don't check the declaration, as it might be null. if (p.getFirstRef().getNode().getLastChild().getBooleanProp(Node.IS_CONSTANT_NAME)) { nameNode.putBooleanProp(Node.IS_CONSTANT_NAME, true); compiler.reportChangeToEnclosingScope(nameNode); } } } } private String appendPropForAlias(String root, String prop) { if (prop.indexOf('$') != -1) { // Encode '$' in a property as '$0'. Because '0' cannot be the // start of an identifier, this will never conflict with our // encoding from '.' -> '$'. prop = prop.replace("$", "$0"); } String result = root + '$' + prop; int id = 1; while (nameMap.containsKey(result)) { result = root + '$' + prop + '$' + id; id++; } return result; } private void gatherDynamicallyImportedModules() { for (CompilerInput input : compiler.getInputsInOrder()) { dynamicallyImportedModules.addAll(input.getDynamicRequires()); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy