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

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

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

The newest version!
/*
 * Copyright 2008 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.LinkedHashMultimap;
import com.google.javascript.jscomp.CodingConvention.SubclassRelationship;
import com.google.javascript.jscomp.ReferenceCollector.Behavior;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import org.jspecify.nullness.Nullable;

/**
 * Using the infrastructure provided by {@link ReferenceCollector}, identify variables that are used
 * in a way that is safe to move, and then inline them.
 *
 * 

This pass has two "modes." One mode only inlines variables declared as constants, for legacy * compiler clients. The second mode inlines any variable that we can provably inline. Note that the * second mode is a superset of the first mode. We only support the first mode for * backwards-compatibility with compiler clients that don't want --inline_variables. * *

The basic structure of this class is as follows. * *

    *
  1. {@link ReferenceCollector#process} is invoked on the AST with an instance of {@link * InliningBehavior} *
  2. {@link InliningBehavior#afterExitScope} gets invoked on each scope in DFS order *
  3. It iterates through the variables defined in that scope: *
      *
    1. For each variable a {@link VarExpert} is created. This object is responsible for * determining: *
        *
      • Can it be inlined? *
      • If so, how should the inlining be done? *
      • If not, is it safe to inline aliases of the variable? (e.g. {@code const b = * doSomething();} isn't safe to inline due to side-effects, but it should be OK * to inline {@code const aliasB = b;} in many cases. *
      *
    2. The {@link VarExpert} creates an {@link InlineVarAnalysis} object containing its * decisions and possibly a method to invoke to do the inlining. *
    3. The {@link InlineVarAnalysis} may indicate that a variable is an alias of another * variable. If so, {@link InliningBehavior} will wait until the aliased variable has * been analyzed, then pass that analysis back to the {@link VarExpert} for the aliasing * variable, so it can complete its decision. *
    *
*/ class InlineVariables implements CompilerPass { private final AbstractCompiler compiler; enum Mode { // Only inline things explicitly marked as constant. CONSTANTS_ONLY(Var::isDeclaredOrInferredConst), // Locals only LOCALS_ONLY(Var::isLocal), ALL(Predicates.alwaysTrue()); @SuppressWarnings("ImmutableEnumChecker") private final Predicate varPredicate; private Mode(Predicate varPredicate) { this.varPredicate = varPredicate; } } private final Mode mode; InlineVariables(AbstractCompiler compiler, Mode mode) { this.compiler = compiler; this.mode = mode; } @Override public void process(Node externs, Node root) { ReferenceCollector callback = new ReferenceCollector( compiler, new InliningBehavior(), new SyntacticScopeCreator(compiler), mode.varPredicate); callback.process(externs, root); } /** Responsible for analyzing a variable to determine if it can be inlined. */ private abstract static class VarExpert { /** Called to conduct the initial analysis */ abstract InlineVarAnalysis analyze(); /** * Called for a Var that is an alias after the original variable is handled. * * @param aliasedVar The variable that was aliased. * @param aliasedVarAnalysis The analysis results for the aliased variable */ InlineVarAnalysis reanalyzeAfterAliasedVar( Var aliasedVar, InlineVarAnalysis aliasedVarAnalysis) { throw new UnsupportedOperationException("not waiting for an aliased variable"); } } // Canonical `VarExpert` objects for common cases. private static final VarExpert NO_INLINE_SELF_OR_ALIASES_EXPERT = new VarExpert() { @Override public InlineVarAnalysis analyze() { return NO_INLINE_SELF_OR_ALIASES_ANALYSIS; } }; private static final VarExpert NO_INLINE_SELF_ALIASES_OK_EXPERT = new VarExpert() { @Override public InlineVarAnalysis analyze() { return NO_INLINE_SELF_ALIASES_OK_ANALYSIS; } }; /** * The result of analyzing a variable to see if it can be inlined. * *

Non-abstract classes should be created by extending this class and overriding the methods * that should return `true` or perform an operation. */ private abstract static class InlineVarAnalysis { /** * Should we inline this variable? * *

Mutually exclusive with `shouldWaitForAliasedVar()`. */ public boolean shouldInline() { return false; } /** * True if this variable is an alias. * *

The caller should wait for the aliased variable to be handled, then call the expert's * `reanalyzeAfterAliasedVar()` method. * *

Mutually exclusive with `shouldInline()` */ public boolean shouldWaitForAliasedVar() { return false; } /** * Gets the aliased variable, if this is an alias. * * @return The `Var` for which this one is an alias * @throws `UnsupportedOperationException` if `shouldWaitForAliasedVar()` is `false`. */ public Var getAliasedVar() { throw new UnsupportedOperationException("no aliased Var"); } /** True if it is safe for aliases of this variable to inline this variable's name. */ public boolean isSafeToInlineAliases() { return false; } /** Performs the inline operation. */ public void performInline() { throw new UnsupportedOperationException("cannot inline"); } } // Canonical `InlineVarAnalysis` values. // We'll use these instead of creating new objects for each analysis. private static final InlineVarAnalysis NO_INLINE_SELF_OR_ALIASES_ANALYSIS = new InlineVarAnalysis() {}; private static final InlineVarAnalysis NO_INLINE_SELF_ALIASES_OK_ANALYSIS = new InlineVarAnalysis() { @Override public boolean isSafeToInlineAliases() { return true; } }; /** * Builds up information about nodes in each scope. When exiting the scope, inspects all variables * in that scope, and inlines any that we can. */ private class InliningBehavior implements Behavior { /** * Records the analyses of variables that have already been handled in the current scope. * *

This is necessary in order for aliases of those variables to determine whether they may be * inlined. */ final HashMap currentScopeHandledVarAnalysesMap = new HashMap<>(); /** * Records alias variables that are waiting for the original variables to be handled. * *

The value is an object that knows what to do when the original variable is handled. */ final LinkedHashMultimap varToAliasRetryHandlersMap = LinkedHashMultimap.create(); @Override public void afterExitScope(NodeTraversal t, ReferenceMap referenceMap) { doInlinesForScope(t, referenceMap); } /** * For all variables in this scope, see if they are only used once. If it looks safe to do so, * inline them. */ private void doInlinesForScope(NodeTraversal t, ReferenceMap referenceMap) { // Any variables we completed in an earlier scope are now out of scope, // so we can clear this map. currentScopeHandledVarAnalysesMap.clear(); boolean mayBeAParameterModifiedViaArguments = varsInThisScopeMayBeModifiedUsingArguments(t.getScope(), referenceMap); for (Var v : t.getScope().getVarIterable()) { ReferenceCollection referenceInfo = referenceMap.getReferences(v); VarExpert expert = createVarExpert(v, referenceInfo, mayBeAParameterModifiedViaArguments); final InlineVarAnalysis analysis = expert.analyze(); if (analysis.shouldWaitForAliasedVar()) { // We need to wait for the aliased variable to be handled. final AliasInlineRetryHandler retryHandler = new AliasInlineRetryHandler(v, expert); final Var aliasedVar = analysis.getAliasedVar(); final InlineVarAnalysis aliasedVarAnalysis = currentScopeHandledVarAnalysesMap.get(aliasedVar); if (aliasedVarAnalysis != null) { // We've actually already completed the aliased Var retryHandler.handleAliasedVarCompletion(aliasedVar, aliasedVarAnalysis); } else { // wait for completion of the aliased Var varToAliasRetryHandlersMap.put(aliasedVar, retryHandler); } } else { if (analysis.shouldInline()) { analysis.performInline(); } // Record the results for aliases we may find later currentScopeHandledVarAnalysesMap.put(v, analysis); // Retry any aliases we saw before handling this Var retryAliases(v, analysis); } } } /** * Returns true for function scopes (the whole function, not the body), if the function uses * `arguments` in some way other than a few that are known not to change the values of parameter * variables. * *

TODO(bradfordcsmith): In strict mode `arguments` is a copy of the parameters, so modifying * it cannot change the parameters. Sloppy mode is now so rare, that we should probably just * ignore the possibility of `arguments` being used to modify a parameter value. We ignore loose * mode issues or simply don't support them ("with" for instance). */ private boolean varsInThisScopeMayBeModifiedUsingArguments( Scope scope, ReferenceMap referenceMap) { if (scope.isFunctionScope() && !scope.getRootNode().isArrowFunction()) { Var arguments = scope.getArgumentsVar(); ReferenceCollection refs = referenceMap.getReferences(arguments); if (refs != null && !refs.references.isEmpty()) { for (Reference ref : refs.references) { if (!isSafeUseOfArguments(ref.getNode())) { return true; } } } } return false; } private void retryAliases(Var aliasedVar, InlineVarAnalysis aliasedVarAnalysis) { for (AliasInlineRetryHandler aliasInlineRetryHandler : varToAliasRetryHandlersMap.removeAll(aliasedVar)) { aliasInlineRetryHandler.handleAliasedVarCompletion(aliasedVar, aliasedVarAnalysis); } } /** Retries inlining an alias variable after the original variable has been dealt with. */ private class AliasInlineRetryHandler { private final Var v; private final VarExpert expert; AliasInlineRetryHandler(Var v, VarExpert expert) { this.v = v; this.expert = expert; } void handleAliasedVarCompletion(Var aliasedVar, InlineVarAnalysis aliasedVarAnalyis) { InlineVarAnalysis newAnalysis = expert.reanalyzeAfterAliasedVar(aliasedVar, aliasedVarAnalyis); checkState( !newAnalysis.shouldWaitForAliasedVar(), "expert for %s asked to wait a second time", v); if (newAnalysis.shouldInline()) { newAnalysis.performInline(); } currentScopeHandledVarAnalysesMap.put(v, newAnalysis); retryAliases(v, newAnalysis); } } /** * Knows how to analyze a variable to determine whether it should be inlined, how to do the * inlining, and whether it's OK to inline aliases of the variable. */ private class StandardVarExpert extends VarExpert { private final Var v; private final ReferenceCollection referenceInfo; private final Reference declaration; private final boolean isDeclaredOrInferredConstant; private final boolean mayBeAParameterModifiedViaArguments; StandardVarExpert(VarExpertInitData initData) { this.v = initData.v; this.referenceInfo = initData.referenceInfo; this.declaration = initData.referenceInfo.references.get(0); this.isDeclaredOrInferredConstant = initData.isDeclaredOrInferredConstant; this.mayBeAParameterModifiedViaArguments = initData.mayBeAParameterModifiedViaArguments; } private final InitiallyUnknown isNeverAssigned = new InitiallyUnknown<>(); private boolean isNeverAssigned() { if (isNeverAssigned.isKnown()) { return isNeverAssigned.getKnownValue(); } else if (initializationReference.isKnownNotNull() || isAssignedOnceInLifetime.isKnownToBe(true)) { return isNeverAssigned.setKnownValueOnce(false); } else { return isNeverAssigned.setKnownValueOnce(referenceInfo.isNeverAssigned()); } } private final InitiallyUnknown isWellDefined = new InitiallyUnknown<>(); private boolean isWellDefined() { if (isWellDefined.isKnown()) { return isWellDefined.getKnownValue(); } else { return isWellDefined.setKnownValueOnce(referenceInfo.isWellDefined()); } } private final InitiallyUnknown isAssignedOnceInLifetime = new InitiallyUnknown<>(); private boolean isAssignedOnceInLifetime() { if (isAssignedOnceInLifetime.isKnown()) { return isAssignedOnceInLifetime.getKnownValue(); } else { return isAssignedOnceInLifetime.setKnownValueOnce( referenceInfo.isAssignedOnceInLifetime()); } } private final InitiallyUnknown isWellDefinedAssignedOnce = new InitiallyUnknown<>(); /** * A more efficient way to ask if the variable is both well defined and assigned exactly once * in its lifetime. */ private boolean isWellDefinedAssignedOnce() { if (isWellDefinedAssignedOnce.isKnown()) { return isWellDefinedAssignedOnce.getKnownValue(); } else { // Check first to see if either is already known to be false before calculating anything. return isWellDefinedAssignedOnce.setKnownValueOnce( !isWellDefined.isKnownToBe(false) && !isAssignedOnceInLifetime.isKnownToBe(false) // isWellDefined is generally less expensive to calculate && isWellDefined() && isAssignedOnceInLifetime()); } } private final InitiallyUnknown initializationReference = new InitiallyUnknown<>(); private Reference getInitialization() { if (initializationReference.isKnown()) { return initializationReference.getKnownValue(); } else { final Reference initRef = isDeclaredOrInferredConstant ? referenceInfo.getInitializingReferenceForConstants() : referenceInfo.getInitializingReference(); return initializationReference.setKnownValueOnce(initRef); } } private boolean hasValidDeclaration() { return isValidDeclaration(declaration); } private boolean hasValidInitialization() { return isValidInitialization(getInitialization()); } private final InitiallyUnknown allReferencesAreValid = new InitiallyUnknown<>(); private boolean allReferencesAreValid() { if (allReferencesAreValid.isKnown()) { return allReferencesAreValid.getKnownValue(); } else { final boolean hasValidDeclaration = hasValidDeclaration(); final boolean hasValidInitialization = hasValidInitialization(); final boolean neverAssigned = isNeverAssigned(); if (hasValidDeclaration && (hasValidInitialization || neverAssigned)) { Reference initialization = getInitialization(); for (Reference ref : referenceInfo.references) { if (ref != declaration && ref != initialization && !isValidReference(ref)) { return allReferencesAreValid.setKnownValueOnce(false); } } return allReferencesAreValid.setKnownValueOnce(true); } return allReferencesAreValid.setKnownValueOnce(false); } } @Override public InlineVarAnalysis analyze() { if (hasNoInlineAnnotation(v)) { return getNegativeInlineVarAnalysis(); } final Reference initialization = getInitialization(); final Node initValue = initialization == null ? null : initialization.getAssignedValue(); final InitialValueAnalysis initialValueAnalysis = new InitialValueAnalysis(initValue); if (isDeclaredOrInferredConstant && initialValueAnalysis.isImmutableValue() // isAssignedOnceInLifetime() is much more expensive than the other checks && isAssignedOnceInLifetime()) { return createInlineWellDefinedVarAnalysis(initValue); } if (mode == Mode.CONSTANTS_ONLY) { // If we're in constants-only mode, don't run more aggressive // inlining heuristics. See InlineConstantsTest. return getNegativeInlineVarAnalysis(); } if (initialValueAnalysis.isAlias()) { return new VarIsAliasAnalysis(initialValueAnalysis.getAliasedVar()); } return analyzeWithInitialValue(initialization, initValue, initialValueAnalysis); } private InlineVarAnalysis analyzeWithInitialValue( Reference initialization, Node initValue, InitialValueAnalysis initialValueAnalysis) { final int refCount = referenceInfo.references.size(); // TODO(bradfordcsmith): We could remove the `refCount > 1` here, but: // 1. That will require some additional logic to handle stuff like named function // expressions and avoiding the removal of side-effects. // 2. RemoveUnusedCode will remove those cases anyway. // 3. The unit tests will have to be more verbose to make sure stuff we don't want to be // removed is used. if (refCount > 1 && allReferencesAreValid()) { if (referenceInfo.isNeverAssigned()) { return new PositiveInlineVarAnalysis( () -> { // Create a new `undefined` node to inline for a variable that is never // initialized. Node srcLocation = declaration.getNode(); final Node undefinedNode = NodeUtil.newUndefinedNode(srcLocation); inlineWellDefinedVariable(v, undefinedNode, referenceInfo.references); }); } if (isWellDefined() && (initialValueAnalysis.isImmutableValue() || (initValue.isThis() && !referenceInfo.isEscaped()))) { // if the variable is referenced more than once, we can only // inline it if it's immutable and never defined before referenced. return createInlineWellDefinedVarAnalysis(initValue); } final int firstReadRefIndex = (declaration == initialization ? 1 : 2); final int numReadRefs = refCount - firstReadRefIndex; if (numReadRefs == 0) { // The only reference is the initialization. // Remove the assignment and the variable declaration. return createInlineWellDefinedVarAnalysis(initValue); } if (numReadRefs == 1) { // The variable is likely only read once, so we can try some more complex inlining // heuristics. final Reference singleReadReference = referenceInfo.references.get(firstReadRefIndex); if (canInline(declaration, initialization, singleReadReference, initValue)) { // A custom inline method is needed for this case. return new PositiveInlineVarAnalysis( () -> inline(declaration, initialization, singleReadReference)); } } } return getNegativeInlineVarAnalysis(); } private InlineVarAnalysis getNegativeInlineVarAnalysis() { // If we already know whether it is safe to inline aliases of this variable, // use canonical analysis values to save on memory space. if (mayBeAParameterModifiedViaArguments || mode == Mode.CONSTANTS_ONLY || isWellDefinedAssignedOnce.isKnownToBe(false)) { return NO_INLINE_SELF_OR_ALIASES_ANALYSIS; } if (isWellDefinedAssignedOnce.isKnownToBe(true)) { return NO_INLINE_SELF_ALIASES_OK_ANALYSIS; } // Delay calculating safety until we're actually asked. return new InlineVarAnalysis() { @Override public boolean isSafeToInlineAliases() { return isWellDefinedAssignedOnce(); } }; } private PositiveInlineVarAnalysis createInlineWellDefinedVarAnalysis(Node initValue) { return new PositiveInlineVarAnalysis( () -> inlineWellDefinedVariable(v, initValue, referenceInfo.references)); } @Override public InlineVarAnalysis reanalyzeAfterAliasedVar( Var aliasedVar, InlineVarAnalysis aliasedVarAnalysis) { final Reference initialization = getInitialization(); final Node initValue = initialization == null ? null : initialization.getAssignedValue(); final InitialValueAnalysis initialValueAnalysis = new InitialValueAnalysis(initValue); if (isDeclaredOrInferredConstant && initialValueAnalysis.isImmutableValue() // isAssignedOnceInLifetime() is much more expensive than the other checks && isAssignedOnceInLifetime()) { return createInlineWellDefinedVarAnalysis(initValue); } if (initialValueAnalysis.isAlias() && aliasedVarAnalysis.isSafeToInlineAliases() && isWellDefinedAssignedOnce()) { // The variable we aliased couldn't be inlined itself, or it was an alias for another // variable that got inlined in its place. // However, it is safe to inline the name assigned to this variable now. return createInlineWellDefinedVarAnalysis(initValue); } return analyzeWithInitialValue(initialization, initValue, initialValueAnalysis); } /** Information about a value used to initialize a variable. */ private class InitialValueAnalysis { private final Node value; InitialValueAnalysis(Node value) { this.value = value; } private final InitiallyUnknown isImmutableValue = new InitiallyUnknown<>(); boolean isImmutableValue() { if (isImmutableValue.isKnown()) { return isImmutableValue.getKnownValue(); } else { return isImmutableValue.setKnownValueOnce( value != null && NodeUtil.isImmutableValue(value)); } } private final InitiallyUnknown aliasedVar = new InitiallyUnknown<>(); boolean isAlias() { return getAliasedVar() != null; } Var getAliasedVar() { if (aliasedVar.isKnown()) { return aliasedVar.getKnownValue(); } else { if (value != null && value.isName()) { String aliasedName = value.getString(); // Never consider this variable to be an alias of itself. return aliasedVar.setKnownValueOnce( aliasedName.equals(v.getName()) ? null : v.getScope().getVar(aliasedName)); } return aliasedVar.setKnownValueOnce(null); } } } } /** Indicates that the analyzed variable may be inlined. */ private class PositiveInlineVarAnalysis extends InlineVarAnalysis { private final Runnable inliner; private PositiveInlineVarAnalysis(Runnable inliner) { this.inliner = inliner; } @Override public boolean shouldInline() { return true; } @Override public void performInline() { inliner.run(); } @Override public boolean isSafeToInlineAliases() { // If we've inlined this variable, then any aliases of it should definitely try to // inline themselves with the new value that replaced this variable. // If for some reason the logic that requested this analysis decided not to actually // do the inlining, it's still safe to inline the name of this variable. // If it weren't, we wouldn't have said it was OK to inline its value. return true; } } /** * Indicates that the analyzed variable is an alias and the decision about whether to inline it * must wait until inlining has been done (or not) for the original value it aliases. */ private class VarIsAliasAnalysis extends InlineVarAnalysis { private final Var aliasedVar; private VarIsAliasAnalysis(Var aliasedVar) { this.aliasedVar = aliasedVar; } @Override public boolean shouldWaitForAliasedVar() { return true; } @Override public Var getAliasedVar() { return aliasedVar; } @Override public boolean isSafeToInlineAliases() { throw new UnsupportedOperationException("analysis is incomplete"); } } /** Creates a VarExpert object appropriate for the given variable. */ private VarExpert createVarExpert( Var v, ReferenceCollection referenceInfo, boolean mayBeAParameterModifiedViaArguments) { if (referenceInfo == null) { // If we couldn't collect any reference info, don't try to inline and assume it's unsafe // to inline aliases of this variable, too. return NO_INLINE_SELF_OR_ALIASES_EXPERT; } final boolean isDeclaredOrInferredConstant = v.isDeclaredOrInferredConst(); if (!isDeclaredOrInferredConstant && mode == Mode.CONSTANTS_ONLY) { // If we're only inlining constants, then we shouldn't inline an alias. return NO_INLINE_SELF_OR_ALIASES_EXPERT; } if (v.isExtern()) { // TODO(bradfordcsmith): Extern variables are generally unsafe to inline. return NO_INLINE_SELF_OR_ALIASES_EXPERT; } if (compiler.getCodingConvention().isExported(v.getName(), /* local= */ v.isLocal())) { // If the variable is exported, it might be assigned a new value by code we cannot see, // so aliases to it are creating snapshots of its state. // We cannot inline this variable or its aliases. return NO_INLINE_SELF_OR_ALIASES_EXPERT; } if (compiler.getCodingConvention().isPropertyRenameFunction(v.getNameNode())) { // It's not terribly likely that anything creates an alias of our special property rename // function, but its value never changes, so it should be safe to inline aliases to it. return NO_INLINE_SELF_ALIASES_OK_EXPERT; } final VarExpertInitData initData = new VarExpertInitData(); initData.v = v; initData.referenceInfo = referenceInfo; initData.isDeclaredOrInferredConstant = isDeclaredOrInferredConstant; initData.mayBeAParameterModifiedViaArguments = mayBeAParameterModifiedViaArguments; return new StandardVarExpert(initData); } /** Used to initialize fields in a `StandardVarExpert` object. */ private class VarExpertInitData { Var v; ReferenceCollection referenceInfo; boolean isDeclaredOrInferredConstant; boolean mayBeAParameterModifiedViaArguments; } /** * True if `argumentsNode` is a use of `arguments` we know won't modify any parameter values. * *

In sloppy mode it is possible to change the value of a function parameter by assigning to * the corresponding entry in {@code arguments}. * *

This method checks for just a few common uses that are known to be safe. However, we may * soon remove this check entirely because unsafe uses aren't worth worrying about. See the * comment on {@link #varsInThisScopeMayBeModifiedUsingArguments}. */ boolean isSafeUseOfArguments(Node argumentsNode) { checkArgument(argumentsNode.matchesName("arguments")); // `arguments[i]` that is only read, not assigned // or `fn.apply(thisArg, arguments)` return isTargetOfPropertyRead(argumentsNode) || isSecondArgumentToDotApplyMethod(argumentsNode); } /** * True if `n` is the object in a property access expression (`obj[prop]` or `obj.prop` or an * optional chain version of one of those) and the property is only being read, not modified. */ boolean isTargetOfPropertyRead(Node n) { Node getNode = n.getParent(); return n.isFirstChildOf(getNode) && NodeUtil.isNormalOrOptChainGet(getNode) && !NodeUtil.isLValue(getNode); } /** True if `n` is being used in a call like this: `fn.apply(thisArg, n)`. */ boolean isSecondArgumentToDotApplyMethod(Node n) { Node callNode = n.getParent(); if (NodeUtil.isNormalOrOptChainCall(callNode)) { Node calleeNode = callNode.getFirstChild(); if (NodeUtil.isNormalOrOptChainGetProp(calleeNode)) { if (calleeNode.getString().equals("apply")) { Node thisArgNode = calleeNode.getNext(); if (thisArgNode != null) { return thisArgNode.getNext() == n; } } } } return false; } /** Do the actual work of inlining a single declaration into a single reference. */ private void inline(Reference decl, Reference init, Reference ref) { Node value = init.getAssignedValue(); checkState(value != null); // Check for function declarations before the value is moved in the AST. boolean isFunctionDeclaration = NodeUtil.isFunctionDeclaration(value); if (isFunctionDeclaration) { // We're inlining this function declaration because there was only one reference to it, // probably the rhs of an assignment statement. Since we're eliminating the only // reference, we can get rid of it. See b/277538101. Node funcName = value.getFirstChild(); checkNotNull(funcName); checkState(funcName.isName()); funcName.setString(""); // In addition to changing the containing scope, inlining function declarations also changes // the function name scope from the containing scope to the inner scope. compiler.reportChangeToChangeScope(value); compiler.reportChangeToEnclosingScope(value.getParent()); } inlineValue(ref.getNode(), value.detach()); if (decl != init) { Node expressRoot = init.getGrandparent(); checkState(expressRoot.isExprResult()); NodeUtil.removeChild(expressRoot.getParent(), expressRoot); } // Function declarations have already been removed. if (!isFunctionDeclaration) { removeDeclaration(decl); } } /** Inline an immutable variable into all of its references. */ private void inlineWellDefinedVariable(Var v, Node value, List refSet) { for (Reference r : refSet) { if (r.getNode() == v.getNameNode()) { removeDeclaration(r); } else if (r.isSimpleAssignmentToName()) { /* * This is the initialization. * *

Replace the entire assignment with just the value, and use the original value node * in case it contains references to variables that still require inlining. */ inlineValue(r.getParent(), value.detach()); } else { Node clonedValue = value.cloneTree(); NodeUtil.markNewScopesChanged(clonedValue, compiler); inlineValue(r.getNode(), clonedValue); } } } /** Remove the given VAR declaration. */ private void removeDeclaration(Reference decl) { Node varNode = decl.getParent(); checkState(NodeUtil.isNameDeclaration(varNode), varNode); Node grandparent = decl.getGrandparent(); compiler.reportChangeToEnclosingScope(decl.getNode()); decl.getNode().detach(); // Remove var node if empty if (!varNode.hasChildren()) { NodeUtil.removeChild(grandparent, varNode); } } private void inlineValue(Node toRemove, Node toInsert) { compiler.reportChangeToEnclosingScope(toRemove); // Help type-based optimizations by propagating more specific types from type assertions if (toRemove.getColor() != null && toRemove.isColorFromTypeCast()) { toInsert.setColor(toRemove.getColor()); toInsert.setColorFromTypeCast(); } toRemove.replaceWith(toInsert); NodeUtil.markFunctionsDeleted(toRemove, compiler); } /** * @return true if the provided reference and declaration can be safely inlined according to our * criteria */ private boolean canInline( Reference declaration, Reference initialization, Reference reference, Node initValue) { // If the value is read more than once, skip it. // VAR declarations and EXPR_RESULT don't need the value, but other // ASSIGN expressions parents do. if (declaration != initialization && !initialization.getGrandparent().isExprResult()) { return false; } // Be very conservative and do not cross control structures or scope boundaries if (declaration.getBasicBlock() != initialization.getBasicBlock() || declaration.getBasicBlock() != reference.getBasicBlock()) { return false; } // Do not inline into a call node. This would change // the context in which it was being called. For example, // var a = b.c; // a(); // should not be inlined, because it calls a in the context of b // rather than the context of the window. // var a = b.c; // f(a) // is OK. checkState(initValue != null); if (initValue.isGetProp() && reference.getParent().isCall() && reference.getParent().getFirstChild() == reference.getNode()) { return false; } if (initValue.isFunction()) { Node callNode = reference.getParent(); if (reference.getParent().isCall()) { CodingConvention convention = compiler.getCodingConvention(); // Bug 2388531: Don't inline subclass definitions into class defining // calls as this confused class removing logic. SubclassRelationship relationship = convention.getClassesDefinedByCall(callNode); if (relationship != null) { return false; } // issue 668: Don't inline singleton getter methods // calls as this confused class removing logic. if (convention.getSingletonGetterClassName(callNode) != null) { return false; } } } if (initialization.getScope() != declaration.getScope() || !initialization.getScope().contains(reference.getScope())) { return false; } return canMoveAggressively(initValue) || canMoveModerately(initialization, reference); } /** If the value is a literal, we can cross more boundaries to inline it. */ private boolean canMoveAggressively(Node value) { // Function expressions and other mutable objects can move within // the same basic block. return NodeUtil.isLiteralValue(value, true) || value.isFunction(); } /** * If the value of a variable is not constant, then it may read or modify state. Therefore it * cannot be moved past anything else that may modify the value being read or read values that * are modified. */ private boolean canMoveModerately(Reference initialization, Reference reference) { // Check if declaration can be inlined without passing // any side-effect causing nodes. Iterator it; if (NodeUtil.isNameDeclaration(initialization.getParent())) { it = NodeIterators.LocalVarMotion.forVar( compiler, initialization.getNode(), // NAME initialization.getParent(), // VAR/LET/CONST initialization.getGrandparent()); // VAR/LET/CONST container } else if (initialization.getParent().isAssign()) { checkState(initialization.getGrandparent().isExprResult()); it = NodeIterators.LocalVarMotion.forAssign( compiler, initialization.getNode(), // NAME initialization.getParent(), // ASSIGN initialization.getGrandparent(), // EXPR_RESULT initialization.getGrandparent().getParent()); // EXPR container } else { throw new IllegalStateException( "Unexpected initialization parent\n" + initialization.getParent().toStringTree()); } Node targetName = reference.getNode(); while (it.hasNext()) { Node curNode = it.next(); if (curNode == targetName) { return true; } } return false; } /** * @return true if the reference is a normal VAR or FUNCTION declaration. */ private boolean isValidDeclaration(Reference declaration) { return (NodeUtil.isNameDeclaration(declaration.getParent()) && !NodeUtil.isLoopStructure(declaration.getGrandparent())) || NodeUtil.isFunctionDeclaration(declaration.getParent()); } /** * @return Whether there is a initial value. */ private boolean isValidInitialization(Reference initialization) { if (initialization == null) { return false; } else if (initialization.isDeclaration()) { // The reference is a FUNCTION declaration or normal VAR declaration // with a value. if (!NodeUtil.isFunctionDeclaration(initialization.getParent()) && !initialization.getNode().hasChildren()) { return false; } } else { Node parent = initialization.getParent(); checkState(parent.isAssign() && parent.getFirstChild() == initialization.getNode()); } return true; } /** * @return true if the reference is a candidate for inlining */ private boolean isValidReference(Reference reference) { return !reference.isDeclaration() && !reference.isLvalue(); } } private static boolean hasNoInlineAnnotation(Var var) { JSDocInfo jsDocInfo = var.getJSDocInfo(); return jsDocInfo != null && jsDocInfo.isNoInline(); } private static class InitiallyUnknown { protected boolean isKnown = false; protected @Nullable T value = null; boolean isKnown() { return isKnown; } boolean isKnownNotNull() { return isKnownNotToBe(null); } boolean isKnownToBe(T other) { return isKnown && value == other; } boolean isKnownNotToBe(T other) { return isKnown && value != other; } T setKnownValueOnce(T value) { checkState(!isKnown, "already known"); this.value = value; isKnown = true; return value; } T getKnownValue() { checkState(isKnown, "not yet known"); return value; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy