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

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

There is a newer version: 9.0.8
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.TypeI;
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 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}"); 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; CollapseProperties(AbstractCompiler compiler, PropertyCollapseLevel propertyCollapseLevel) { this.compiler = compiler; this.propertyCollapseLevel = propertyCollapseLevel; } @Override public void process(Node externs, Node root) { 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()) { 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.aliasingGets > 0 || name.localSets + name.globalSets > 1 || name.deleteProps > 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); } } } } } private boolean isSafeNamespaceReinit(Ref ref) { // allow "a = a || {}" or "var a = a || {}" Node valParent = getValueParent(ref); Node val = valParent.getLastChild(); if (val.isOr()) { Node maybeName = val.getFirstChild(); if (ref.node.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.node.getParent(); return (n != null && NodeUtil.isNameDeclaration(n)) ? ref.node : ref.node.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.node, 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.node, 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.node, name.getFullName()); Node varNode = IR.var(nameNode).useSourceInfoIfMissingFrom(nameNode); checkState(ref.node.getParent().isExprResult()); Node parent = ref.node.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.node.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.isObjectLitKey(r.node) && (r.getTwin() == null || r.isSet())) { flattenNameRef(alias, r.node, rParent, originalName); } } // 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.node != null && decl.node.isGetProp()) { flattenNameRefAtDepth(alias, decl.node, 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.node, 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.isObjectLitKey(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); } TypeI type = n.getTypeI(); if (type != null) { ref.setTypeI(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.node.isGetProp()) { return; } Node rvalue = ref.node.getNext(); Node parent = ref.node.getParent(); Node grandparent = parent.getParent(); if (rvalue != null && rvalue.isFunction()) { checkForHosedThisReferences(rvalue, refName.docInfo, refName); } // Create the new alias node. Node nameNode = NodeUtil.newName(compiler, alias, grandparent.getFirstChild(), refName.getFullName()); NodeUtil.copyNameAnnotations(ref.node.getLastChild(), nameNode); // BEFORE: // ... (x.y = 3); // // AFTER: // var x$y; // ... (x$y = 3); Node current = grandparent; Node currentParent = grandparent.getParent(); for (; !currentParent.isScript() && !currentParent.isNormalBlock(); 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.node, 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.node.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; 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.node.getNext(); if (ref.getTwin() != null) { updateTwinnedDeclaration(alias, ref.name, ref); return; } Node varNode = new Node(Token.VAR); Node varParent = ref.node.getAncestor(3); Node grandparent = ref.node.getAncestor(2); boolean isObjLit = rvalue.isObjectLit(); boolean insertedVarNode = false; if (isObjLit && canEliminate(n)) { // Eliminate the object literal altogether. varParent.replaceChild(grandparent, varNode); ref.node = null; insertedVarNode = true; compiler.reportChangeToEnclosingScope(varNode); } else if (!n.isSimpleName()) { // Create a VAR node to declare the name. if (rvalue.isFunction()) { checkForHosedThisReferences(rvalue, n.docInfo, n); } compiler.reportChangeToEnclosingScope(rvalue); ref.node.getParent().removeChild(rvalue); Node nameNode = NodeUtil.newName(compiler, alias, ref.node.getAncestor(2), n.getFullName()); JSDocInfo info = NodeUtil.getBestJSDocInfo(ref.node.getParent()); if (ref.node.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. ref.node = 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.traverseEs6(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.node.getString(); Node rvalue = ref.node.getFirstChild(); Node variableNode = ref.node.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.node); compiler.reportChangeToEnclosingScope(variableNode); if (!variableNode.hasChildren()) { grandparent.removeChild(variableNode); } // Clear out the object reference, since we've eliminated it from the // parse tree. ref.node = 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.node.getString(); addStubsForUndeclaredProperties(n, fnName, ref.node.getAncestor(2), ref.node.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.node.getString(); addStubsForUndeclaredProperties( n, className, ref.node.getAncestor(2), ref.node.getParent()); } /** * 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. if (key.isGetterDef() || key.isSetterDef() || key.isComputedProp()) { continue; } // 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) { Ref newAlias = p.getDeclaration().cloneAndReclassify(Ref.Type.ALIASING_GET); newAlias.node = refNode; p.addRef(newAlias); } p.getDeclaration().node = 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.getRefs().get(0).node.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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy