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

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

There is a newer version: 9.0.8
Show newest version
/*
 * Copyright 2021 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.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.CompilerOptions.ChunkOutputType;
import com.google.javascript.jscomp.CompilerOptions.PropertyCollapseLevel;
import com.google.javascript.jscomp.GlobalNamespace.AstChange;
import com.google.javascript.jscomp.GlobalNamespace.Inlinability;
import com.google.javascript.jscomp.GlobalNamespace.Name;
import com.google.javascript.jscomp.GlobalNamespace.Ref;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.ExternsSkippingCallback;
import com.google.javascript.jscomp.Normalize.PropagateConstantAnnotationsOverVars;
import com.google.javascript.jscomp.deps.ModuleLoader.ResolutionMode;
import com.google.javascript.jscomp.diagnostic.LogFile;
import com.google.javascript.jscomp.parsing.parser.util.format.SimpleFormat;
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 java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.annotation.Nullable;

/**
 * Perform inlining of aliases and collapsing of qualified names in order to improve later
 * optimizations, such as RemoveUnusedCode.
 */
class InlineAndCollapseProperties implements CompilerPass {

  // Warnings
  static final DiagnosticType PARTIAL_NAMESPACE_WARNING =
      DiagnosticType.warning(
          "JSC_PARTIAL_NAMESPACE",
          "Partial alias created for namespace {0}, possibly due to await/yield transpilation.\n"
              + "This may prevent optimization of anything nested under this namespace.\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 RECEIVER_AFFECTED_BY_COLLAPSE =
      DiagnosticType.warning(
          "JSC_RECEIVER_AFFECTED_BY_COLLAPSE",
          "Receiver reference in function {0} changes meaning when namespace is collapsed.\n"
              + " Consider annotating @nocollapse; however, other properties on the receiver may"
              + " still be collapsed.");

  static final DiagnosticType UNSAFE_CTOR_ALIASING =
      DiagnosticType.warning(
          "JSC_UNSAFE_CTOR_ALIASING",
          "Variable {0} aliases a constructor, so it cannot be assigned multiple times");

  static final DiagnosticType ALIAS_CYCLE =
      DiagnosticType.error("JSC_ALIAS_CYCLE", "Alias path contains a cycle: {0} to {1}");

  private final AbstractCompiler compiler;
  private final PropertyCollapseLevel propertyCollapseLevel;
  private final ChunkOutputType chunkOutputType;
  private final boolean haveModulesBeenRewritten;
  private final ResolutionMode moduleResolutionMode;

  /**
   * Used by `AggressiveInlineAliasesTest` to enable execution of the aggressive inlining logic
   * without doing any collapsing.
   */
  private final boolean testAggressiveInliningOnly;

  /**
   * Supplied by `AggressiveInlineAliasesTest`.
   *
   * 

The `GlobalNamespace` created by `AggressiveInlineAliases` will be passed to this `Consumer` * for examination. */ private final Optional> optionalGlobalNamespaceTester; /** * Records decisions made by this class and related logic. * *

This field is allocated and cleaned up by process(). It's a class field to avoid having to * pass it as an extra argument through a lot of methods. */ private LogFile decisionsLog = null; /** A `GlobalNamespace` that is shared by alias inlining and property collapsing code. */ private GlobalNamespace namespace; private InlineAndCollapseProperties(Builder builder) { this.compiler = builder.compiler; this.propertyCollapseLevel = builder.propertyCollapseLevel; this.chunkOutputType = builder.chunkOutputType; this.haveModulesBeenRewritten = builder.haveModulesBeenRewritten; this.moduleResolutionMode = builder.moduleResolutionMode; this.testAggressiveInliningOnly = builder.testAggressiveInliningOnly; this.optionalGlobalNamespaceTester = builder.optionalGlobalNamespaceTester; } static final class Builder { final AbstractCompiler compiler; private PropertyCollapseLevel propertyCollapseLevel; private ChunkOutputType chunkOutputType; private boolean haveModulesBeenRewritten; private ResolutionMode moduleResolutionMode; private boolean testAggressiveInliningOnly = false; private Optional> optionalGlobalNamespaceTester = Optional.empty(); Builder(AbstractCompiler compiler) { this.compiler = compiler; } public Builder setPropertyCollapseLevel(PropertyCollapseLevel propertyCollapseLevel) { this.propertyCollapseLevel = propertyCollapseLevel; return this; } public Builder setChunkOutputType(ChunkOutputType chunkOutputType) { this.chunkOutputType = chunkOutputType; return this; } public Builder setHaveModulesBeenRewritten(boolean haveModulesBeenRewritten) { this.haveModulesBeenRewritten = haveModulesBeenRewritten; return this; } public Builder setModuleResolutionMode(ResolutionMode moduleResolutionMode) { this.moduleResolutionMode = moduleResolutionMode; return this; } @VisibleForTesting public Builder testAggressiveInliningOnly(Consumer globalNamespaceTester) { this.testAggressiveInliningOnly = true; this.optionalGlobalNamespaceTester = Optional.of(globalNamespaceTester); return this; } InlineAndCollapseProperties build() { return new InlineAndCollapseProperties(this); } } static Builder builder(AbstractCompiler compiler) { return new Builder(compiler); } @Override public void process(Node externs, Node root) { checkState( !testAggressiveInliningOnly || propertyCollapseLevel == PropertyCollapseLevel.ALL, "testAggressiveInlining is invalid for: %s", propertyCollapseLevel); try (LogFile logFile = compiler.createOrReopenIndexedLog(this.getClass(), "decisions.log")) { // NOTE: decisionsLog will be a do-nothing proxy object unless the compiler // was given an option telling it to generate log files and where to put them. decisionsLog = logFile; switch (propertyCollapseLevel) { case NONE: performMinimalInliningAndNoCollapsing(externs, root); break; case MODULE_EXPORT: performMinimalInliningAndModuleExportCollapsing(externs, root); break; case ALL: if (testAggressiveInliningOnly) { performAggressiveInliningForTest(externs, root); } else { performAggressiveInliningAndCollapsing(externs, root); } break; } } finally { decisionsLog = null; } } private void performMinimalInliningAndNoCollapsing(Node externs, Node root) { // TODO(b/124915436): Remove InlineAliases completely after cleaning up the codebase. new InlineAliases().process(externs, root); } private void performMinimalInliningAndModuleExportCollapsing(Node externs, Node root) { // TODO(b/124915436): Remove InlineAliases completely after cleaning up the codebase. new InlineAliases().process(externs, root); // CollapseProperties needs this namespace. // TODO(bradfordcsmith): Have `InlineAliases` update the namespace it already created // and reuse that one instead. namespace = new GlobalNamespace(decisionsLog, compiler, root); new CollapseProperties().process(externs, root); } private void performAggressiveInliningAndCollapsing(Node externs, Node root) { new ConcretizeStaticInheritanceForInlining(compiler).process(externs, root); new AggressiveInlineAliases().process(externs, root); new CollapseProperties().process(externs, root); } private void performAggressiveInliningForTest(Node externs, Node root) { final AggressiveInlineAliases aggressiveInlineAliases = new AggressiveInlineAliases(); aggressiveInlineAliases.process(externs, root); optionalGlobalNamespaceTester .get() .accept(aggressiveInlineAliases.getLastUsedGlobalNamespace()); } /** * Inlines type aliases if they are explicitly or effectively const. Also inlines inherited static * property accesses for ES6 classes. * *

This frees subsequent optimization passes from the responsibility of having to reason about * alias chains and is a requirement for correct behavior in at least CollapseProperties and * J2clPropertyInlinerPass. * *

This is designed to be no more unsafe than CollapseProperties. It will in some cases inline * properties, possibly past places that change the property value. However, it will only do so in * cases where CollapseProperties would unsafely collapse the property anyway. */ class AggressiveInlineAliases implements CompilerPass { private boolean codeChanged; AggressiveInlineAliases() { this.codeChanged = true; } @VisibleForTesting GlobalNamespace getLastUsedGlobalNamespace() { return namespace; } @Override public void process(Node externs, Node root) { new StaticSuperPropReplacer(compiler).replaceAll(root); NodeTraversal.traverse(compiler, root, new RewriteSimpleDestructuringAliases()); // Building the `GlobalNamespace` dominates the cost of this pass, so it is built once and // updated as changes are made so it can be reused for the next iteration. namespace = new GlobalNamespace(decisionsLog, compiler, root); while (codeChanged) { codeChanged = false; inlineAliases(namespace); } } /** * For each qualified name N in the global scope, we check if: (a) No ancestor of N is ever * aliased or assigned an unknown value type. (If N = "a.b.c", "a" and "a.b" are never aliased). * (b) N has exactly one write, and it lives in the global scope. (c) N is aliased in a local * scope. (d) N is aliased in global scope * *

If (a) is true, then GlobalNamespace must know all the writes to N. If (a) and (b) are * true, then N cannot change during the execution of a local scope. If (a) and (b) and (c) are * true, then the alias can be inlined if the alias obeys the usual rules for how we decide * whether a variable is inlineable. If (a) and (b) and (d) are true, then inline the alias if * possible (if it is assigned exactly once unconditionally). * *

For (a), (b), and (c) are true and the alias is of a constructor, we may also partially * inline the alias - i.e. replace some references with the constructor but not all - since * constructor properties are always collapsed, so we want to be more aggressive about removing * aliases. This is similar to what FlowSensitiveInlineVariables does. * *

If (a) is not true, but the property is a 'declared type' (which CollapseProperties will * unsafely collapse), we also inline any properties without @nocollapse. This is unsafe but no * more unsafe than what CollapseProperties does. This pass and CollapseProperties share the * logic to determine when a name is unsafely collapsible in {@link Name#canCollapse()} * * @see InlineVariables */ private void inlineAliases(GlobalNamespace namespace) { // Invariant: All the names in the worklist meet condition (a). // adds all top-level names to the worklist, but not any properties on those names. Deque workList = new ArrayDeque<>(namespace.getNameForest()); while (!workList.isEmpty()) { Name name = workList.pop(); // Don't attempt to inline a getter or setter property as a variable. if (name.isGetOrSetDefinition()) { continue; } if (!name.inExterns() // not an externs definition && name.getGlobalSets() == 1 // set exactly once and only set in the global scope && name.getLocalSets() == 0) { // {@code name} meets condition (b). Find all of its aliases // and try to inline them. maybeInlineInnerName(name); if (name.getAliasingGets() > 0 || name.getSubclassingGets() > 0) { // condition (c) and/or condition (d) are true inlineAliasesForName(name, namespace); } } maybeAddPropertiesToWorklist(name, workList); } } /** * Inlines a global name into all the places where references for aliases to it currently exist. * *

e.g. * *


     *   const globalName = { method() {} };
     *   const aliasForGlobalName = globalName;
     *   aliasForGlobalName.method(); // replace this with globalName.method();
     * 
* *

This method only handles aliases created by assignment. In particular, it doesn't handle * aliases created by inner names on class or function expressions. See (maybeInlineInnerName() * for that). * * @param name the global name whose aliases will be replaced * @param namespace used to find references to the global name that create aliases e.g. {@code * const aliasName = globalName}. */ private void inlineAliasesForName(Name name, GlobalNamespace namespace) { List refs = new ArrayList<>(name.getRefs()); for (Ref ref : refs) { Scope hoistScope = ref.scope.getClosestHoistScope(); if (ref.type == Ref.Type.ALIASING_GET && !mayBeGlobalAlias(ref) && ref.getTwin() == null) { // {@code name} meets condition (c). Try to inline it. // TODO(johnlenz): consider picking up new aliases at the end // of the pass instead of immediately like we do for global // inlines. inlineAliasIfPossible(name, ref, namespace); } else if (ref.type == Ref.Type.ALIASING_GET && hoistScope.isGlobal() && ref.getTwin() == null) { // ignore aliases in chained assignments inlineGlobalAliasIfPossible(name, ref, namespace); } else if (name.isClass() && ref.type == Ref.Type.SUBCLASSING_GET && name.props != null) { for (Name prop : name.props) { rewriteAllSubclassInheritedAccesses(name, ref, prop, namespace); } } } } /** * If the global name is a class or function with an inner-scope name, inline references to that * name with the global name. * *

e.g. * *


     *   var globalFunction = function innerName() {
     *     // change this to globalFunction.someProp
     *     use(innerName.someProp);
     *   }
     *   var GlobalClass = class InnerName {
     *     method() {
     *       // change this to GlobalClass.someProp
     *       use(InnerName.someProp);
     *     }
     *   };
     * 
*/ private void maybeInlineInnerName(Name globalName) { final Ref globalNameDeclaration = checkNotNull(globalName.getDeclaration(), globalName); final Node globalDeclarationNode = checkNotNull(globalNameDeclaration.getNode(), globalNameDeclaration); final Node valueNode = NodeUtil.getRValueOfLValue(globalDeclarationNode); if (valueNode == null) { // no function or class expression, so no inner name return; } final Node innerNameNode = maybeGetInnerNameNode(valueNode); if (innerNameNode == null) { // no inner name to require inlining return; } final String innerName = innerNameNode.getString(); final SyntacticScopeCreator syntacticScopeCreator = new SyntacticScopeCreator(compiler); final Scope innerScope = syntacticScopeCreator.createScope(valueNode, globalNameDeclaration.scope); final Var innerNameVar = checkNotNull(innerScope.getVar(innerName)); final ReferenceCollector collector = new ReferenceCollector( compiler, ReferenceCollector.DO_NOTHING_BEHAVIOR, syntacticScopeCreator, Predicates.equalTo(innerNameVar)); collector.processScope(innerScope); final ReferenceCollection innerNameRefs = collector.getReferences(innerNameVar); final Set newNodes = new LinkedHashSet<>(); for (Reference innerNameRef : innerNameRefs) { // replace all references to the inner name other than its declaration final Node innerNameRefNode = innerNameRef.getNode(); if (NodeUtil.isNormalGet(innerNameRefNode.getParent())) { // Replace `innerName` with `globalName` for `innerName.prop` and `innerName[expr]` // // TODO(b/148237949): We are intentionally ignoring cases where the inner name // escapes to other scopes where properties may be accessed on it (e.g. `use(InnerName)`). // This is unsafe, but currently necessary to avoid large code size regressions. // // NOTE: We also don't want to introduce a global reference for cases like // `x instanceof innerName`. It would be safe to inline these, but it also isn't // necessary, // and the introduction of a reference to a global in a local scope can cause other // optimizations to back off. newNodes.add(replaceAliasReference(globalNameDeclaration, innerNameRef)); } } namespace.scanNewNodes(newNodes); } /** * Inline all references to inherited static superclass properties from the subclass or any * descendant of the given subclass. Avoids inlining references to inherited methods when * possible, since they may use this or super(). * * @param superclassNameObj The Name of the superclass * @param superclassRef The SUBCLASSING_REF * @param prop The property on the superclass to rewrite, if any descendant accesses it. * @param namespace The GlobalNamespace containing superclassNameObj */ private boolean rewriteAllSubclassInheritedAccesses( Name superclassNameObj, Ref superclassRef, Name prop, GlobalNamespace namespace) { if (!prop.canCollapse()) { return false; // inlining is a) unnecessary if there is @nocollapse and b) might break // usages of `this` in the method } Node subclass = getSubclassForEs6Superclass(superclassRef.getNode()); if (subclass == null || !subclass.isQualifiedName()) { return false; } String subclassName = subclass.getQualifiedName(); String subclassQualifiedPropName = subclassName + "." + prop.getBaseName(); Name subclassPropNameObj = namespace.getOwnSlot(subclassQualifiedPropName); // Don't rewrite if the subclass ever shadows the parent static property. // This may also back off on cases where the subclass first accesses the parent property, then // shadows it. if (subclassPropNameObj != null && (subclassPropNameObj.getLocalSets() > 0 || subclassPropNameObj.getGlobalSets() > 0)) { return false; } // Recurse to find potential sub-subclass accesses of the superclass property. Name subclassNameObj = namespace.getOwnSlot(subclassName); if (subclassNameObj != null && subclassNameObj.subclassingGetCount() > 0) { for (Ref ref : subclassNameObj.getRefs()) { if (ref.type == Ref.Type.SUBCLASSING_GET) { rewriteAllSubclassInheritedAccesses(superclassNameObj, ref, prop, namespace); } } } if (subclassPropNameObj != null) { Set newNodes = new LinkedHashSet<>(); // Use this node as a template for rewriteNestedAliasReference. Node superclassNameNode = superclassNameObj.getDeclaration().getNode(); if (superclassNameNode.isName()) { superclassNameNode = superclassNameNode.cloneNode(); } else if (superclassNameNode.isGetProp()) { superclassNameNode = superclassNameNode.cloneTree(); } else { return false; } rewriteNestedAliasReference(superclassNameNode, 0, newNodes, subclassPropNameObj); namespace.scanNewNodes(newNodes); } return true; } /** * Attempts to inline a non-global alias of a global name. * *

It is assumed that the name for which it is an alias meets conditions (a) and (b). * *

The non-global alias is only inlinable if it is well-defined and assigned once, according * to the definitions in {@link ReferenceCollection} * *

If the aliasing name is completely removed, also deletes the aliasing Ref. * * @param name The global name being aliased * @param alias The aliasing reference to the name to remove */ private void inlineAliasIfPossible(Name name, Ref alias, GlobalNamespace namespace) { // Ensure that the alias is assigned to a local variable at that // variable's declaration. If the alias's parent is a NAME, // then the NAME must be the child of a VAR, LET, or CONST node, and we must // be in a VAR, LET, or CONST assignment. // Otherwise if the parent is an assign, we are in a "a = alias" case. Node aliasParent = alias.getNode().getParent(); if (aliasParent.isName() || aliasParent.isAssign()) { Node aliasLhsNode = aliasParent.isName() ? aliasParent : aliasParent.getFirstChild(); String aliasVarName = aliasLhsNode.getString(); Var aliasVar = alias.scope.getVar(aliasVarName); checkState(aliasVar != null, "Expected variable to be defined in scope", aliasVarName); ReferenceCollector collector = new ReferenceCollector( compiler, ReferenceCollector.DO_NOTHING_BEHAVIOR, new SyntacticScopeCreator(compiler), Predicates.equalTo(aliasVar)); Scope aliasScope = aliasVar.getScope(); collector.processScope(aliasScope); ReferenceCollection aliasRefs = collector.getReferences(aliasVar); Set newNodes = new LinkedHashSet<>(); if (aliasRefs.isWellDefined() && aliasRefs.isAssignedOnceInLifetime()) { // The alias is well-formed, so do the inlining now. int size = aliasRefs.references.size(); // It's initialized on either the first or second reference. int firstRead = aliasRefs.references.get(0).isInitializingDeclaration() ? 1 : 2; for (int i = firstRead; i < size; i++) { Reference aliasRef = aliasRefs.references.get(i); newNodes.add(replaceAliasReference(alias, aliasRef)); } // just set the original alias to null. tryReplacingAliasingAssignment(alias, aliasLhsNode); // Inlining the variable may have introduced new references // to descendants of {@code name}. So those need to be collected now. namespace.scanNewNodes(newNodes); return; } if (name.isConstructor()) { // TODO(lharker): the main reason this was added is because method decomposition inside // generators introduces some constructor aliases that weren't getting inlined. // If we find another (safer) way to avoid aliasing in method decomposition, consider // removing this. if (!partiallyInlineAlias(alias, namespace, aliasRefs, aliasLhsNode)) { // If we can't inline all alias references, make sure there are no unsafe property // accesses. if (referencesCollapsibleProperty(aliasRefs, name, namespace)) { compiler.report(JSError.make(aliasParent, UNSAFE_CTOR_ALIASING, aliasVarName)); } } } } } /** * Inlines some references to an alias with its value. This handles cases where the alias is not * declared at initialization. It does nothing if the alias is reassigned after being * initialized, unless the reassignment occurs because of an enclosing function or a loop. * * @param alias An alias of some variable, which may not be well-defined. * @param namespace The GlobalNamespace, which will be updated with all new nodes created. * @param aliasRefs All references to the alias in its scope. * @param aliasLhsNode The lhs name of the alias when it is first initialized. * @return Whether all references to the alias were inlined */ private boolean partiallyInlineAlias( Ref alias, GlobalNamespace namespace, ReferenceCollection aliasRefs, Node aliasLhsNode) { BasicBlock aliasBlock = null; // This initial iteration through all the alias references does two things: // a) Find the control flow block in which the alias is assigned. // b) See if the alias var is assigned to in multiple places, and return if that's the case. // NOTE: we still may inline if the alias is assigned in a loop or inner function and that // assignment statement is potentially executed multiple times. // This is more aggressive than what "inlineAliasIfPossible" does. for (Reference aliasRef : aliasRefs) { Node aliasRefNode = aliasRef.getNode(); if (aliasRefNode == aliasLhsNode) { aliasBlock = aliasRef.getBasicBlock(); continue; } else if (aliasRef.isLvalue()) { // Don't replace any references if the alias is reassigned return false; } } Set newNodes = new LinkedHashSet<>(); boolean alreadySeenInitialAlias = false; boolean foundNonReplaceableAlias = false; // Do a second iteration through all the alias references, and replace any inlinable // references. for (Reference aliasRef : aliasRefs) { Node aliasRefNode = aliasRef.getNode(); if (aliasRefNode == aliasLhsNode) { alreadySeenInitialAlias = true; continue; } else if (aliasRef.isDeclaration()) { // Ignore any alias declarations, e.g. "var alias;", since there's nothing to inline. continue; } BasicBlock refBlock = aliasRef.getBasicBlock(); if ((refBlock != aliasBlock && aliasBlock.provablyExecutesBefore(refBlock)) || (refBlock == aliasBlock && alreadySeenInitialAlias)) { // We replace the alias only if the alias and reference are in the same BasicBlock, // the aliasing assignment takes place before the reference, and the alias is // never reassigned. codeChanged = true; newNodes.add(replaceAliasReference(alias, aliasRef)); } else { foundNonReplaceableAlias = true; } } // We removed all references to the alias, so remove the original aliasing assignment. if (!foundNonReplaceableAlias) { tryReplacingAliasingAssignment(alias, aliasLhsNode); } if (codeChanged) { // Inlining the variable may have introduced new references // to descendants of {@code name}. So those need to be collected now. namespace.scanNewNodes(newNodes); } return !foundNonReplaceableAlias; } /** * Replaces the rhs of an aliasing assignment with null, unless the assignment result is used in * a complex expression. */ private boolean tryReplacingAliasingAssignment(Ref alias, Node aliasLhsNode) { // either VAR/CONST/LET or ASSIGN. Node assignment = aliasLhsNode.getParent(); if (!NodeUtil.isNameDeclaration(assignment) && NodeUtil.isExpressionResultUsed(assignment)) { // e.g. don't change "if (alias = someVariable)" to "if (alias = null)" // TODO(lharker): instead replace the entire assignment with the RHS - "alias = x" becomes // "x" return false; } Node aliasParent = alias.getNode().getParent(); alias.getNode().replaceWith(IR.nullNode()); alias.name.removeRef(alias); codeChanged = true; compiler.reportChangeToEnclosingScope(aliasParent); return true; } /** * @param alias A GlobalNamespace.Ref of the variable being aliased * @param aliasRef One particular usage of an alias that we want to replace with the aliased * var. * @return an AstChange representing the new node(s) added to the AST * */ private AstChange replaceAliasReference(Ref alias, Reference aliasRef) { final Node originalRefNode = alias.getNode(); final Node nodeToReplace = aliasRef.getNode(); checkState(nodeToReplace.isQualifiedName(), nodeToReplace); // If the reference node is a NAME it could be // const origName = value; // If we use cloneTree() for that we'll clone the value, which we don't want. // Otherwise, we do want to clone the tree of GETPROP nodes. final Node newNode = originalRefNode.isName() ? originalRefNode.cloneNode() : originalRefNode.cloneTree(); newNode.srcrefTree(nodeToReplace); nodeToReplace.replaceWith(newNode); compiler.reportChangeToEnclosingScope(newNode); return new AstChange(aliasRef.getScope(), newNode); } /** * Attempt to inline an global alias of a global name. This requires that the name is well * defined: assigned unconditionally, assigned exactly once. It is assumed that, the name for * which it is an alias must already meet these same requirements. * *

If the alias is completely removed, also deletes the aliasing Ref. * * @param name The global name being aliased * @param alias The alias to inline */ private void inlineGlobalAliasIfPossible(Name name, Ref alias, GlobalNamespace namespace) { // Ensure that the alias is assigned to global name at that the // declaration. Node aliasParent = alias.getNode().getParent(); if (((aliasParent.isAssign() || aliasParent.isName()) && NodeUtil.isExecutedExactlyOnce(aliasParent)) // We special-case for constructors here, to inline constructor aliases // more aggressively in global scope. // We do this because constructor properties are always collapsed, // so we want to inline the aliases also to avoid breakages. || (aliasParent.isName() && name.isConstructor())) { Node lvalue = aliasParent.isName() ? aliasParent : aliasParent.getFirstChild(); if (!lvalue.isQualifiedName()) { return; } if (lvalue.isName() && compiler.getCodingConvention().isExported(lvalue.getString(), /* local */ false)) { return; } Name aliasingName = namespace.getSlot(lvalue.getQualifiedName()); if (aliasingName == null) { // this is true for names in externs or properties on extern names return; } if (name.equals(aliasingName) && aliasParent.isAssign()) { // Ignore `a.b.c = a.b.c;` with `a.b.c;`. return; } Inlinability aliasInlinability = aliasingName.calculateInlinability(); if (!aliasInlinability.shouldInlineUsages()) { // nothing to do here return; } Set newNodes = new LinkedHashSet<>(); // Rewrite all references to the aliasing name, except for the initialization rewriteAliasReferences(aliasingName, alias, newNodes); rewriteAliasProps(aliasingName, alias.getNode(), 0, newNodes); if (aliasInlinability.shouldRemoveDeclaration()) { // Rewrite the initialization of the alias, unless this is an unsafe alias inline // caused by an @constructor. In that case, we need to leave the initialization around. Ref aliasDeclaration = aliasingName.getDeclaration(); if (aliasDeclaration.getTwin() != null) { // This is in a nested assign. // Replace // a.b = aliasing.name = aliased.name // with // a.b = aliased.name checkState(aliasParent.isAssign(), aliasParent); Node aliasGrandparent = aliasParent.getParent(); aliasParent.replaceWith(alias.getNode().detach()); // remove both of the refs aliasingName.removeTwinRefs(aliasDeclaration); newNodes.add(new AstChange(alias.scope, alias.getNode())); compiler.reportChangeToEnclosingScope(aliasGrandparent); } else { // just set the original alias to null. alias.getNode().replaceWith(IR.nullNode()); compiler.reportChangeToEnclosingScope(aliasParent); } codeChanged = true; // Update the original aliased name to say that it has one less ALIASING_REF. name.removeRef(alias); } // Inlining the variable may have introduced new references // to descendants of {@code name}. So those need to be collected now. namespace.scanNewNodes(newNodes); } } /** Replaces all reads of a name with the name it aliases */ private void rewriteAliasReferences( Name aliasingName, Ref aliasingRef, Set newNodes) { List refs = new ArrayList<>(aliasingName.getRefs()); for (Ref ref : refs) { switch (ref.type) { case SET_FROM_GLOBAL: continue; case DIRECT_GET: case ALIASING_GET: case PROTOTYPE_GET: case CALL_GET: case SUBCLASSING_GET: if (ref.getTwin() != null) { // The reference is the left-hand side of a nested assignment. This means we store two // separate 'twin' Refs with the same node of types ALIASING_GET and SET_FROM_GLOBAL. // For example, the read of `c.d` has a twin reference in // a.b = c.d = e.f; // We handle this case later. checkState(ref.type == Ref.Type.ALIASING_GET, ref); break; } if (ref.getNode().isStringKey()) { // e.g. `y` in `const {y} = x;` DestructuringGlobalNameExtractor.reassignDestructringLvalue( ref.getNode(), aliasingRef.getNode().cloneTree(), newNodes, ref, compiler); } else { // e.g. `x.y` checkState(ref.getNode().isGetProp() || ref.getNode().isName()); Node newNode = aliasingRef.getNode().cloneTree(); Node node = ref.getNode(); node.replaceWith(newNode); compiler.reportChangeToEnclosingScope(newNode); newNodes.add(new AstChange(ref.scope, newNode)); } aliasingName.removeRef(ref); break; default: throw new IllegalStateException(); } } } /** * @param name The Name whose properties references should be updated. * @param value The value to use when rewriting. * @param depth The chain depth. * @param newNodes Expression nodes that have been updated. */ private void rewriteAliasProps(Name name, Node value, int depth, Set newNodes) { if (name.props == null) { return; } Preconditions.checkState( !value.matchesQualifiedName(name.getFullName()), "%s should not match name %s", value, name.getFullName()); for (Name prop : name.props) { rewriteNestedAliasReference(value, depth, newNodes, prop); } } /** * Replaces references to an alias that are nested inside a longer getprop chain or an object * literal * *

For example: if we have an inlined alias 'const A = B;', and reference a property 'A.x', * then this method is responsible for replacing 'A.x' with 'B.x'. * *

This is necessary because in the above example, given 'A.x', there is only one {@link Ref} * that points to the whole name 'A.x', not a direct {@link Ref} to 'A'. So the only way to * replace 'A.x' with 'B.x' is by looking at the property 'x' reference. * * @param value The value to use when rewriting. * @param depth The property chain depth. * @param newNodes Expression nodes that have been updated. * @param prop The property to rewrite with value. */ private void rewriteNestedAliasReference( Node value, int depth, Set newNodes, Name prop) { rewriteAliasProps(prop, value, depth + 1, newNodes); List refs = new ArrayList<>(prop.getRefs()); for (Ref ref : refs) { Node target = ref.getNode(); if (target.isStringKey() && target.getParent().isDestructuringPattern()) { // Do nothing for alias properties accessed through object destructuring. This would be // redundant. This method is intended for names nested inside getprop chains, because // GlobalNamespace only creates a single Ref for the outermost getprop. However, for // destructuring property accesses, GlobalNamespace creates multiple Refs, one for the // destructured object, and one for each string key in the pattern. // // For example, consider: // const originalObj = {key: 0}; // const rhs = originalObj; // const {key: lhs} = rhs; // const otherLhs = rhs.key; // AggressiveInlineAliases is inlining rhs -> originalObj. // // GlobalNamespace creates two Refs for the name 'rhs': one for its declaration, // and one for 'const {key: lhs} = rhs;'. There is no Ref pointing directly to the 'rhs' // in 'const otherLhs = rhs.key', though. // There are also two Refs to the name 'rhs.key': one for the destructuring access and one // for the getprop access. This loop will visit both Refs. // This method is responsible for inlining "const otherLhs = originalObj.key" but not // "const {key: lhs} = originalObj;". We bail out at the Ref in the latter case. checkState( target.getGrandparent().isAssign() || target.getGrandparent().isDestructuringLhs(), // Currently GlobalNamespace doesn't create Refs for 'b' in const {a: {b}} = obj; // If it does start creating those Refs, we may have to update this method to handle // them explicitly. "Did not expect GlobalNamespace to create Ref for key in nested object pattern %s", target); continue; } for (int i = 0; i <= depth; i++) { if (target.isGetProp()) { target = target.getFirstChild(); } else if (NodeUtil.isObjectLitKey(target)) { // Object literal key definitions are a little trickier, as we // need to find the assignment target Node gparent = target.getGrandparent(); if (gparent.isAssign()) { target = gparent.getFirstChild(); } else { checkState(NodeUtil.isObjectLitKey(gparent)); target = gparent; } } else { throw new IllegalStateException("unexpected node: " + target); } } checkState(target.isGetProp() || target.isName()); Node newValue = value.cloneTree(); target.replaceWith(newValue); compiler.reportChangeToEnclosingScope(newValue); prop.removeRef(ref); // Rescan the expression root. newNodes.add(new AstChange(ref.scope, ref.getNode())); codeChanged = true; } } } /** * Rewrite "simple" destructuring aliases to a format that is more amenable to inlining. * *

To be specific, this rewrites aliases of the form: const {x} = qualified.name; to: const x = * qualified.name.x; */ private static class RewriteSimpleDestructuringAliases extends AbstractPostOrderCallback { public boolean isSimpleDestructuringAlias(Node n) { if (!NodeUtil.isStatement(n) || !n.isConst()) { return false; } checkState(n.hasOneChild()); Node destructuringLhs = n.getFirstChild(); if (!destructuringLhs.isDestructuringLhs()) { return false; } Node objectPattern = destructuringLhs.getFirstChild(); if (!objectPattern.isObjectPattern()) { return false; } Node rhs = destructuringLhs.getLastChild(); if (!rhs.isQualifiedName()) { return false; } for (Node key = objectPattern.getFirstChild(); key != null; key = key.getNext()) { if (!key.isStringKey() || key.isQuotedString()) { return false; } checkState(key.hasOneChild()); Node identifier = key.getFirstChild(); if (!identifier.isName()) { return false; } } return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (!isSimpleDestructuringAlias(n)) { return; } Node insertionPoint = n; Node destructuringLhs = n.getFirstChild(); Node objectPattern = destructuringLhs.getFirstChild(); Node rhs = destructuringLhs.getLastChild(); for (Node key = objectPattern.getFirstChild(); key != null; key = key.getNext()) { Node identifier = key.getFirstChild(); Node newRhs = IR.getprop(rhs.cloneTree(), key.getString()).srcref(identifier); Node newConstNode = IR.constNode(identifier.detach(), newRhs).srcref(n); newConstNode.insertAfter(insertionPoint); insertionPoint = newConstNode; } n.detach(); t.reportCodeChange(); } } @Nullable private static Node maybeGetInnerNameNode(Node maybeFunctionOrClassNode) { if (NodeUtil.isFunctionExpression(maybeFunctionOrClassNode)) { Node nameNode = maybeFunctionOrClassNode.getFirstChild(); checkState(nameNode.isName(), nameNode); // functions with no name have a NAME node with an empty string return nameNode.getString().isEmpty() ? null : nameNode; } else if (NodeUtil.isClassExpression(maybeFunctionOrClassNode)) { Node nameNode = maybeFunctionOrClassNode.getFirstChild(); // classes with no name have an EMPTY node first child return nameNode.isName() ? nameNode : null; } else { return null; // not a function or class expression } } /** * Adds properties of `name` to the worklist if the following conditions hold: * *

    *
  1. 1. The given property of `name` either meets condition (a) or is unsafely collapsible (as * defined by {@link Name#canCollapse()} *
  2. 2. `name` meets condition (b) *
* * This only adds direct properties of a name, not all its descendants. For example, this adds * `a.b` given `a`, but not `a.b.c`. */ private static void maybeAddPropertiesToWorklist(Name name, Deque workList) { if (!(name.isObjectLiteral() || name.isFunction() || name.isClass())) { // Don't add properties for things like `Foo` in // const Foo = someMysteriousFunctionCall(); // Since `Foo` is not declared as an object, class, or function literal, assume its value // may be aliased somewhere and its properties do not meet condition (a). return; } if (isUnsafelyReassigned(name)) { // Don't add properties if this was assigned multiple times, except for 'safe' // reassignments: // var ns = ns || {}; // This is equivalent to condition (b) return; } if (name.props == null) { return; } if (name.getAliasingGets() == 0) { // All of {@code name}'s children meet condition (a), so they can be // added to the worklist. workList.addAll(name.props); } else { // The children do NOT meet condition (a) but we may try to add them anyway. // This is because CollapseProperties will unsafely collapse properties on constructors and // enums, so we want to be more aggressive about inlining references to their children. for (Name property : name.props) { // Only add properties that would be unsafely collapsed by CollapseProperties if (property.canCollapse()) { workList.add(property); } } } } /** * Returns true if the alias is possibly defined in the global scope, which we handle with more * caution than with locally scoped variables. May return false positives. * * @param alias An aliasing get. * @return If the alias is possibly defined in the global scope. */ private static boolean mayBeGlobalAlias(Ref alias) { // Note: alias.scope is the closest scope in which the aliasing assignment occurred. // So for "if (true) { var alias = aliasedVar; }", the alias.scope would be the IF block // scope. if (alias.scope.isGlobal()) { return true; } // If the scope in which the alias is assigned is not global, look up the LHS of the // assignment. Node aliasParent = alias.getNode().getParent(); if (!aliasParent.isAssign() && !aliasParent.isName()) { // Only handle variable assignments and initializing declarations. return true; } Node aliasLhsNode = aliasParent.isName() ? aliasParent : aliasParent.getFirstChild(); if (!aliasLhsNode.isName()) { // Only handle assignments to simple names, not qualified names or GETPROPs. return true; } String aliasVarName = aliasLhsNode.getString(); Var aliasVar = alias.scope.getVar(aliasVarName); if (aliasVar != null) { return aliasVar.isGlobal(); } return true; } /** * Returns whether a ReferenceCollection for some aliasing variable references a property on the * original aliased variable that may be collapsed in CollapseProperties. * *

See {@link Name#canCollapse} for what can/cannot be collapsed. */ private static boolean referencesCollapsibleProperty( ReferenceCollection aliasRefs, Name aliasedName, GlobalNamespace namespace) { for (Reference ref : aliasRefs.references) { if (ref.getParent() == null) { continue; } if (NodeUtil.isNormalOrOptChainGetProp(ref.getParent())) { // e.g. if the reference is "alias.b.someProp", this will be "b". String propertyName = ref.getParent().getString(); // e.g. if the aliased name is "originalName", this will be "originalName.b". String originalPropertyName = aliasedName.getName() + "." + propertyName; Name originalProperty = namespace.getOwnSlot(originalPropertyName); // If the original property isn't in the namespace or can't be collapsed, keep going. if (originalProperty == null || !originalProperty.canCollapse()) { continue; } return true; } } return false; } /** Check if the name has multiple sets that are not of the form "a = a || {}" */ private static boolean isUnsafelyReassigned(Name name) { boolean foundOriginalDefinition = false; for (Ref ref : name.getRefs()) { if (!ref.isSet()) { continue; } if (isSafeNamespaceReinit(ref)) { continue; } if (!foundOriginalDefinition) { foundOriginalDefinition = true; } else { return true; } } return false; } /** * Tries to find an lvalue for the subclass given the superclass node in an `class ... extends ` * clause * *

Only handles cases where we have either a class declaration or a class expression in an * assignment or name declaration. Otherwise returns null. */ @Nullable private static Node getSubclassForEs6Superclass(Node superclass) { Node classNode = superclass.getParent(); checkArgument(classNode.isClass(), classNode); if (NodeUtil.isNameDeclaration(classNode.getGrandparent())) { // const Clazz = class extends Super { return classNode.getParent(); } else if (superclass.getGrandparent().isAssign()) { // ns.foo.Clazz = class extends Super { return classNode.getPrevious(); } else if (NodeUtil.isClassDeclaration(classNode)) { // class Clazz extends Super { return classNode.getFirstChild(); } return null; } /** * 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 { /** Maps names (e.g. "a.b.c") to nodes in the global namespace tree */ private Map nameMap; private final HashSet dynamicallyImportedModules = new HashSet<>(); @Override public void process(Node externs, Node root) { if (propertyCollapseLevel == PropertyCollapseLevel.MODULE_EXPORT || chunkOutputType == ChunkOutputType.ES_MODULES) { NodeTraversal.traverse( compiler, root, new FindDynamicallyImportedModules(haveModulesBeenRewritten, moduleResolutionMode)); } nameMap = checkNotNull(namespace, "namespace was not initialized").getNameIndex(); List globalNames = namespace.getNameForest(); Set escaped = checkNamespaces(); for (Name name : globalNames) { flattenReferencesToCollapsibleDescendantNames(name, name.getBaseName(), escaped); // 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. collapseDeclarationOfNameAndDescendants(name, name.getBaseName(), escaped); } // 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) { final Inlinability inlinability = name.canCollapseOrInline(); if (!inlinability.canCollapse()) { logDecisionForName(name, inlinability, "canCollapse() returns false"); return false; } if (propertyCollapseLevel == PropertyCollapseLevel.MODULE_EXPORT) { if (!name.isModuleExport()) { logDecisionForName(name, inlinability, "module export: canCollapse() returns false"); return false; } else if (dynamicallyImportedModules.contains(name.getBaseName())) { logDecisionForName( name, inlinability, "dynamic module export: canCollapse() returns false"); return false; } } logDecisionForName(name, inlinability, "canCollapse() returns true"); 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 Set checkNamespaces() { ImmutableSet.Builder escaped = ImmutableSet.builder(); HashSet dynamicallyImportedModuleRefs = new HashSet<>(dynamicallyImportedModules); if (!dynamicallyImportedModules.isEmpty()) { // When the output chunk type is ES_MODULES, properties of the module namespace // must not be collapsed as they are referenced off the namespace object. The namespace // objects escape via the dynamic import expression. for (Name name : nameMap.values()) { // Test if the name is a rewritten module namespace variable and if so mark it as escaped // to prevent any property collapsing. // // example: // /** @const */ var module$foo = {}; if (dynamicallyImportedModules.contains(name.getFullName())) { logDecisionForName(name, "escapes - dynamically imported module namespace"); escaped.add(name); if (name.props == null) { continue; } for (Name prop : name.props) { Ref propDeclaration = prop.getDeclaration(); if (propDeclaration == null) { continue; } if (propDeclaration.getNode() != null) { // ES Module rewriting creates aliases on the module namespace object. These aliased // names also escape and their properties may not be collapsed. // // example: // class Foo$$module$foo { // static bar() { return 'bar'; } // } // /** @const */ var module$foo = {}; // /** @const */ module$foo.Foo = Foo$$module$foo; // // While module$foo.Foo cannot be collapsed because we marked the module namespace // as escaped, we also need to prevent any property collapsing on the // Foo$$module$foo // class itself. Node rValue = NodeUtil.getRValueOfLValue(propDeclaration.getNode()); if (rValue.isName()) { logDecisionForName( name, "escapes - dynamically imported module namespace property alias"); dynamicallyImportedModuleRefs.add(rValue.getQualifiedName()); } } } } } } for (Name name : nameMap.values()) { if (dynamicallyImportedModuleRefs.contains(name.getFullName())) { escaped.add(name); } if (!name.isNamespaceObjectLit()) { continue; } if (name.getAliasingGets() == 0 && name.getLocalSets() + name.getGlobalSets() <= 1 && name.getDeleteProps() == 0) { continue; } 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); logDecisionForName(name, "escapes"); escaped.add(name); break; } } } return escaped.build(); } /** * 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(), PARTIAL_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, Set escaped) { if (n.props == null) { return; } if (n.isCollapsingExplicitlyDenied()) { logDecisionForName(n, "@nocollapse: will not flatten descendant name references"); return; } if (escaped.contains(n)) { logDecisionForName(n, "escapes: will not flatten descendant name references"); return; } for (Name p : n.props) { String propAlias = appendPropForAlias(alias, p.getBaseName()); final Inlinability inlinability = p.canCollapseOrInline(); boolean isAllowedToCollapse = propertyCollapseLevel != PropertyCollapseLevel.MODULE_EXPORT || p.isModuleExport(); if (isAllowedToCollapse) { if (inlinability.canCollapse()) { logDecisionForName(p, inlinability, "will flatten references"); flattenReferencesTo(p, propAlias); } else if (p.isCollapsingExplicitlyDenied()) { logDecisionForName(p, "@nocollapse: will not flatten references"); } else if (p.isSimpleStubDeclaration()) { logDecisionForName(p, "simple stub declaration: will flatten references"); flattenSimpleStubDeclaration(p, propAlias); } else { logDecisionForName(p, inlinability, "will not flatten references"); } } flattenReferencesToCollapsibleDescendantNames(p, propAlias, escaped); } } private void logDecisionForName(Name name, Inlinability inlinability, String message) { logDecisionForName( name, () -> SimpleFormat.format("inlinability %s: %s", inlinability, message)); } private void logDecisionForName(Name name, String message) { decisionsLog.log(() -> SimpleFormat.format("%s: %s", name.getFullName(), message)); } private void logDecisionForName(Name name, Supplier messageSupplier) { decisionsLog.log( () -> SimpleFormat.format("%s: %s", name.getFullName(), messageSupplier.get())); } /** 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).srcrefIfMissing(nameNode); checkState(ref.getNode().getParent().isExprResult()); Node parent = ref.getNode().getParent(); parent.replaceWith(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).copyTypeFrom(n); NodeUtil.copyNameAnnotations(n, ref); if (NodeUtil.isNormalOrOptChainCall(parent) && n.isFirstChildOf(parent)) { // The node was a call target. We are deliberately flattening these as // the "this" isn't provided by the namespace. Mark it as such: parent.putBooleanProp(Node.FREE_CALL, true); } n.replaceWith(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, Set escaped) { final Inlinability childNameInlinability = n.canCollapseOrInlineChildNames(); final boolean canCollapseChildNames; if (!childNameInlinability.canCollapse()) { logDecisionForName( n, () -> SimpleFormat.format( "child name inlinability: %s: will not collapse child names", childNameInlinability)); canCollapseChildNames = false; } else if (escaped.contains(n)) { logDecisionForName(n, "escapes: will not collapse child names"); canCollapseChildNames = false; } else { canCollapseChildNames = true; } // Handle this name first so that nested object literals get unrolled. if (canCollapse(n)) { logDecisionForName(n, "collapsing"); updateGlobalNameDeclaration(n, alias, canCollapseChildNames); } if (n.props == null || escaped.contains(n)) { return; } logDecisionForName(n, "collapsing descendants"); for (Name p : n.props) { collapseDeclarationOfNameAndDescendants( p, appendPropForAlias(alias, p.getBaseName()), escaped); } } /** * 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()) { checkForReceiverAffectedByCollapse(rvalue, refName.getJSDocInfo(), refName); } // Create the new alias node. Node nameNode = NodeUtil.newName(compiler, alias, grandparent.getFirstChild(), refName.getFullName()); NodeUtil.copyNameAnnotations(ref.getNode(), 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()).srcrefIfMissing(nameNode); stubVar.insertBefore(current); ref.getNode().replaceWith(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. logDecisionForName(n, "no global declaration found"); return; } final Node declNode = decl.getNode(); switch (declNode.getParent().getToken()) { case ASSIGN: logDeclarationAction(n, declNode, "updating assignment"); updateGlobalNameDeclarationAtAssignNode(n, alias, canCollapseChildNames); break; case VAR: case LET: case CONST: logDeclarationAction(n, declNode, "updating variable declaration"); updateGlobalNameDeclarationAtVariableNode(n, canCollapseChildNames); break; case FUNCTION: logDeclarationAction(n, declNode, "updating function declaration"); updateGlobalNameDeclarationAtFunctionNode(n, canCollapseChildNames); break; case CLASS: logDeclarationAction(n, declNode, "updating class declaration"); updateGlobalNameDeclarationAtClassNode(n, canCollapseChildNames); break; case CLASS_MEMBERS: logDeclarationAction(n, declNode, "updating static member declaration"); updateGlobalNameDeclarationAtStaticMemberNode(n, alias, canCollapseChildNames); break; default: logDeclarationAction(n, declNode, "not updating an unsupported type of declaration node"); break; } } private void logDeclarationAction(Name name, Node declarationNode, String message) { logDecisionForName(name, () -> SimpleFormat.format("%s: %s", declarationNode, message)); } /** * 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. grandparent.replaceWith(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()) { checkForReceiverAffectedByCollapse(rvalue, n.getJSDocInfo(), n); } compiler.reportChangeToEnclosingScope(rvalue); rvalue.detach(); Node nameNode = NodeUtil.newName(compiler, alias, ref.getNode().getAncestor(2), n.getFullName()); Node constPropNode = ref.getNode(); JSDocInfo info = NodeUtil.getBestJSDocInfo(ref.getNode().getParent()); nameNode.putBooleanProp( Node.IS_CONSTANT_NAME, (info != null && info.hasConstAnnotation()) || constPropNode.getBooleanProp(Node.IS_CONSTANT_NAME)); if (info != null) { varNode.setJSDocInfo(info); } varNode.addChildToBack(nameNode); nameNode.addChildToFront(rvalue); grandparent.replaceWith(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()); } addStubsForUndeclaredProperties(n, alias, varParent, varNode); } if (insertedVarNode) { if (!varNode.hasChildren()) { varNode.detach(); } } } /** * Warns about any references to "this" in the given FUNCTION. The function is getting * collapsed, so the references will change. */ private void checkForReceiverAffectedByCollapse(Node function, JSDocInfo docInfo, Name name) { checkState(function.isFunction()); if (docInfo != null) { // Don't rely on type inference for this check. if (docInfo.isConstructorOrInterface()) { return; // Ctors and interfaces need to be able to reference `this` } if (docInfo.hasThisType()) { /** * Use `@this` as a signal that the reference is intentional. * *

TODO(b/156823102): This signal also silences the check on all transpiled static * methods. */ return; } } // Use the NodeUtil method so we don't forget to update this logic. if (NodeUtil.referencesOwnReceiver(function)) { compiler.report(JSError.make(function, RECEIVER_AFFECTED_BY_COLLAPSE, 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) { logDecisionForName(n, "cannot collapse child names: skipping"); 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()); } addStubsForUndeclaredProperties(n, name, grandparent, variableNode); if (isObjLit && canEliminate(n)) { ref.getNode().detach(); compiler.reportChangeToEnclosingScope(variableNode); if (!variableNode.hasChildren()) { variableNode.detach(); } // 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(); checkForReceiverAffectedByCollapse(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); varDecl.insertBefore(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) */ private void declareVariablesForObjLitValues( Name objlitName, String alias, Node objlit, Node varNode, Node nameToAddAfter) { 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 OBJECT_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) { key.detach(); 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); } value.replaceWith(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).srcrefTreeIfMissing(key); if (nameToAddAfter != null) { newVar.insertAfter(nameToAddAfter); } else { newVar.insertBefore(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()) { checkForReceiverAffectedByCollapse(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()) { continue; } String propAlias = appendPropForAlias(alias, p.getBaseName()); Node nameNode = IR.name(propAlias); Node newVar = IR.var(nameNode).srcrefTreeIfMissing(addAfter); newVar.insertAfter(addAfter); // Determine if this is a constant var by checking the first // reference to it. Don't check the declaration, as it might be null. Node constPropNode = p.getFirstRef().getNode(); nameNode.putBooleanProp( Node.IS_CONSTANT_NAME, constPropNode.getBooleanProp(Node.IS_CONSTANT_NAME)); compiler.reportChangeToEnclosingScope(newVar); addAfter = newVar; } } 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; } /** Find all the module namespace objects which are referenced by a dynamic import */ class FindDynamicallyImportedModules extends AbstractPostOrderCallback { private final boolean processCommonJSModules; private final ResolutionMode moduleResolutionMode; FindDynamicallyImportedModules( boolean processCommonJSModules, ResolutionMode resolutionMode) { this.processCommonJSModules = processCommonJSModules; this.moduleResolutionMode = resolutionMode; } @Override public void visit(NodeTraversal t, Node n, Node parent) { // After rewriting, CommonJS dynamic imports are of the form // __webpack_require__.e(2).then(function() { return module$mod1.default; }) // // Mark the base module name as being dynamically imported. if (processCommonJSModules && n.isGetProp() && n.isQualifiedName() && n.getParent() != null && n.getParent().isReturn() && n.getGrandparent().isBlock() && n.getGrandparent().hasOneChild() && n.getGrandparent().getParent().isFunction()) { Node potentialCallback = NodeUtil.getEnclosingFunction(n); if (potentialCallback != null && ProcessCommonJSModules.isCommonJsDynamicImportCallback( NodeUtil.getEnclosingFunction(potentialCallback), moduleResolutionMode)) { dynamicallyImportedModules.add(NodeUtil.getRootOfQualifiedName(n.getQualifiedName())); } } else if (ConvertChunksToESModules.isDynamicImportCallback(n)) { Node moduleNamespace = ConvertChunksToESModules.getDynamicImportCallbackModuleNamespace(compiler, n); if (moduleNamespace != null) { dynamicallyImportedModules.add(moduleNamespace.getQualifiedName()); } } } } } 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(); } /** * Inline constant aliases * *

This pass was originally necessary because typechecking did not handle type aliases well. * Now typechecking understands type aliases. In theory, this pass can be deleted, but in practice * this pass affects some check passes that run post-typechecking. * *

This alias inliner is not very aggressive. It will only inline explicitly const aliases but * not effectively const ones (for example ones that are only ever assigned a value once). This is * done to be conservative since it's not a good idea to be making dramatic AST changes during * checks (or really, any AST changes at all). There is a more aggressive alias inliner that runs * at the start of optimization. * *

TODO(b/124915436): Delete this pass. */ final class InlineAliases implements CompilerPass { private final Map aliases = new LinkedHashMap<>(); private GlobalNamespace namespace; private final AstFactory astFactory; InlineAliases() { this.astFactory = compiler.createAstFactory(); } @Override public void process(Node externs, Node root) { namespace = new GlobalNamespace(compiler, externs, root); NodeTraversal.traverseRoots(compiler, new AliasesCollector(), externs, root); NodeTraversal.traverseRoots(compiler, new AliasesInliner(), externs, root); } private class AliasesCollector extends ExternsSkippingCallback { @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case VAR: case CONST: case LET: if (n.hasOneChild() && t.inGlobalScope()) { visitAliasDefinition(n.getFirstChild(), NodeUtil.getBestJSDocInfo(n.getFirstChild())); } break; case ASSIGN: if (parent != null && parent.isExprResult() && t.inGlobalScope()) { visitAliasDefinition(n.getFirstChild(), n.getJSDocInfo()); } break; default: break; } } /** * Maybe record that given lvalue is an alias of the qualified name on its rhs. Note that * since we are doing a post-order traversal, any previous aliases contained in the rhs will * have already been substituted by the time we record the new alias. */ private void visitAliasDefinition(Node lhs, JSDocInfo info) { if (isDeclaredConst(lhs, info) && (info == null || !info.hasTypeInformation()) && lhs.isQualifiedName()) { Node rhs = NodeUtil.getRValueOfLValue(lhs); if (rhs != null && rhs.isQualifiedName()) { Name lhsName = namespace.getOwnSlot(lhs.getQualifiedName()); Name rhsName = namespace.getOwnSlot(rhs.getQualifiedName()); if (lhsName != null && lhsName.calculateInlinability().shouldInlineUsages() && rhsName != null && rhsName.calculateInlinability().shouldInlineUsages()) { aliases.put(lhs.getQualifiedName(), rhs.getQualifiedName()); } } } } private boolean isDeclaredConst(Node lhs, JSDocInfo info) { if (info != null && info.hasConstAnnotation()) { return true; } return lhs.getParent().isConst(); } } private class AliasesInliner extends ExternsSkippingCallback { @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case NAME: case GETPROP: if (n.isQualifiedName() && aliases.containsKey(n.getQualifiedName())) { if (isLeftmostNameLocal(t, n)) { // The alias is shadowed by a local variable. Don't rewrite. return; } if (NodeUtil.isNameDeclOrSimpleAssignLhs(n, parent)) { // The node defines an alias. Don't rewrite. return; } Node newNode = astFactory.createQName(namespace, resolveAlias(n.getQualifiedName(), n)); if (isLeftmostNameLocal(t, newNode)) { // The aliased name is shadowed by a local variable. Don't rewrite. return; } // If n is get_prop like "obj.foo" then newNode should use only location of foo, not // obj.foo. newNode.srcrefTree(n); // Similarly if n is get_prop like "obj.foo" we should index only foo. obj should not // be indexed as it's invisible to users. if (newNode.isGetProp()) { newNode.getFirstChild().makeNonIndexableRecursive(); } n.replaceWith(newNode); t.reportCodeChange(); } break; default: break; } } private boolean isLeftmostNameLocal(NodeTraversal t, Node n) { checkState(n.isQualifiedName()); String leftmostName = NodeUtil.getRootOfQualifiedName(n).getString(); Var v = t.getScope().getVar(leftmostName); return v != null && v.isLocal(); } /** * Use the alias table to look up the resolved name of the given alias. If the result is also * an alias repeat until the real name is resolved. * * @param n */ private String resolveAlias(String name, Node n) { Set aliasPath = new LinkedHashSet<>(); while (aliases.containsKey(name)) { if (!aliasPath.add(name)) { compiler.report(JSError.make(n, ALIAS_CYCLE, aliasPath.toString(), name)); // Cut the cycle so that it doesn't get reported more than once. aliases.remove(name); break; } name = aliases.get(name); } return name; } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy