blob: 2593e823d7ec89ceffba43d0b75e67535f37d575 [file] [log] [blame]
/*
* 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.common.io.Files;
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.InputId;
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.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Apache Royale copied CollapseProperties and modified it to handle
* Royale modules.
*
* 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().
*
* <p>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).
*
* <p>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.
*
* <p>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.
*
* <p> 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:
* <code> a = {b: 0}; c = a; c.b = 5; </code>.
*
* <p>This pass doesn't flatten property accesses of the form: a[b].
*
* <p>For lots of examples, see the unit test.
*
*/
class CollapsePropertiesWithModuleSupport 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<Name> globalNames;
/** Maps names (e.g. "a.b.c") to nodes in the global namespace tree */
private Map<String, Name> nameMap;
/** name of a source file we can use to inject other code */
private String sourceFileName;
/** name of the file of renamed variables */
private File varRenameMapFile;
/** list of renamed variables */
private ArrayList<String> renamedVars = null;
/** list of aliases that are also in externs */
private List<String> externAliases;
/** list of aliases that came from goog.provides */
private List<String> providedAliases = new ArrayList<String>();
/** list of namespaces that came from goog.provides */
private List<String> providedNamespaces = new ArrayList<String>();
CollapsePropertiesWithModuleSupport(AbstractCompiler compiler, PropertyCollapseLevel propertyCollapseLevel, String sourceFileName, File varRenameMapFile) {
this.compiler = compiler;
this.propertyCollapseLevel = propertyCollapseLevel;
this.varRenameMapFile = varRenameMapFile;
this.sourceFileName = sourceFileName;
}
@Override
public void process(Node externs, Node root) {
// ProcessClosurePrimitives runs first and builds up an initial list of
// namespaces from goog.provides that were also in externs.
externAliases = ProcessClosurePrimitivesWithModuleSupport.externedAliases.get(externs);
int n = externAliases.size();
for (int i = 0; i < n; i++)
{
String s = externAliases.get(i);
String t = s.replace(".", "$");
externAliases.set(i, t);
}
// ProcessClosurePrimitives runs first and builds up an initial list of
// namespaces from goog.provides.
providedAliases = ProcessClosurePrimitivesWithModuleSupport.providedsMap.get(externs);
providedNamespaces.addAll(providedAliases);
n = providedAliases.size();
for (int i = 0; i < n; i++)
{
String s = providedAliases.get(i);
String t = s.replace(".", "$");
providedAliases.set(i, t);
}
/*
* The goal is to use knowledge of the aliases and which are externed
* to eliminate warnings and allow aliases in more places than
* closure compiler would normally. Closure compiler doesn't want to
* collapse things found in externs, but we want to collapse
* them to save size and need to collapse them when there are
* overrides in the module of a collapsed property in code
* in the main app. Or when the code in the main app is calling
* via an interface and code in the module implements that
* interface.
*/
GlobalNamespace namespace = new GlobalNamespace(compiler, externs, 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.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) &&
!providedNamespaces.contains(name.getFullName())) {
warnAboutNamespaceRedefinition(name, ref);
}
initialized = true;
} else if (ref.type == Ref.Type.ALIASING_GET) {
if (!providedNamespaces.contains(name.getFullName()) &&
!ref.name.inExterns())
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.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() || wasCollapsed(propAlias) || shouldCollapse(propAlias))) {
flattenReferencesTo(p, propAlias);
} else if (isAllowedToCollapse
&& p.isSimpleStubDeclaration()
&& !p.isCollapsingExplicitlyDenied()) {
flattenSimpleStubDeclaration(p, propAlias);
}
flattenReferencesToCollapsibleDescendantNames(p, propAlias);
}
}
/**
* we should collapse aliases in the module no matter what.
*
* @param propAlias The alias being considered.
* @return true If alias is for a namespace provided in the module.
*/
private boolean shouldCollapse(String propAlias) {
if (providedAliases.contains(propAlias))
{
InputId inputId = new InputId(
sourceFileName);
CompilerInput compilerInput = compiler.getInput(inputId);
Node nameNode = IR.name(propAlias);
Node child = IR.var(nameNode);
compilerInput.getAstRoot(compiler).addChildToBack(child);
compiler.reportChangeToEnclosingScope(child);
if (!externAliases.contains(propAlias))
externAliases.add(propAlias);
return true;
}
return false;
}
/**
* See if this property was collapse by the loading app.
* @param propAlias The alias that might have been collapsed.
* @return true If the alias was collapsed in the loading app.
*/
private boolean wasCollapsed(String propAlias) {
if (varRenameMapFile == null)
return false;
if (renamedVars == null)
{
List<String> fileLines;
renamedVars = new ArrayList<String>();
try {
fileLines = Files.readLines(varRenameMapFile, Charset.defaultCharset());
for (String line : fileLines)
{
int c = line.indexOf(":");
if (c > 0)
{
renamedVars.add(line.substring(0, c));
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if (renamedVars.contains(propAlias) || externAliases.contains(propAlias)) {
// if it was collapsed, add a var for it in our source for now.
// RemoveUnusedNames will move the var before it gets code-genned.
InputId inputId = new InputId(
sourceFileName);
CompilerInput compilerInput = compiler.getInput(inputId);
Node nameNode = IR.name(propAlias);
Node child = IR.var(nameNode);
compilerInput.getAstRoot(compiler).addChildToBack(child);
compiler.reportChangeToEnclosingScope(child);
// add it to the list of aliases from the externs so
// rename vars can know what variables we created here.
if (!externAliases.contains(propAlias))
externAliases.add(propAlias);
return true;
}
return false;
}
/**
* 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 (!r.node.isFromExterns() && !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.isFromExterns() && 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()) {
if (r.node != null && !r.node.isFromExterns())
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 != null && 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);
}
if (parent == null)
{
// in some cases parent is null (not sure why)
// but ref is a StringNode and a child of n
// is a StringNode so replace that StringNode
parent = n;
n = n.getFirstChild();
parent.replaceChild(n, ref);
}
else
{
parent.replaceChild(n, ref);
Node enclosingScopeNode = NodeUtil.getEnclosingChangeScopeRoot(n.getParent());
if (enclosingScopeNode != null)
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)/* && !externStrings.contains(n.getName())*/) {
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.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.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;
}
if (decl.node == null)
return; // package stubs (org.apache.royale)
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.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.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;
}
}