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

com.google.javascript.jscomp.InlineSimpleMethods 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 2007 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.collect.ImmutableSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.SetMultimap;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jspecify.nullness.Nullable;

/**
 * Inlines methods that take no arguments and have only a return statement returning a property.
 * Because it works on method names rather than type inference, a method with multiple definitions
 * will be inlined if each definition is identical.
 *
 * 
 * A.prototype.foo = function() { return this.b; }
 * B.prototype.foo = function() { return this.b; }
 * 
* * will inline foo, but * *
 * A.prototype.foo = function() { return this.b; }
 * B.prototype.foo = function() { return this.c; }
 * 
* * will not. * *

Declarations are not removed because we do not find all possible call sites. For examples, * calls of the form foo["bar"] are not detected. * *

This pass is not on by default because it is not safe in simple mode. If the prototype method * is mutated and we don't detect that, inlining it is unsafe. We enable it whenever function * inlining is enabled. */ class InlineSimpleMethods implements CompilerPass { /** List of methods defined in externs */ final ImmutableSet externProperties; /** List of property names that may not be methods */ final Set nonMethodProperties = new HashSet<>(); // Use a linked map here to keep the output deterministic. Otherwise, // the choice of method bodies is random when multiple identical definitions // are found which causes problems in the source maps. final SetMultimap methodDefinitions = LinkedHashMultimap.create(); private final AbstractCompiler compiler; private static final Logger logger = Logger.getLogger(InlineSimpleMethods.class.getName()); private final AstAnalyzer astAnalyzer; InlineSimpleMethods(AbstractCompiler compiler) { this.compiler = compiler; astAnalyzer = compiler.getAstAnalyzer(); externProperties = compiler.getExternProperties(); } @Override public void process(Node externs, Node root) { checkState(compiler.getLifeCycleStage().isNormalized(), compiler.getLifeCycleStage()); checkState(methodDefinitions.isEmpty()); checkState(externs != null); NodeTraversal.traverseRoots(compiler, new GatherSignatures(), externs, root); NodeTraversal.traverseRoots(compiler, new InlineTrivialAccessors(), externs, root); } /** * For each method call, see if it is a candidate for inlining. * TODO(kushal): Cache the results of the checks */ private class InlineTrivialAccessors extends InvocationsCallback { @Override void visit(NodeTraversal t, Node callNode, Node parent, String callName) { if (externProperties.contains(callName) || nonMethodProperties.contains(callName)) { return; } Set definitions = methodDefinitions.get(callName); if (definitions == null || definitions.isEmpty()) { return; } // Do check of arity, complexity, and consistency in what we think is // the order from least to most complex Node firstDefinition = definitions.iterator().next(); // Check any multiple definitions if (definitions.size() == 1 || allDefinitionsEquivalent(definitions)) { // Do not inline if the callsite is a derived class calling a base method using `super.` if (!argsMayHaveSideEffects(callNode) && !NodeUtil.referencesSuper(callNode)) { // Verify this is a trivial return Node returned = returnedExpression(firstDefinition); if (returned != null) { if (isPropertyTree(returned) && !firstDefinition.isArrowFunction()) { if (logger.isLoggable(Level.FINE)) { logger.fine("Inlining property accessor: " + callName); } inlinePropertyReturn(callNode, returned); } else if (NodeUtil.isLiteralValue(returned, false) && !astAnalyzer.mayHaveSideEffects(callNode.getFirstChild())) { if (logger.isLoggable(Level.FINE)) { logger.fine("Inlining constant accessor: " + callName); } inlineConstReturn(callNode, returned); } } else if (isEmptyMethod(firstDefinition) && !astAnalyzer.mayHaveSideEffects(callNode.getFirstChild())) { if (logger.isLoggable(Level.FINE)) { logger.fine("Inlining empty method: " + callName); } inlineEmptyMethod(t, parent, callNode); } } } else { if (logger.isLoggable(Level.FINE)) { logger.fine("Method '" + callName + "' has conflicting definitions."); } } } } /** * Returns true if the provided node is a getprop for * which the left child is this or a valid property tree * and for which the right side is a string. */ private static boolean isPropertyTree(Node expectedGetprop) { if (!expectedGetprop.isGetProp()) { return false; } Node getpropLhs = expectedGetprop.getFirstChild(); return getpropLhs.isThis() || isPropertyTree(getpropLhs); } /** * Finds the occurrence of "this" in the provided property tree and replaces * it with replacement */ private static void replaceThis(Node expectedGetprop, Node replacement) { Node leftChild = expectedGetprop.getFirstChild(); if (leftChild.isThis()) { leftChild.replaceWith(replacement); } else { replaceThis(leftChild, replacement); } } /** * Return the node that represents the expression returned by the method, given a FUNCTION node. */ private static @Nullable Node returnedExpression(Node fn) { Node expectedBlock = NodeUtil.getFunctionBody(fn); if (!expectedBlock.hasOneChild()) { return null; } Node expectedReturn = expectedBlock.getFirstChild(); if (!expectedReturn.isReturn()) { return null; } if (!expectedReturn.hasOneChild()) { return null; } return expectedReturn.getOnlyChild(); } /** * Return whether the given FUNCTION node is an empty method definition. * * Must be private, or moved to NodeUtil. */ private static boolean isEmptyMethod(Node fn) { return NodeUtil.isEmptyBlock(NodeUtil.getFunctionBody(fn)); } /** Given a set of method definitions, verify they are the same. */ private boolean allDefinitionsEquivalent(Set definitions) { Node first = null; for (Node n : definitions) { if (first == null) { first = n; } else if (!compiler.areNodesEqualForInlining(first, n)) { return false; } // else continue } return true; } /** * Replace the provided method call with the tree specified in returnedValue * *

Parse tree of a call is name call getprop obj string */ private void inlinePropertyReturn(Node call, Node returnedValue) { Node getProp = returnedValue.cloneTree(); replaceThis(getProp, call.getFirstChild().removeFirstChild()); call.replaceWith(getProp); compiler.reportChangeToEnclosingScope(getProp); } /** * Replace the provided object and its method call with the tree specified in returnedValue. * Should be called only if the object reference has no side effects. */ private void inlineConstReturn(Node call, Node returnedValue) { Node retValue = returnedValue.cloneTree(); call.replaceWith(retValue); compiler.reportChangeToEnclosingScope(retValue); } /** * Remove the provided object and its method call. */ private void inlineEmptyMethod(NodeTraversal t, Node parent, Node call) { // If the return value of the method call is read, // replace it with "void 0". Otherwise, remove the call entirely. if (NodeUtil.isExprCall(parent)) { parent.replaceWith(IR.empty()); NodeUtil.markFunctionsDeleted(parent, compiler); } else { Node srcLocation = call; call.replaceWith(NodeUtil.newUndefinedNode(srcLocation)); NodeUtil.markFunctionsDeleted(call, compiler); } t.reportCodeChange(); } /** * Check whether the given method call's arguments have side effects. * @param call The call node of a method invocation. */ private boolean argsMayHaveSideEffects(Node call) { for (Node currentChild = call.getSecondChild(); currentChild != null; currentChild = currentChild.getNext()) { if (astAnalyzer.mayHaveSideEffects(currentChild)) { return true; } } return false; } /** * Adds a node that may represent a function signature (if it's a function itself or the name of a * function). */ private void addPossibleSignature(String name, Node node) { if (node != null && node.isFunction()) { // The node we're looking at is a function, so we can add it directly addSignature(name, node); } else { nonMethodProperties.add(name); } } private void addSignature(String name, Node function) { if (externProperties.contains(name)) { return; } methodDefinitions.put(name, function); } /** Gather signatures from the source to be compiled. */ private class GatherSignatures extends AbstractPostOrderCallback { @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { // TODO(b/251573710): Handle ES2015+ language features that can reference properties case GETPROP: case GETELEM: String name = getPropName(n); if (name == null) { return; } if (name.equals("prototype")) { processPrototypeParent(parent); } else { // Static methods of the form Foo.bar = function() {} or // Static methods of the form Foo.bar = baz (where baz is a // function name). Parse tree looks like: // assign <- parent // getprop <- n // name Foo // string bar // function or name <- n.getNext() if (parent.isAssign() && n.isFirstChildOf(parent)) { addPossibleSignature(name, n.getNext()); } } break; case OBJECTLIT: case CLASS_MEMBERS: for (Node key = n.getFirstChild(); key != null; key = key.getNext()) { switch (key.getToken()) { case MEMBER_FUNCTION_DEF: case MEMBER_FIELD_DEF: case STRING_KEY: addPossibleSignature(key.getString(), key.getFirstChild()); break; case SETTER_DEF: case GETTER_DEF: nonMethodProperties.add(key.getString()); break; case COMPUTED_PROP: // complicated case OBJECT_SPREAD: case COMPUTED_FIELD_DEF: break; default: throw new IllegalStateException("Unexpected " + n.getToken() + " key: " + key); } } break; default: break; } } /** * Processes the parent of a GETPROP prototype, which can either be another GETPROP (in the case * of Foo.prototype.bar), or can be an assignment (in the case of Foo.prototype = ...). */ private void processPrototypeParent(Node n) { switch (n.getToken()) { // Foo.prototype.getBar = function() { ... } or // Foo.prototype.getBar = getBaz (where getBaz is a function) // parse tree looks like: // assign <- parent // getprop <- n // getprop // name Foo // string prototype // string getBar // function or name <- assignee case GETPROP: case GETELEM: Node grandparent = n.getGrandparent(); String name = getPropName(n); if (name != null && grandparent.isAssign()) { Node assignee = grandparent.getSecondChild(); addPossibleSignature(name, assignee); } break; default: break; } } } private static @Nullable String getPropName(Node getPropElem) { if (getPropElem.isGetProp()) { return getPropElem.getString(); } else if (getPropElem.getSecondChild().isStringLit()) { return getPropElem.getSecondChild().getString(); } else { return null; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy