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

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

There is a newer version: 9.0.8
Show newest version
/*
 * Copyright 2016 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.checkState;

import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.javascript.jscomp.GlobalNamespace.AstChange;
import com.google.javascript.jscomp.GlobalNamespace.Name;
import com.google.javascript.jscomp.GlobalNamespace.Ref;
import com.google.javascript.jscomp.GlobalNamespace.Ref.Type;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
 * 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. */ class AggressiveInlineAliases implements CompilerPass { static final DiagnosticType UNSAFE_CTOR_ALIASING = DiagnosticType.warning( "JSC_UNSAFE_CTOR_ALIASING", "Variable {0} aliases a constructor, " + "so it cannot be assigned multiple times"); /** * @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) { rewriteAliasProp(value, depth, newNodes, prop); } } /** * @param value The value to use when rewriting. * @param depth The chain depth. * @param newNodes Expression nodes that have been updated. * @param prop The property to rewrite with value. */ private void rewriteAliasProp(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.node; 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: " + 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.module, ref.scope, ref.node)); codeChanged = true; } } private final AbstractCompiler compiler; private boolean codeChanged; AggressiveInlineAliases(AbstractCompiler compiler) { this.compiler = compiler; this.codeChanged = true; } @Override public void process(Node externs, Node root) { while (this.codeChanged) { this.codeChanged = false; GlobalNamespace namespace = new GlobalNamespace(compiler, root); inlineAliases(namespace); } } private JSModule getRefModule(Reference ref) { CompilerInput input = compiler.getInput(ref.getInputId()); return input == null ? null : input.getModule(); } /** * 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. * * @see InlineVariables */ private void inlineAliases(GlobalNamespace namespace) { // Invariant: All the names in the worklist meet condition (a). 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.type == Name.Type.GET || name.type == Name.Type.SET) { continue; } if (!name.inExterns && name.globalSets == 1 && name.localSets == 0 && name.aliasingGets > 0) { // {@code name} meets condition (b). Find all of its local aliases // and try to inline them. List refs = new ArrayList<>(name.getRefs()); for (Ref ref : refs) { Scope hoistScope = ref.scope.getClosestHoistScope(); if (ref.type == 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. if (inlineAliasIfPossible(name, ref, namespace)) { name.removeRef(ref); } } else if (ref.type == Type.ALIASING_GET && hoistScope.isGlobal() && ref.getTwin() == null) { // ignore aliases in chained assignments if (inlineGlobalAliasIfPossible(name, ref, namespace)) { name.removeRef(ref); } } } } if (!name.inExterns && name.type == Name.Type.CLASS) { List subclasses = name.subclasses; if (subclasses != null && name.props != null) { for (Name subclass : subclasses) { for (Name prop : name.props) { rewriteAllSubclassInheritedAccesses(name, subclass, prop, namespace); } } } } // Check if {@code name} has any aliases left after the // local-alias-inlining above. if ((name.type == Name.Type.OBJECTLIT || name.type == Name.Type.FUNCTION || name.type == Name.Type.CLASS) && name.aliasingGets == 0 && name.props != null) { // All of {@code name}'s children meet condition (a), so they can be // added to the worklist. workList.addAll(name.props); } } } /** * 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 subclassNameObj The Name of the subclass * @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, Name subclassNameObj, Name prop, GlobalNamespace namespace) { Ref propDeclRef = prop.getDeclaration(); if (propDeclRef == null || propDeclRef.node == null || !propDeclRef.node.getParent().isAssign()) { return false; } Node propRhs = propDeclRef.node.getParent().getLastChild(); if (propRhs.isFunction()) { return false; } String subclassQualifiedPropName = subclassNameObj.getFullName() + "." + 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.localSets > 0 || subclassPropNameObj.globalSets > 0)) { return false; } // Recurse to find potential sub-subclass accesses of the superclass property. if (subclassNameObj.subclasses != null) { for (Name name : subclassNameObj.subclasses) { rewriteAllSubclassInheritedAccesses(superclassNameObj, name, prop, namespace); } } if (subclassPropNameObj != null) { Set newNodes = new LinkedHashSet<>(); // Use this node as a template for rewriteAliasProp. Node superclassNameNode = superclassNameObj.getDeclaration().node; if (superclassNameNode.isName()) { superclassNameNode = superclassNameNode.cloneNode(); } else if (superclassNameNode.isGetProp()) { superclassNameNode = superclassNameNode.cloneTree(); } else { return false; } rewriteAliasProp(superclassNameNode, 0, newNodes, subclassPropNameObj); namespace.scanNewNodes(newNodes); } return true; } /** * 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 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.node.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; } private boolean 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.node.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); ReferenceCollectingCallback collector = new ReferenceCollectingCallback( compiler, ReferenceCollectingCallback.DO_NOTHING_BEHAVIOR, new Es6SyntacticScopeCreator(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. replaceAliasAssignment(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 true; } 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)) { return true; } else { // 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)); } } } } return false; } /** * 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) { replaceAliasAssignment(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 void replaceAliasAssignment(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; } Node aliasParent = alias.node.getParent(); aliasParent.replaceChild(alias.node, IR.nullNode()); alias.name.removeRef(alias); codeChanged = true; compiler.reportChangeToEnclosingScope(aliasParent); } /** * Returns whether a ReferenceCollection for some aliasing variable references a property on the * original aliased variable that may be collapsed in CollapseProperties. * *

See {@link GlobalNamespace.Name#canCollapse} for what can/cannot be collapsed. */ private boolean referencesCollapsibleProperty( ReferenceCollection aliasRefs, Name aliasedName, GlobalNamespace namespace) { for (Reference ref : aliasRefs.references) { if (ref.getParent() == null) { continue; } if (ref.getParent().isGetProp()) { Node propertyNode = ref.getNode().getNext(); // e.g. if the reference is "alias.b.someProp", this will be "b". String propertyName = propertyNode.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; } /** * @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) { Node newNode = alias.node.cloneTree(); aliasRef.getParent().replaceChild(aliasRef.getNode(), newNode); compiler.reportChangeToEnclosingScope(newNode); return new AstChange(getRefModule(aliasRef), 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. * * @param alias The alias to inline * @return Whether the alias was inlined. */ private boolean inlineGlobalAliasIfPossible(Name name, Ref alias, GlobalNamespace namespace) { // Ensure that the alias is assigned to global name at that the // declaration. Node aliasParent = alias.node.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. // TODO(tbreisacher): Do we still need this special case? || (aliasParent.isName() && name.isConstructor())) { Node lvalue = aliasParent.isName() ? aliasParent : aliasParent.getFirstChild(); if (!lvalue.isQualifiedName()) { return false; } if (lvalue.isName() && compiler.getCodingConvention().isExported(lvalue.getString(), /* local */ false)) { return false; } name = namespace.getSlot(lvalue.getQualifiedName()); if (name != null && name.isInlinableGlobalAlias()) { Set newNodes = new LinkedHashSet<>(); List refs = new ArrayList<>(name.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: Node newNode = alias.node.cloneTree(); Node node = ref.node; node.getParent().replaceChild(node, newNode); compiler.reportChangeToEnclosingScope(newNode); newNodes.add(new AstChange(ref.module, ref.scope, newNode)); name.removeRef(ref); break; default: throw new IllegalStateException(); } } rewriteAliasProps(name, alias.node, 0, newNodes); // just set the original alias to null. aliasParent.replaceChild(alias.node, IR.nullNode()); codeChanged = true; compiler.reportChangeToEnclosingScope(aliasParent); // Inlining the variable may have introduced new references // to descendants of {@code name}. So those need to be collected now. namespace.scanNewNodes(newNodes); return true; } } return false; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy