com.google.javascript.jscomp.CollapseProperties Maven / Gradle / Ivy
Show all versions of closure-compiler-linter Show documentation
/*
* 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());
}
}
}