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

com.google.javascript.jscomp.OptimizeArgumentsArray 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.

There is a newer version: v20240317
Show newest version
/*
 * Copyright 2009 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 static com.google.javascript.jscomp.base.JSCompDoubles.isExactInt32;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.javascript.jscomp.NodeTraversal.ScopedCallback;
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.List;
import org.jspecify.nullness.Nullable;

/**
 * Optimization for functions that have {@code var_args} or access the
 * arguments array.
 *
 * 

Example: *

 * function() { alert(arguments[0] + argument[1]) }
 * 
* to: *
 * function(a, b) { alert(a, b) }
 * 
* * Each newly inserted variable name will be unique very much like the output * of the AST found after the {@link Normalize} pass. */ class OptimizeArgumentsArray implements CompilerPass, ScopedCallback { // The arguments object as described by ECMAScript version 3 section 10.1.8 private static final String ARGUMENTS = "arguments"; // To ensure that the newly introduced parameter names are unique. We will // use this string as prefix unless the caller specifies a different prefix. private static final String PARAMETER_PREFIX = "JSCompiler_OptimizeArgumentsArray_p"; // The prefix for the newly introduced parameter name. private final String paramPrefix; // To make each parameter name unique in the function we append a unique integer. private int uniqueId = 0; // Reference to the compiler object to notify any changes to source code AST. private final AbstractCompiler compiler; // A stack of arguments access list to the corresponding outer functions. private final Deque> argumentsAccessStack = new ArrayDeque<>(); // The `arguments` access in the current scope. // // The elements are NAME nodes. This initial value is a error-detecting sentinel for the global // scope, which is used because since `ArrayDeque` is null-hostile. private List currentArgumentsAccesses = ImmutableList.of(); /** * Construct this pass and use {@link #PARAMETER_PREFIX} as the prefix for * all parameter names that it introduces. */ OptimizeArgumentsArray(AbstractCompiler compiler) { this(compiler, PARAMETER_PREFIX); } /** * @param paramPrefix the prefix to use for all parameter names that this * pass introduces */ OptimizeArgumentsArray(AbstractCompiler compiler, String paramPrefix) { this.compiler = checkNotNull(compiler); this.paramPrefix = checkNotNull(paramPrefix); } @Override public void process(Node externs, Node root) { NodeTraversal.traverse(compiler, checkNotNull(root), this); } @Override public void enterScope(NodeTraversal traversal) { if (!definesArgumentsVar(traversal.getScopeRoot())) { return; } // Introduces a new access list and stores the access list of the outer scope. argumentsAccessStack.push(currentArgumentsAccesses); currentArgumentsAccesses = new ArrayList<>(); } @Override public void exitScope(NodeTraversal traversal) { if (!definesArgumentsVar(traversal.getScopeRoot())) { return; } // Attempt to replace the argument access and if the AST has been change, // report back to the compiler. tryReplaceArguments(traversal.getScopeRoot()); currentArgumentsAccesses = argumentsAccessStack.pop(); } private static boolean definesArgumentsVar(Node root) { return root.isFunction() && !root.isArrowFunction(); } @Override public boolean shouldTraverse(NodeTraversal unused0, Node unused1, Node unused2) { return true; } @Override public void visit(NodeTraversal traversal, Node node, Node parent) { if (traversal.inGlobalHoistScope()) { return; // Do no rewriting in the global scope. } if (node.isName() && ARGUMENTS.equals(node.getString())) { currentArgumentsAccesses.add(node); // Record all potential references to the arguments array. } } /** * Tries to optimize all the arguments array access in this scope by assigning a name to each * element. * * @param scope scope of the function */ private void tryReplaceArguments(Node scopeRoot) { Node scopeRootParent = scopeRoot.getParent(); // Getters cannot have any params, so there are none to replace or synthesize. if (scopeRootParent.isGetterDef()) { return; } // Find the number of parameters that can be accessed without using `arguments`. Node parametersList = NodeUtil.getFunctionParameters(scopeRoot); checkState(parametersList.isParamList(), parametersList); int numParameters = parametersList.getChildCount(); // Determine the highest index that is used to make an access on `arguments`. By default, assume // that the value is the number of parameters to the function. int highestIndex = getHighestIndex(numParameters - 1); if (highestIndex < 0) { return; } int maxCount = highestIndex + 1; // Setters can only have one param and it is required to be present, so we cannot synthesize any // new params, but we can still replace references to the first param. if (scopeRootParent.isSetterDef()) { maxCount = 1; } ImmutableSortedMap argNames = assembleParamNames(parametersList, maxCount); changeMethodSignature(argNames, parametersList); changeBody(argNames); } /** * Iterate through all the references to arguments array in the * function to determine the real highestIndex. Returns -1 when we should not * be replacing any arguments for this scope - we should exit tryReplaceArguments * * @param highestIndex highest index that has been accessed from the arguments array */ private int getHighestIndex(int highestIndex) { for (Node ref : currentArgumentsAccesses) { Node getElem = ref.getParent(); // Bail on anything but argument[c] access where c is a constant. // TODO(user): We might not need to bail out all the time, there might // be more cases that we can cover. if (!getElem.isGetElem() || ref != getElem.getFirstChild()) { return -1; } Node indexNode = ref.getNext(); // We have something like arguments[x] where x is not a constant. That // means at least one of the access is not known. if (!indexNode.isNumber()) { // TODO(user): Its possible not to give up just yet. The type // inference did a 'semi value propagation'. If we know that string // is never a subclass of the type of the index. We'd know that // it is never 'callee'. return -1; // Give up. } // We want to bail out if someone tries to access arguments[0.5] for example double index = indexNode.getDouble(); if (!isExactInt32(index)) { return -1; } Node getElemParent = getElem.getParent(); // When we have argument[0](), replacing it with a() is semantically // different if argument[0] is a function call that refers to 'this' if (getElemParent.isCall() && getElemParent.getFirstChild() == getElem) { // TODO(user): We can consider using .call() if aliasing that // argument allows shorter alias for other arguments. return -1; } // Replace the highest index if we see an access that has a higher index // than all the one we saw before. int indexInt = (int) index; if (indexInt > highestIndex) { highestIndex = indexInt; } } return highestIndex; } /** * Inserts new formal parameters into the method's signature based on the given set of names. * *

Example: function() --> function(r0, r1, r2) * * @param argNames maps param index to param name, if the param with that index has a name. * @param paramList node representing the function signature */ private void changeMethodSignature(ImmutableSortedMap argNames, Node paramList) { ImmutableSortedMap newParams = argNames.tailMap(paramList.getChildCount()); for (String name : newParams.values()) { paramList.addChildToBack(IR.name(name).srcrefIfMissing(paramList)); } if (!newParams.isEmpty()) { compiler.reportChangeToEnclosingScope(paramList); } } /** * Performs the replacement of arguments[x] -> a if x is known. * * @param argNames maps param index to param name, if the param with that index has a name. */ private void changeBody(ImmutableMap argNames) { for (Node ref : currentArgumentsAccesses) { Node index = ref.getNext(); Node parent = ref.getParent(); int value = (int) index.getDouble(); // This was validated earlier. @Nullable String name = argNames.get(value); if (name == null) { continue; } Node newName = IR.name(name).srcrefIfMissing(parent); parent.replaceWith(newName); // TODO(nickreid): See if we can do this fewer times. The accesses may be in different scopes. compiler.reportChangeToEnclosingScope(newName); } } /** * Generates a {@link Map} from argument indices to parameter names. * *

A {@link Map} is used because the sequence may be sparse in the case that there is an * anonymous param, such as a destructuring param. There may also be fewer returned names than * {@code maxCount} if there is a rest param, since no additional params may be synthesized. * * @param maxCount The maximum number of argument names in the returned map. */ private ImmutableSortedMap assembleParamNames(Node paramList, int maxCount) { checkArgument(paramList.isParamList(), paramList); ImmutableSortedMap.Builder builder = ImmutableSortedMap.naturalOrder(); int index = 0; // Collect all existing param names first... for (Node param = paramList.getFirstChild(); param != null; param = param.getNext()) { switch (param.getToken()) { case NAME: builder.put(index, param.getString()); break; case ITER_REST: return builder.buildOrThrow(); case DEFAULT_VALUE: // `arguments` doesn't consider default values. It holds exactly the provided args. case OBJECT_PATTERN: case ARRAY_PATTERN: // Patterns have no names to substitute into the body. break; default: throw new IllegalArgumentException(param.toString()); } index++; } // ... then synthesize any additional param names. for (; index < maxCount; index++) { builder.put(index, paramPrefix + uniqueId++); } return builder.buildOrThrow(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy