com.google.javascript.jscomp.GlobalNamespace Maven / Gradle / Ivy
Show all versions of closure-compiler-unshaded 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.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Table;
import com.google.javascript.jscomp.CodingConvention.SubclassRelationship;
import com.google.javascript.jscomp.base.format.SimpleFormat;
import com.google.javascript.jscomp.diagnostic.LogFile;
import com.google.javascript.jscomp.modules.ModuleMap;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleMetadata;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.QualifiedName;
import com.google.javascript.rhino.StaticRef;
import com.google.javascript.rhino.StaticScope;
import com.google.javascript.rhino.StaticSlot;
import com.google.javascript.rhino.StaticSourceFile;
import com.google.javascript.rhino.StaticSymbolTable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.jspecify.nullness.Nullable;
/**
* Builds a namespace of all qualified names whose root is in the global scope or a module, plus an
* index of all references to those global names.
*
* This class tracks assignments to qualified names (e.g. `a.b.c`), because we often want to
* treat them as if they were global variables (e.g. CollapseProperties). However, when a qualified
* name begins an optional chain, we will not consider the optional chain to be part of the
* qualified name (e.g. `a.b?.c` is the qualified name `a.b` with an optional reference to property
* `c`.) We will record such optional chains as ALIASING_GET references to the non-optional
* qualified name part.
*
*
When used as a StaticScope this class acts like a single parentless global scope. The module
* references are currently only accessible by {@link #getNameFromModule(ModuleMetadata, String)},
* as many use cases only care about global names. (This may change as module rewriting is moved
* later in compilation). Module tracking also only occurs when {@link
* com.google.javascript.jscomp.modules.ModuleMapCreator} has run.
*
*
The namespace can be updated as the AST is changed. Removing names or references should be
* done by the methods on Name. Adding new names should be done with {@link #scanNewNodes}.
*/
class GlobalNamespace
implements StaticScope, StaticSymbolTable {
private final AbstractCompiler compiler;
private final boolean enableImplicitlyAliasedValues;
private final Node root;
private final Node externsRoot;
private final Node globalRoot = IR.root();
private final LinkedHashMap spreadSiblingCache = new LinkedHashMap<>();
private SourceKind sourceKind;
private boolean generated = false;
/**
* Records decisions made by this class.
*
* Since this class is a utility used by others, the creating class may provide the log file.
*/
private final @Nullable LogFile decisionsLog;
private static final QualifiedName GOOG_PROVIDE = QualifiedName.of("goog.provide");
enum SourceKind {
EXTERN,
TYPE_SUMMARY,
CODE;
static SourceKind fromScriptNode(Node n) {
if (!n.isFromExterns()) {
return CODE;
} else if (NodeUtil.isFromTypeSummary(n)) {
return TYPE_SUMMARY;
} else {
return EXTERN;
}
}
}
/** Global namespace tree */
private final List globalNames = new ArrayList<>();
/** Maps names (e.g. "a.b.c") to nodes in the global namespace tree */
private final Map nameMap = new LinkedHashMap<>();
/** Maps names (e.g. "a.b.c") and MODULE_BODY nodes to Names in that module */
private final Table nameMapByModule = HashBasedTable.create();
/** Limits traversal to scripts matching the given predicate. */
private Predicate shouldTraverseScript = (n) -> true;
/**
* Creates an instance that may emit warnings when building the namespace.
*
* @param compiler The AbstractCompiler, for reporting code changes
* @param root The root of the rest of the code to build a namespace for.
*/
GlobalNamespace(LogFile decisionsLog, AbstractCompiler compiler, Node root) {
this(decisionsLog, compiler, null, root);
}
/**
* Creates an instance that may emit warnings when building the namespace.
*
* @param compiler The AbstractCompiler, for reporting code changes
* @param root The root of the rest of the code to build a namespace for.
*/
GlobalNamespace(AbstractCompiler compiler, Node root) {
this(/* decisionsLog= */ null, compiler, null, root);
}
/**
* Creates an instance that may emit warnings when building the namespace.
*
* @param compiler The AbstractCompiler, for reporting code changes
* @param externsRoot The root of the externs to build a namespace for. If this is null, externs
* and properties defined on extern types will not be included in the global namespace. If
* non-null, it allows user-defined function on extern types to be included in the global
* namespace. E.g. String.foo.
* @param root The root of the rest of the code to build a namespace for.
*/
GlobalNamespace(AbstractCompiler compiler, Node externsRoot, Node root) {
this(null, compiler, externsRoot, root);
}
/**
* Creates an instance that may emit warnings when building the namespace.
*
* @param decisionsLog where to log decisions made by this instance
* @param compiler The AbstractCompiler, for reporting code changes
* @param externsRoot The root of the externs to build a namespace for. If this is null, externs
* and properties defined on extern types will not be included in the global namespace. If
* non-null, it allows user-defined function on extern types to be included in the global
* namespace. E.g. String.foo.
* @param root The root of the rest of the code to build a namespace for.
*/
GlobalNamespace(
@Nullable LogFile decisionsLog,
AbstractCompiler compiler,
@Nullable Node externsRoot,
Node root) {
this.decisionsLog = decisionsLog;
this.compiler = compiler;
this.externsRoot = externsRoot;
this.root = root;
this.enableImplicitlyAliasedValues =
!compiler.getOptions().getAssumeStaticInheritanceIsNotUsed();
}
void setShouldTraverseScriptPredicate(Predicate shouldTraverseScript) {
this.shouldTraverseScript = shouldTraverseScript;
}
boolean hasExternsRoot() {
return externsRoot != null;
}
@Override
public Node getRootNode() {
return root.getParent();
}
/**
* Returns the root node of the scope in which the root of a qualified name is declared, or null.
*
* @param name A variable name (e.g. "a")
* @param s The scope in which the name is referenced
* @return The root node of the scope in which this is defined, or null if this is undeclared.
*/
private @Nullable Node getRootNode(String name, Scope s) {
name = getTopVarName(name);
Var v = s.getVar(name);
if (v == null) {
Name providedName = nameMap.get(name);
return providedName != null && providedName.getBooleanProperty(NameProp.IS_PROVIDED)
? globalRoot
: null;
}
return v.isLocal() ? v.getScopeRoot() : globalRoot;
}
@Override
public StaticScope getParentScope() {
return null;
}
@Override
public Name getSlot(String name) {
return getOwnSlot(name);
}
@Override
public Name getOwnSlot(String name) {
ensureGenerated();
return nameMap.get(name);
}
@Override
public Iterable getReferences(Name slot) {
ensureGenerated();
return Collections.unmodifiableCollection(slot.getRefs());
}
@Override
public StaticScope getScope(Name slot) {
return this;
}
@Override
public Collection getAllSymbols() {
ensureGenerated();
return Collections.unmodifiableCollection(getNameIndex().values());
}
private void ensureGenerated() {
if (!generated) {
process();
}
}
/**
* Gets a list of the roots of the forest of the global names, where the roots are the top-level
* names.
*/
List getNameForest() {
ensureGenerated();
return globalNames;
}
/**
* Gets an index of all the global names, indexed by full qualified name (as in "a", "a.b.c",
* etc.).
*/
Map getNameIndex() {
ensureGenerated();
return nameMap;
}
/**
* A simple data class that contains the information necessary to inspect a node for changes to
* the global namespace.
*/
static class AstChange {
final Scope scope;
final Node node;
AstChange(Scope scope, Node node) {
this.scope = scope;
this.node = node;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof AstChange) {
AstChange other = (AstChange) obj;
return Objects.equals(this.scope, other.scope) && Objects.equals(this.node, other.node);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(this.scope, this.node);
}
}
/**
* If the client adds new nodes to the AST, scan these new nodes to see if they've added any
* references to the global namespace.
*
* @param newNodes New nodes to check.
*/
void scanNewNodes(Set newNodes) {
BuildGlobalNamespace builder = new BuildGlobalNamespace();
for (AstChange info : newNodes) {
if (!info.node.isQualifiedName() && !NodeUtil.mayBeObjectLitKey(info.node)) {
continue;
}
scanFromNode(builder, info.scope, info.node);
}
}
private void scanFromNode(BuildGlobalNamespace builder, Scope scope, Node n) {
// Check affected parent nodes first.
Node parent = n.getParent();
if ((n.isName() || n.isGetProp()) && parent.isGetProp()) {
// e.g. when replacing "my.alias.prop" with "foo.bar.prop"
// we want also want to visit "foo.bar.prop", since that's a new global qname we are now
// referencing.
scanFromNode(builder, scope, n.getParent());
} else if (n.getPrevious() != null && n.getPrevious().isObjectPattern()) {
// e.g. if we change `const {x} = bar` to `const {x} = foo`, add a new reference to `foo.x`
// attached to the STRING_KEY `x`
Node pattern = n.getPrevious();
for (Node key = pattern.getFirstChild(); key != null; key = key.getNext()) {
if (key.isStringKey()) {
scanFromNode(builder, scope, key);
}
}
}
builder.collect(scope, n);
}
/** Builds the namespace lazily. */
private void process() {
NodeTraversal.Builder traversal =
NodeTraversal.builder().setCompiler(compiler).setCallback(new BuildGlobalNamespace());
if (hasExternsRoot()) {
traversal.traverseRoots(externsRoot, root);
} else {
traversal.traverse(root);
}
generated = true;
}
/**
* Gets the top variable name from a possibly namespaced name.
*
* @param name A variable or qualified property name (e.g. "a" or "a.b.c.d")
* @return The top variable name (e.g. "a")
*/
private static String getTopVarName(String name) {
int firstDotIndex = name.indexOf('.');
return firstDotIndex == -1 ? name : name.substring(0, firstDotIndex);
}
@Nullable Name getNameFromModule(ModuleMetadata moduleMetadata, String name) {
checkNotNull(moduleMetadata);
checkNotNull(name);
ensureGenerated();
return nameMapByModule.get(moduleMetadata, name);
}
/**
* Returns whether a declaration node, inside an object-literal, has a following OBJECT_SPREAD
* sibling.
*
* This check is implemented using a cache because otherwise it has aggregate {@code O(n^2)}
* performance in terms of the size of an OBJECTLIT. If each declaration checked each sibling
* independently, each sibling would be checked up-to once for each of it's preceeding siblings.
*/
private boolean declarationHasFollowingObjectSpreadSibling(Node declaration) {
checkState(declaration.getParent().isObjectLit(), declaration);
@Nullable Boolean cached = this.spreadSiblingCache.get(declaration);
if (cached != null) {
return cached;
}
/*
* Iterate backward over all children of the object-literal, filling in the cache.
*
*
We iterate the entire literal because we expect to eventually need the result for each of
* them. Additionally, it makes the loop conditions simpler.
*
*
We use a loop rather than recursion to minimize stack depth. Large object-literals were
* the reason caching was added.
*/
boolean toCache = false;
for (Node sibling = declaration.getParent().getLastChild();
sibling != null;
sibling = sibling.getPrevious()) {
if (sibling.isSpread()) {
toCache = true;
}
this.spreadSiblingCache.put(sibling, toCache);
}
return this.spreadSiblingCache.get(declaration);
}
// -------------------------------------------------------------------------
/** Builds a tree representation of the global namespace. Omits prototypes. */
private class BuildGlobalNamespace extends NodeTraversal.AbstractPreOrderCallback {
private @Nullable Node curModuleRoot = null;
private @Nullable ModuleMetadata curMetadata = null;
/** Collect the references in pre-order. */
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
if (n.isScript() && !shouldTraverseScript.test(n)) {
return false;
}
if (hasExternsRoot() && n.isScript()) {
// When checking type-summary files, we want to consider them like normal code
// for some things (like alias inlining) but like externs for other things.
sourceKind = SourceKind.fromScriptNode(n);
} else if (n == root) {
sourceKind = SourceKind.CODE;
}
if (n.isModuleBody() || NodeUtil.isBundledGoogModuleScopeRoot(n)) {
setupModuleMetadata(n);
} else if (n.isScript() || NodeUtil.isBundledGoogModuleCall(n)) {
curModuleRoot = null;
curMetadata = null;
}
collect(t.getScope(), n);
return true;
}
/**
* Initializes the {@link ModuleMetadata} for a goog;.module or ES module
*
* @param moduleRoot either a MODULE_BODY or a goog.loadModule BLOCK.
*/
private void setupModuleMetadata(Node moduleRoot) {
ModuleMap moduleMap = compiler.getModuleMap();
if (moduleMap == null) {
return;
}
curModuleRoot = moduleRoot;
curMetadata =
checkNotNull(
ModuleImportResolver.getModuleFromScopeRoot(moduleMap, compiler, moduleRoot)
.metadata());
if (curMetadata.isGoogModule()) {
getOrCreateName("exports", curMetadata);
}
}
private void collect(Scope scope, Node n) {
Node parent = n.getParent();
String name;
boolean isSet = false;
NameProp type = NameProp.OTHER_OBJECT;
switch (n.getToken()) {
case GETTER_DEF:
case SETTER_DEF:
case MEMBER_FUNCTION_DEF:
if (parent.isClassMembers() && !n.isStaticMember()) {
return; // Within a class, only static members define global names.
}
name = NodeUtil.getBestLValueName(n);
isSet = true;
type = n.isMemberFunctionDef() ? NameProp.FUNCTION : NameProp.GET_SET;
break;
case STRING_KEY:
name = null;
if (parent.isObjectLit()) {
ObjLitStringKeyAnalysis analysis = createObjLitStringKeyAnalysis(n);
name = analysis.getNameString();
type = analysis.getNameType();
isSet = true;
} else if (parent.isObjectPattern()) {
name = getNameForObjectPatternKey(n);
type = getValueType(n.getFirstChild());
// not a set
} // else not a reference we should record
break;
case NAME:
case GETPROP:
// OPTCHAIN_GETPROP is intentionally not included in this case.
// "a.b?.c" is not a reference to the global name "a.b.c" for the
// purposes of GlobalNamespace.
// TODO(b/127505242): CAST parents may indicate a set.
// This may be a variable get or set.
switch (parent.getToken()) {
case VAR:
case LET:
case CONST:
isSet = true;
Node rvalue = n.getFirstChild();
type = (rvalue == null) ? NameProp.OTHER_OBJECT : getValueType(rvalue);
break;
case ASSIGN:
if (parent.getFirstChild() == n) {
isSet = true;
type = getValueType(n.getNext());
}
break;
case GETPROP:
// This name is nested in a getprop. Return and only create a Ref for the outermost
// getprop in the chain.
return;
case FUNCTION:
Node grandparent = parent.getParent();
if (grandparent == null || NodeUtil.isFunctionExpression(parent)) {
return;
}
isSet = true;
type = NameProp.FUNCTION;
break;
case CATCH:
case INC:
case DEC:
isSet = true;
type = NameProp.OTHER_OBJECT;
break;
case CLASS:
// The first child is the class name, and the second child is the superclass name.
if (parent.getFirstChild() == n) {
isSet = true;
type = NameProp.CLASS;
}
break;
case STRING_KEY:
case ARRAY_PATTERN:
case DEFAULT_VALUE:
case COMPUTED_PROP:
case ITER_REST:
case OBJECT_REST:
// This may be a set.
// TODO(b/120303257): this should extend to qnames too, but doing
// so causes invalid output. Covered in CollapsePropertiesTest
if (n.isName() && NodeUtil.isLhsByDestructuring(n)) {
isSet = true;
type = NameProp.OTHER_OBJECT;
}
break;
case ITER_SPREAD:
case OBJECT_SPREAD:
break; // isSet = false, type = OTHER.
case CALL:
if (n.isFirstChildOf(parent) && isObjectHasOwnPropertyCall(parent)) {
String qname = n.getFirstChild().getQualifiedName();
Name globalName = getOrCreateName(qname, curMetadata);
globalName.setBooleanProperty(NameProp.IS_USED_HAS_OWN_PROPERTY);
}
break;
default:
if (NodeUtil.isAssignmentOp(parent) && parent.getFirstChild() == n) {
isSet = true;
type = NameProp.OTHER_OBJECT;
}
}
if (!n.isQualifiedName()) {
return;
}
name = n.getQualifiedName();
break;
case CALL:
if (parent.isExprResult()
&& GOOG_PROVIDE.matches(n.getFirstChild())
&& n.getSecondChild().isStringLit()) {
// goog.provide goes through a different code path than regular sets because it can
// create multiple names, e.g. `goog.provide('a.b.c');` creates the global names
// a, a.b, and a.b.c. Other sets only create a single global name.
createNamesFromProvide(n.getSecondChild().getString());
return;
}
return;
default:
return;
}
if (name == null) {
return;
}
Node root = getRootNode(name, scope);
// We are only interested in global and module names.
if (!isTopLevelScopeRoot(root)) {
return;
}
ModuleMetadata nameMetadata = root == globalRoot ? null : curMetadata;
if (isSet) {
// Use the closest hoist scope to select handleSetFromGlobal or handleSetFromLocal
// because they use the term 'global' in an ES5, pre-block-scoping sense.
Scope hoistScope = scope.getClosestHoistScope();
// Consider a set to be 'global' if it is in the hoist scope in which the name is defined.
// For example, a global name set in a module scope is a 'local' set, but a module-level
// name set in a module scope is a 'global' set.
if (hoistScope.isGlobal()
|| (root != globalRoot && hoistScope.getRootNode() == curModuleRoot)) {
handleSetFromGlobal(scope, n, name, type, nameMetadata);
} else {
handleSetFromLocal(scope, n, name, nameMetadata);
}
} else {
handleGet(scope, n, name, nameMetadata);
}
}
private ObjLitStringKeyAnalysis createObjLitStringKeyAnalysis(Node stringKeyNode) {
String nameString = NodeUtil.getBestLValueName(stringKeyNode);
if (nameString != null) {
// `parent.qname = { myPropName: myValue }`;
// `NodeUtil.getBestLValueName()` finds the name being assigned for this case.
return ObjLitStringKeyAnalysis.forObjLitAssignment(
nameString, getValueType(stringKeyNode.getOnlyChild()));
} else {
// maybe we have a case like
// `Object.defineProperties(parentName, { myPropName: { get: ..., set: ..., ... })`
Node objLitNode = stringKeyNode.getParent();
checkArgument(objLitNode.isObjectLit(), objLitNode);
Node objLitParentNode = objLitNode.getParent();
if (NodeUtil.isObjectDefinePropertiesDefinition(objLitParentNode)) {
Node receiverNode = objLitParentNode.getSecondChild();
if (receiverNode.isQualifiedName()) {
checkState(objLitNode == receiverNode.getNext(), objLitParentNode);
nameString = receiverNode.getQualifiedName() + "." + stringKeyNode.getString();
return ObjLitStringKeyAnalysis.forObjectDefineProperty(nameString);
}
}
return ObjLitStringKeyAnalysis.forNonReference();
}
}
/** Declares all subnamespaces from `goog.provide('some.long.namespace')` globally. */
private void createNamesFromProvide(String namespace) {
Name name;
int dot = 0;
while (dot >= 0) {
dot = namespace.indexOf('.', dot + 1);
String subNamespace = dot < 0 ? namespace : namespace.substring(0, dot);
checkState(!subNamespace.isEmpty());
name = getOrCreateName(subNamespace, null);
name.setBooleanProperty(NameProp.IS_PROVIDED);
}
Name newName = getOrCreateName(namespace, null);
newName.setBooleanProperty(NameProp.IS_PROVIDED);
}
/**
* Whether the given name root represents a global or module-level name
*
*
This method will return false for functions and blocks and true for module bodies and the
* {@code globalRoot}. The one exception is if the function or block is from a goog.loadModule
* argument, as those functions/blocks are treated as module roots.
*/
private boolean isTopLevelScopeRoot(Node root) {
if (root == null) {
return false;
} else if (root == globalRoot) {
return true;
} else if (root == curModuleRoot) {
return true;
}
// Given
// goog.loadModule(function(exports) {
// pretend that assignments to `exports` or `exports.x = ...` are scoped to the function body,
// although `exports` is really in the enclosing function parameter scope.
return curModuleRoot != null && curModuleRoot.isBlock() && root == curModuleRoot.getParent();
}
/**
* Gets the fully qualified name corresponding to an object pattern key, as long as it is not in
* a nested pattern and is destructuring an qualified name.
*
* @param stringKey A child of an OBJECT_PATTERN node
* @return The global name, or null if {@code n} doesn't correspond to the key of an object
* literal that can be named
*/
@Nullable String getNameForObjectPatternKey(Node stringKey) {
Node parent = stringKey.getParent();
checkState(parent.isObjectPattern());
Node patternParent = parent.getParent();
if (patternParent.isAssign() || patternParent.isDestructuringLhs()) {
// this is a top-level string key. we find the name.
Node rhs = patternParent.getSecondChild();
if (rhs == null || !rhs.isQualifiedName()) {
// The rhs is null for patterns in parameter lists, enhanced for loops, and catch exprs
return null;
}
return rhs.getQualifiedName() + "." + stringKey.getString();
} else {
// skip this step for nested patterns for now
return null;
}
}
/**
* Gets the type of a value or simple expression.
*
* @param n An r-value in an assignment or variable declaration (not null)
*/
NameProp getValueType(Node n) {
switch (n.getToken()) {
case CLASS:
return NameProp.CLASS;
case OBJECTLIT:
return NameProp.OBJECTLIT;
case FUNCTION:
return NameProp.FUNCTION;
case OR:
// Recurse on the second value. If the first value were an object
// literal or function, then the OR would be meaningless and the
// second value would be dead code. Assume that if the second value
// is an object literal or function, then the first value will also
// evaluate to one when it doesn't evaluate to false.
return getValueType(n.getLastChild());
case HOOK:
// The same line of reasoning used for the OR case applies here.
Node second = n.getSecondChild();
NameProp t = getValueType(second);
if (t != NameProp.OTHER_OBJECT) {
return t;
}
Node third = second.getNext();
return getValueType(third);
default:
break;
}
return NameProp.OTHER_OBJECT;
}
/**
* Updates our representation of the global namespace to reflect an assignment to a global name
* in any scope where variables are hoisted to the global scope (i.e. the global scope in an ES5
* sense).
*
* @param scope the current scope
* @param n The node currently being visited
* @param name The global name (e.g. "a" or "a.b.c.d")
* @param type The type of the value that the name is being assigned
*/
void handleSetFromGlobal(
Scope scope, Node n, String name, NameProp type, ModuleMetadata metadata) {
if (maybeHandlePrototypePrefix(scope, n, name, metadata)) {
return;
}
Name nameObj = getOrCreateName(name, metadata);
if (!nameObj.isGetOrSetDefinition()) {
// Don't change the type of a getter or setter. This is because given:
// var a = {set b(item) {}}; a.b = class {};
// `a.b = class {};` does not change the runtime value of a.b, and we do not want to change
// the 'type' of a.b to Type.CLASS.
// TODO(lharker): for non-setter cases, do we really want to just treat the last set of
// a name as canonical? e.g. what if a name is first set to a class, then an object literal?
nameObj.setNameType(type);
}
if (n.getBooleanProp(Node.MODULE_EXPORT)) {
nameObj.setBooleanProperty(NameProp.IS_MODULE_PROP);
}
if (isNestedAssign(n.getParent())) {
// This assignment is both a set and a get that creates an alias.
Ref.Type refType = Ref.Type.GET_AND_SET_FROM_GLOBAL;
addOrConfirmRef(nameObj, n, refType, scope);
} else {
addOrConfirmRef(nameObj, n, Ref.Type.SET_FROM_GLOBAL, scope);
nameObj.setDeclaredTypeKind(getDeclaredTypeKind(n));
}
}
/**
* Determines whether a set operation is a constructor or enumeration or interface declaration.
* The set operation may either be an assignment to a name, a variable declaration, or an object
* literal key mapping.
*
* @param n The node that represents the name being set
*/
private NameProp getDeclaredTypeKind(Node n) {
Node valueNode = NodeUtil.getRValueOfLValue(n);
final NameProp kind;
if (valueNode == null) {
kind = NameProp.NOT_A_TYPE;
} else if (valueNode.isClass()) {
// Always treat classes as having a declared type. (Transpiled classes are annotated
// @constructor)
kind = NameProp.CONSTRUCTOR_TYPE;
} else {
JSDocInfo info = NodeUtil.getBestJSDocInfo(n);
// Heed the annotations only if they're sensibly used.
if (info == null) {
kind = NameProp.NOT_A_TYPE;
} else if (info.isConstructor() && valueNode.isFunction()) {
kind = NameProp.CONSTRUCTOR_TYPE;
} else if (info.isInterface() && valueNode.isFunction()) {
kind = NameProp.INTERFACE_TYPE;
} else if (info.hasEnumParameterType() && valueNode.isObjectLit()) {
kind = NameProp.ENUM_TYPE;
} else {
kind = NameProp.NOT_A_TYPE;
}
}
return kind;
}
/**
* Updates our representation of the global namespace to reflect an assignment to a global name
* in a local scope.
*
* @param scope The current scope
* @param n The node currently being visited
* @param name The global name (e.g. "a" or "a.b.c.d")
*/
void handleSetFromLocal(Scope scope, Node n, String name, ModuleMetadata metadata) {
if (maybeHandlePrototypePrefix(scope, n, name, metadata)) {
return;
}
Name nameObj = getOrCreateName(name, metadata);
if (n.getBooleanProp(Node.MODULE_EXPORT)) {
nameObj.setBooleanProperty(NameProp.IS_MODULE_PROP);
}
if (isNestedAssign(n.getParent())) {
// This assignment is both a set and a get that creates an alias.
addOrConfirmRef(nameObj, n, Ref.Type.GET_AND_SET_FROM_LOCAL, scope);
} else {
addOrConfirmRef(nameObj, n, Ref.Type.SET_FROM_LOCAL, scope);
}
}
/**
* Updates our representation of the global namespace to reflect a read of a global name.
*
* @param scope The current scope
* @param n The node currently being visited
* @param name The global name (e.g. "a" or "a.b.c.d")
*/
void handleGet(Scope scope, Node n, String name, ModuleMetadata metadata) {
if (maybeHandlePrototypePrefix(scope, n, name, metadata)) {
return;
}
Ref.Type type = determineRefTypeForGet(n, n, name);
addOrConfirmRef(getOrCreateName(name, metadata), n, type, scope);
}
/**
* Determine the Ref.Type for referenceNode by inspecting parents and recusively ascending as
* necessary, where n represents.
*
* @param n The node currently being visited
* @param referenceNode The node hosting the name.
* @param name The global name (e.g. "a" or "a.b.c.d")
*/
private Ref.Type determineRefTypeForGet(Node n, Node referenceNode, String name) {
Ref.Type type;
Node parent = n.getParent();
switch (parent.getToken()) {
case EXPR_RESULT:
case IF:
case WHILE:
case FOR:
case INSTANCEOF:
case TYPEOF:
case VOID:
case NOT:
case BITNOT:
case POS:
case NEG:
case SHEQ:
case EQ:
case SHNE:
case NE:
case LT:
case LE:
case GT:
case GE:
case ADD:
case SUB:
case MUL:
case DIV:
case MOD:
case EXPONENT:
case BITAND:
case BITOR:
case BITXOR:
case LSH:
case RSH:
case URSH:
type = Ref.Type.DIRECT_GET;
break;
case OPTCHAIN_CALL:
case CALL:
if (n == parent.getFirstChild()) {
// It is a call target
type = Ref.Type.CALL_GET;
} else if (isClassDefiningCall(parent)) {
type = Ref.Type.DIRECT_GET;
} else {
type = Ref.Type.ALIASING_GET;
}
break;
case NEW:
type = n == parent.getFirstChild() ? Ref.Type.DIRECT_GET : Ref.Type.ALIASING_GET;
break;
case CAST:
case OR:
case AND:
case COALESCE:
// This node is x or y in (x||y), (x&&y), or (x??y). We only know that an
// alias is not getting created for this name if the result is used
// in a boolean context or assigned to the same name
// (e.g. var a = a || {}).
type = determineRefTypeForGet(parent, referenceNode, name);
break;
case NAME:
// Only LET, CONST, VAR declarations have NAME nodes
// with children.
// Of particular interest is "var n = n || {}"
if (n != referenceNode && name.equals(parent.getString())) {
type = Ref.Type.DIRECT_GET;
} else {
type = Ref.Type.ALIASING_GET;
}
break;
case COMMA:
case HOOK:
if (n != parent.getFirstChild()) {
// This node is y or z in (x?y:z) or (x,y). We only know that an alias is
// not getting created for this name if the result is assigned to
// the same name (e.g. var a = a ? a : {}).
type = determineRefTypeForGet(parent, referenceNode, name);
} else {
type = Ref.Type.DIRECT_GET;
}
break;
case DELPROP:
type = Ref.Type.DELETE_PROP;
break;
case CLASS:
// This node is the superclass in an extends clause.
type = Ref.Type.SUBCLASSING_GET;
break;
case DESTRUCTURING_LHS:
case ASSIGN:
Node lhs = n.getPrevious();
if (lhs == null) {
// TODO(b/127505242): CAST confused the "is this a get or set?"
// logic and "handleGet" should not have been called.
type = Ref.Type.ALIASING_GET;
break;
}
while (lhs.isCast()) {
// Case: `/** @type {!Foo} */ (x) = ...`; or multiple casts like `(cast(cast(x)) =`
lhs = lhs.getOnlyChild();
}
// This is a recursive ascent check if this an assignment
// to itself. This handles cases like: "a.b = a.b || {}"
if (n != referenceNode) {
if (lhs.matchesQualifiedName(name)) {
return Ref.Type.DIRECT_GET;
}
}
switch (lhs.getToken()) {
case NAME:
case GETPROP:
case GETELEM:
// The rhs of an assign or a name declaration is escaped if it's assigned to a name
// directly ...
case ARRAY_PATTERN:
case OBJECT_PATTERN:
// ... or referenced through numeric/object keys.
type = Ref.Type.ALIASING_GET;
break;
default:
throw new IllegalStateException(
"Unexpected previous sibling of " + n.getToken() + ": " + n.getPrevious());
}
break;
case OBJECT_PATTERN: // Handle STRING_KEYS in object patterns.
case ITER_SPREAD:
case OBJECT_SPREAD:
case RETURN:
case THROW:
default:
// NOTE: There are likely more cases where we should be returning
// DIRECT_GET.
type = Ref.Type.ALIASING_GET;
break;
}
return type;
}
/**
* If there is already a Ref for the given name & node, confirm it matches what we would create.
* Otherwise add a new one.
*/
private void addOrConfirmRef(Name nameObj, Node node, Ref.Type refType, Scope scope) {
Ref existingRef = nameObj.getRefForNode(node);
if (existingRef == null) {
nameObj.addRef(scope, node, refType);
} else {
// module and scope are dependent on Node, so not much point in checking them
Ref.Type existingRefType = existingRef.type;
checkState(
existingRefType == refType,
"existing ref type: %s expected: %s",
existingRefType,
refType);
}
}
private boolean isClassDefiningCall(Node callNode) {
CodingConvention convention = compiler.getCodingConvention();
// Look for goog.inherits and J2CL mixin calls
SubclassRelationship classes = convention.getClassesDefinedByCall(callNode);
if (classes != null) {
return true;
}
// Look for calls to goog.addSingletonGetter calls.
String className = convention.getSingletonGetterClassName(callNode);
return className != null;
}
/** Detect calls of the form a.b.hasOwnProperty(c); that prevent property collapsing on a.b */
private boolean isObjectHasOwnPropertyCall(Node callNode) {
checkArgument(callNode.isCall(), callNode);
if (!callNode.hasTwoChildren()) {
return false;
}
Node callee = callNode.getFirstChild();
if (!callee.isGetProp()) {
return false;
}
Node receiver = callee.getFirstChild();
return "hasOwnProperty".equals(callee.getString()) && receiver.isQualifiedName();
}
/**
* Updates our representation of the global namespace to reflect a read of a global name's
* longest prefix before the "prototype" property if the name includes the "prototype" property.
* Does nothing otherwise.
*
* @param scope The current scope
* @param n The node currently being visited
* @param name The global name (e.g. "a" or "a.b.c.d")
* @return Whether the name was handled
*/
boolean maybeHandlePrototypePrefix(Scope scope, Node n, String name, ModuleMetadata metadata) {
// We use a string-based approach instead of inspecting the parse tree
// to avoid complexities with object literals, possibly nested, beneath
// assignments.
int numLevelsToRemove;
String prefix;
if (name.endsWith(".prototype")) {
numLevelsToRemove = 1;
prefix = name.substring(0, name.length() - 10);
} else {
int i = name.indexOf(".prototype.");
if (i == -1) {
return false;
}
prefix = name.substring(0, i);
numLevelsToRemove = 2;
i = name.indexOf('.', i + 11);
while (i >= 0) {
numLevelsToRemove++;
i = name.indexOf('.', i + 1);
}
}
if (NodeUtil.mayBeObjectLitKey(n)) {
// Object literal keys have no prefix that's referenced directly per
// key, so we're done.
return true;
}
for (int i = 0; i < numLevelsToRemove; i++) {
n = n.getFirstChild();
}
addOrConfirmRef(getOrCreateName(prefix, metadata), n, Ref.Type.PROTOTYPE_GET, scope);
return true;
}
/**
* Determines whether an assignment is nested (i.e. whether its return value is used).
*
* @param parent The parent of the current traversal node (not null)
* @return Whether it appears that the return value of the assignment is used
*/
boolean isNestedAssign(Node parent) {
return parent.isAssign() && !parent.getParent().isExprResult();
}
/**
* Gets a {@link Name} instance for a global name. Creates it if necessary, as well as instances
* for any of its prefixes that are not yet defined.
*
* @param name A global name (e.g. "a", "a.b.c.d")
* @return The {@link Name} instance for {@code name}
*/
Name getOrCreateName(String name, @Nullable ModuleMetadata metadata) {
Name node = metadata == null ? nameMap.get(name) : nameMapByModule.get(metadata, name);
if (node == null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
String parentName = name.substring(0, i);
Name parent = getOrCreateName(parentName, metadata);
node = parent.addProperty(name.substring(i + 1), sourceKind);
if (metadata == null) {
nameMap.put(name, node);
} else {
nameMapByModule.put(metadata, name, node);
}
} else {
node = new Name(name, null, sourceKind);
if (metadata == null) {
globalNames.add(node);
nameMap.put(name, node);
} else {
nameMapByModule.put(metadata, name, node);
}
}
}
return node;
}
}
// -------------------------------------------------------------------------
@VisibleForTesting
Name createNameForTesting(String name) {
return new Name(name, null, SourceKind.CODE);
}
/**
* How much to inline a {@link Name}.
*
*
The `Inlinability#INLINE_BUT_KEEP_DECLARATION_*` cass are really an indicator that something
* 'unsafe' is happening in order to not break CollapseProperties as badly. Sadly {@link
* Inlinability #INLINE_COMPLETELY} may also be unsafe.
*/
enum Inlinability {
INLINE_COMPLETELY(
/* shouldInlineUsages= */ true,
/* shouldRemoveDeclaration= */ true,
/* canCollapse= */ true),
INLINE_BUT_KEEP_DECLARATION_ENUM(
/* shouldInlineUsages= */ true,
/* shouldRemoveDeclaration= */ false,
/* canCollapse= */ true),
INLINE_BUT_KEEP_DECLARATION_INTERFACE(
/* shouldInlineUsages= */ true,
/* shouldRemoveDeclaration= */ false,
/* canCollapse= */ true),
INLINE_BUT_KEEP_DECLARATION_CLASS(
/* shouldInlineUsages= */ true,
/* shouldRemoveDeclaration= */ false,
/* canCollapse= */ true),
DO_NOT_INLINE(
/* shouldInlineUsages= */ false,
/* shouldRemoveDeclaration= */ false,
/* canCollapse= */ false);
private final boolean shouldInlineUsages;
private final boolean shouldRemoveDeclaration;
private final boolean canCollapse;
Inlinability(boolean shouldInlineUsages, boolean shouldRemoveDeclaration, boolean canCollapse) {
this.shouldInlineUsages = shouldInlineUsages;
this.shouldRemoveDeclaration = shouldRemoveDeclaration;
this.canCollapse = canCollapse;
}
boolean shouldInlineUsages() {
return this.shouldInlineUsages;
}
boolean shouldRemoveDeclaration() {
return this.shouldRemoveDeclaration;
}
boolean canCollapse() {
return this.canCollapse;
}
}
/**
* A name defined in global scope (e.g. "a" or "a.b.c.d").
*
*
Instances form a tree describing the "Closure namespaces" in the program. As the parse tree
* traversal proceeds, we'll discover that some names correspond to JavaScript objects whose
* properties we should consider collapsing.
*/
final class Name implements StaticSlot {
private final String baseName;
private final Name parent;
// The children of this name. Must be null if there are no children.
@Nullable List props;
/** The declaration of a name. */
private @Nullable Ref declaration;
/** The first global assignment to a name. */
private @Nullable Ref initialization;
/**
* Keep track of which Nodes are Refs for this Name.
*
* This is either null, a {@code Map} refsForNodeMap or a singleton Ref.
*
* This makes the code using refsForNode more complex but greatly decreases the memory usage
* of this class. For example, one project had more than 5 million Names, and more than 80% of
* those Names had exactly 1 Ref, their declaration. So specializing this field to sometimes be
* a single Ref, not a map, saves on creating > 4 million Map instances.
*/
private Object refsForNode = null;
private int globalSets = 0;
private int localSets = 0;
private int localSetsWithNoCollapse = 0;
private int aliasingGets = 0;
private int totalGets = 0;
private int callGets = 0;
private int deleteProps = 0;
private int subclassingGets = 0;
/**
* Bitset containing {@link NameProp}s
*
*
Using a bit set over boolean fields is a memory optimization. Large projects can have
* millions of Name objects, so saving a few bytes per Name is useful.
*/
private int propertyBitSet = 0;
// Will be set to the JSDocInfo associated with the first SET_FROM_GLOBAL reference added
// that has JSDocInfo.
// e.g.
// /** @type {number} */
// X.numberProp = 3;
private @Nullable JSDocInfo firstDeclarationJSDocInfo = null;
// Will be set to the JSDocInfo associated with the first get reference that is a statement
// by itself.
// e.g.
// /** @type {number} */
// X.numberProp;
private @Nullable JSDocInfo firstQnameDeclarationWithoutAssignmentJsDocInfo = null;
private Name(String name, Name parent, SourceKind sourceKind) {
this.baseName = name;
this.parent = parent;
this.setBooleanProperty(NameProp.NOT_A_TYPE);
this.setBooleanProperty(NameProp.OTHER_OBJECT);
if (sourceKind.equals(SourceKind.EXTERN)) {
this.setBooleanProperty(NameProp.IS_EXTERN);
}
}
Name addProperty(String name, SourceKind sourceKind) {
if (props == null) {
props = new ArrayList<>();
}
Name node = new Name(name, this, sourceKind);
props.add(node);
return node;
}
String getBaseName() {
return baseName;
}
boolean inExterns() {
return this.getBooleanProperty(NameProp.IS_EXTERN);
}
int subclassingGetCount() {
return this.subclassingGets;
}
@Override
public String getName() {
return getFullName();
}
String getFullName() {
return parent == null ? baseName : parent.getFullName() + '.' + baseName;
}
boolean usesHasOwnProperty() {
return getBooleanProperty(NameProp.IS_USED_HAS_OWN_PROPERTY);
}
@Override
public @Nullable Ref getDeclaration() {
return declaration;
}
public @Nullable Ref getInitialization() {
return initialization;
}
boolean isFunction() {
return getBooleanProperty(NameProp.FUNCTION);
}
boolean isClass() {
return getBooleanProperty(NameProp.CLASS);
}
boolean isObjectLiteral() {
return getBooleanProperty(NameProp.OBJECTLIT);
}
int getAliasingGets() {
return aliasingGets;
}
int getSubclassingGets() {
return subclassingGets;
}
int getLocalSets() {
return localSets;
}
int getGlobalSets() {
return globalSets;
}
int getTotalSets() {
return globalSets + localSets;
}
int getCallGets() {
return callGets;
}
int getTotalGets() {
return totalGets;
}
int getDeleteProps() {
return deleteProps;
}
Name getParent() {
return parent;
}
@Override
public StaticScope getScope() {
throw new UnsupportedOperationException();
}
private void setBooleanProperty(NameProp property) {
this.propertyBitSet = this.propertyBitSet | property.bit;
}
private boolean getBooleanProperty(NameProp property) {
return (this.propertyBitSet & property.bit) != 0;
}
private void addRef(Scope scope, Node node, Ref.Type type) {
checkNoExistingRefsForNode(node);
Ref ref = createNewRef(scope, node, type);
putRef(node, ref);
updateStateForAddedRef(ref);
}
private void checkNoExistingRefsForNode(Node node) {
if (this.refsForNode == null) {
return;
}
if (this.refsForNode instanceof Ref) {
checkState(
((Ref) this.refsForNode).node != node, "Ref already exists for node: %s", refsForNode);
return;
}
Ref refForNode = castRefsForNodeMap().get(node);
checkState(refForNode == null, "Ref already exists for node: %s", refForNode);
}
@SuppressWarnings("unchecked")
private Map castRefsForNodeMap() {
return (Map) refsForNode;
}
private Ref createNewRef(Scope scope, Node node, Ref.Type type) {
return new Ref(
checkNotNull(scope),
checkNotNull(node), // may be null later, but not on creation
type);
}
private void putRef(Node node, Ref ref) {
if (this.refsForNode == null) {
this.refsForNode = ref;
return;
}
if (refsForNode instanceof Ref) {
// Convert the singleton Ref object into a map, so that we can store a second Ref.
Map refsForNodeMap = new LinkedHashMap<>();
Ref existingRef = (Ref) this.refsForNode;
refsForNodeMap.put(existingRef.node, existingRef);
this.refsForNode = refsForNodeMap;
}
castRefsForNodeMap().put(node, ref);
}
Ref addSingleRefForTesting(Node node, Ref.Type type) {
Ref ref = new Ref(/* scope= */ null, /* node= */ node, type);
putRef(node, ref);
updateStateForAddedRef(ref);
return ref;
}
/**
* Add an ALIASING_GET Ref for the given Node using the same Ref properties as the declaration
* Ref, which must exist.
*
* Only for use by CollapseProperties.
*
* @param newRefNode newly added AST node that refers to this Name and appears in the same
* module and scope as the Ref that declares this Name
*/
void addAliasingGetClonedFromDeclaration(Node newRefNode) {
// TODO(bradfordcsmith): It would be good to add checks that the scope is correct.
Ref declRef = checkNotNull(declaration);
addRef(declRef.scope, newRefNode, Ref.Type.ALIASING_GET);
}
/**
* Updates counters and JSDocInfo recorded for the name to include a newly added Ref.
*
*
Must be called exactly once when a new Ref is added.
*
* @param ref a Ref that has just been added for this Name
*/
private void updateStateForAddedRef(Ref ref) {
switch (ref.type) {
case GET_AND_SET_FROM_GLOBAL:
case SET_FROM_GLOBAL:
if (declaration == null) {
declaration = ref;
}
if (initialization == null && !ref.isUninitializedDeclaration()) {
// Record the reference where the first value is actually assigned.
// Do not include the automatically-assigned `undefined` value case.
// e.g. `var name;`
initialization = ref;
}
if (firstDeclarationJSDocInfo == null) {
// JSDocInfo from the first SET_FROM_GLOBAL will be assumed to be canonical
// Note that this will not change if the first declaration is later removed
// by optimizations.
firstDeclarationJSDocInfo = getDocInfoForDeclaration(ref);
}
globalSets++;
if (ref.type.equals(Ref.Type.GET_AND_SET_FROM_GLOBAL)) {
aliasingGets++;
totalGets++;
}
break;
case GET_AND_SET_FROM_LOCAL:
case SET_FROM_LOCAL:
localSets++;
JSDocInfo info = ref.getNode() == null ? null : NodeUtil.getBestJSDocInfo(ref.getNode());
if (info != null && info.isNoCollapse()) {
localSetsWithNoCollapse++;
}
if (ref.type.equals(Ref.Type.GET_AND_SET_FROM_LOCAL)) {
aliasingGets++;
totalGets++;
}
break;
case PROTOTYPE_GET:
case DIRECT_GET:
Node node = ref.getNode();
if (firstQnameDeclarationWithoutAssignmentJsDocInfo == null
&& isQnameDeclarationWithoutAssignment(node)) {
// /** @type {sometype} */
// some.qname.ref;
firstQnameDeclarationWithoutAssignmentJsDocInfo = node.getJSDocInfo();
}
totalGets++;
break;
case ALIASING_GET:
aliasingGets++;
totalGets++;
break;
case CALL_GET:
callGets++;
totalGets++;
break;
case DELETE_PROP:
deleteProps++;
break;
case SUBCLASSING_GET:
subclassingGets++;
totalGets++;
break;
}
}
/**
* This is the only safe way to update the Node belonging to a Ref once it is added to a Name.
*
*
This is a specialized method that exists only for use by CollapseProperties.
*
* @param ref reference to update - it must belong to this name
* @param newNode new value for the ref's node
*/
void updateRefNode(Ref ref, @Nullable Node newNode) {
checkArgument(ref.node != newNode, "redundant update to Ref node: %s", ref);
// Once a Ref's node is set to null, it shouldn't ever be set to anything else.
// TODO(bradfordcsmith): Document here what it means when we set the node to null.
// Seems to be a way to keep name.getDeclaration() returning the original declaration
// Ref even though its node is no longer in the AST.
Node oldNode = ref.getNode();
checkState(oldNode != null, "Ref's node is already null: %s", ref);
ref.node = newNode;
if (refsForNode == null) {
refsForNode = ref;
} else if (refsForNode instanceof Ref) {
// No update needed, since refsForNode is a singleton.
checkState(refsForNode == ref);
} else {
Map refsForNodeMap = castRefsForNodeMap();
refsForNodeMap.remove(oldNode);
if (newNode != null) {
Ref existingRefForNewNode = refsForNodeMap.get(newNode);
checkArgument(
existingRefForNewNode == null, "refs already exist: %s", existingRefForNewNode);
refsForNodeMap.put(newNode, ref);
}
}
}
/**
* Removes the given Ref, which must belong to this Name.
*
*