/* * 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 com.google.common.base.Preconditions; import com.google.javascript.jscomp.DefinitionsRemover.Definition; import com.google.javascript.rhino.FunctionTypeI; import com.google.javascript.rhino.IR; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.TypeI; import java.util.Collection; /** * Rewrites prototyped methods calls as static calls that take "this" * as their first argument. This transformation simplifies the call * graph so smart name removal, cross module code motion and other * passes can do more. * * <p>This pass should only be used in production code if property * and variable renaming are turned on. Resulting code may also * benefit from --collapse_anonymous_functions and * --collapse_variable_declarations * * <p>This pass only rewrites functions that are part of an objects * prototype. Functions that access the "arguments" variable * arguments object are not eligible for this optimization. * * <p>For example: * <pre> * A.prototype.accumulate = function(value) { * this.total += value; return this.total * } * var total = a.accumulate(2) * </pre> * * <p>will be rewritten as: * * <pre> * var accumulate = function(self, value) { * self.total += value; return self.total * } * var total = accumulate(a, 2) * </pre> * */ class DevirtualizePrototypeMethods implements OptimizeCalls.CallGraphCompilerPass, CompilerPass { private final AbstractCompiler compiler; DevirtualizePrototypeMethods(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { DefinitionUseSiteFinder defFinder = new DefinitionUseSiteFinder(compiler); defFinder.process(externs, root); process(externs, root, defFinder); } @Override public void process( Node externs, Node root, DefinitionUseSiteFinder definitions) { for (DefinitionSite defSite : definitions.getDefinitionSites()) { rewriteDefinitionIfEligible(defSite, definitions); } } /** * Determines if the name node acts as the function name in a call expression. */ private static boolean isCall(UseSite site) { Node node = site.node; Node parent = node.getParent(); if (parent == null) { return false; } return (parent.getFirstChild() == node) && parent.isCall(); } /** * Determines if the current node is a function prototype definition. */ private static boolean isPrototypeMethodDefinition(Node node) { Node parent = node.getParent(); if (parent == null) { return false; } Node gramp = parent.getParent(); if (gramp == null) { return false; } if (node.isGetProp()) { if (parent.getFirstChild() != node) { return false; } if (!NodeUtil.isExprAssign(gramp)) { return false; } Node functionNode = parent.getLastChild(); if ((functionNode == null) || !functionNode.isFunction()) { return false; } Node nameNode = node.getFirstChild(); return nameNode.isGetProp() && nameNode.getLastChild().getString().equals("prototype"); } else if (node.isStringKey()) { Preconditions.checkState(parent.isObjectLit()); if (!gramp.isAssign()) { return false; } if (gramp.getLastChild() != parent) { return false; } Node greatGramp = gramp.getParent(); if (greatGramp == null || !greatGramp.isExprResult()) { return false; } Node functionNode = node.getFirstChild(); if ((functionNode == null) || !functionNode.isFunction()) { return false; } Node target = gramp.getFirstChild(); return target.isGetProp() && target.getLastChild().getString().equals("prototype"); } else { return false; } } private static String getMethodName(Node node) { if (node.isGetProp()) { return node.getLastChild().getString(); } else if (node.isStringKey()) { return node.getString(); } else { throw new IllegalStateException("unexpected"); } } /** * @return The new name for a rewritten method. */ private static String getRewrittenMethodName(String originalMethodName) { return "JSCompiler_StaticMethods_" + originalMethodName; } /** * Rewrites method definition and call sites if the method is * defined in the global scope exactly once. * * Definition and use site information is provided by the * {@link DefinitionUseSiteFinder} passed in as an argument. * * @param defSite definition site to process. * @param defFinder structure that hold Node -> Definition and * Definition -> [UseSite] maps. */ private void rewriteDefinitionIfEligible(DefinitionSite defSite, DefinitionUseSiteFinder defFinder) { if (defSite.inExterns || !defSite.inGlobalScope || !isEligibleDefinition(defFinder, defSite)) { return; } Node node = defSite.node; if (!isPrototypeMethodDefinition(node)) { return; } for (Node ancestor = node.getParent(); ancestor != null; ancestor = ancestor.getParent()) { if (NodeUtil.isControlStructure(ancestor)) { return; } } // TODO(user) The code only works if there is a single definition associated with a property // name. // Whatever scheme we use should not break stable renaming. String newMethodName = getRewrittenMethodName( getMethodName(node)); rewriteDefinition(node, newMethodName); rewriteCallSites(defFinder, defSite.definition, newMethodName); } /** * Determines if a method definition is eligible for rewrite as a * global function. In order to be eligible for rewrite, the * definition must: * * - Refer to a function that takes a fixed number of arguments. * - Function must not be exported. * - Function must be used at least once. * - Property is never accessed outside a function call context. * - The definition under consideration must be the only possible * choice at each call site. * - Definition must happen in a module loaded before the first use. */ private boolean isEligibleDefinition(DefinitionUseSiteFinder defFinder, DefinitionSite definitionSite) { Definition definition = definitionSite.definition; JSModule definitionModule = definitionSite.module; // Only functions may be rewritten. // Functions that access "arguments" are not eligible since // rewrite changes the structure of this object. Node rValue = definition.getRValue(); if (rValue == null || !rValue.isFunction() || NodeUtil.isVarArgsFunction(rValue)) { return false; } Node lValue = definition.getLValue(); if ((lValue == null) || !lValue.isGetProp()) { return false; } // Note: the definition for prototype defined with an object literal returns // a mock return LValue of the form "{}.prop". if (!lValue.isQualifiedName() && !lValue.getFirstChild().isObjectLit()) { return false; } // Exporting a method prevents rewrite. CodingConvention codingConvention = compiler.getCodingConvention(); if (codingConvention.isExported(lValue.getLastChild().getString())) { return false; } Collection<UseSite> useSites = defFinder.getUseSites(definition); // Rewriting unused methods is not sound. if (useSites.isEmpty()) { return false; } JSModuleGraph moduleGraph = compiler.getModuleGraph(); for (UseSite site : useSites) { // Accessing the property directly prevents rewrite. if (!isCall(site)) { return false; } Node nameNode = site.node; // Multiple definitions prevent rewrite. Collection<Definition> singleSiteDefinitions = defFinder.getDefinitionsReferencedAt(nameNode); if (!allDefinitionsEquivalent(singleSiteDefinitions)) { return false; } Preconditions.checkState(!singleSiteDefinitions.isEmpty()); Preconditions.checkState(singleSiteDefinitions.contains(definition)); // Accessing the property in a module loaded before the // definition module prevents rewrite; accessing a variable // before definition results in a parse error. JSModule callModule = site.module; if ((definitionModule != callModule) && ((callModule == null) || !moduleGraph.dependsOn(callModule, definitionModule))) { return false; } } return true; } /** Given a set of method definitions, verify they are the same. */ boolean allDefinitionsEquivalent(Collection<Definition> definitions) { if (definitions.size() <= 1) { return true; } Definition first = null; for (Definition definition : definitions) { if (definition.getRValue() == null) { return false; // We can't tell if they're all the same. } if (first == null) { first = definition; continue; } if (!compiler.areNodesEqualForInlining(first.getRValue(), definition.getRValue())) { return false; } } return true; } /** * Rewrites object method call sites as calls to global functions * that take "this" as their first argument. * * Before: * o.foo(a, b, c) * * After: * foo(o, a, b, c) */ private void rewriteCallSites(DefinitionUseSiteFinder defFinder, Definition definition, String newMethodName) { Collection<UseSite> useSites = defFinder.getUseSites(definition); for (UseSite site : useSites) { Node node = site.node; Node parent = node.getParent(); Node objectNode = node.getFirstChild(); node.removeChild(objectNode); parent.replaceChild(node, objectNode); parent.addChildToFront(IR.name(newMethodName).srcref(node)); Preconditions.checkState(parent.isCall()); parent.putBooleanProp(Node.FREE_CALL, true); compiler.reportChangeToEnclosingScope(parent); } } /** * Rewrites method definitions as global functions that take "this" * as their first argument. * * Before: * a.prototype.b = function(a, b, c) {...} * * After: * var b = function(self, a, b, c) {...} */ private void rewriteDefinition(Node node, String newMethodName) { boolean isObjLitDefKey = node.isStringKey(); Node parent = node.getParent(); Node refNode = isObjLitDefKey ? node : parent.getFirstChild(); Node newNameNode = IR.name(newMethodName).useSourceInfoIfMissingFrom(refNode); Node newVarNode = IR.var(newNameNode).useSourceInfoIfMissingFrom(refNode); Node functionNode; if (!isObjLitDefKey) { Preconditions.checkState(parent.isAssign()); functionNode = parent.getLastChild(); Node expr = parent.getParent(); Node block = expr.getParent(); parent.removeChild(functionNode); newNameNode.addChildToFront(functionNode); block.replaceChild(expr, newVarNode); } else { Preconditions.checkState(parent.isObjectLit()); functionNode = node.getFirstChild(); Node assign = parent.getParent(); Node expr = assign.getParent(); Node block = expr.getParent(); node.removeChild(functionNode); parent.removeChild(node); newNameNode.addChildToFront(functionNode); block.addChildAfter(newVarNode, expr); } compiler.reportChangeToEnclosingScope(newVarNode); // add extra argument String self = newMethodName + "$self"; Node argList = functionNode.getSecondChild(); argList.addChildToFront(IR.name(self) .useSourceInfoIfMissingFrom(functionNode)); compiler.reportChangeToEnclosingScope(argList); // rewrite body Node body = functionNode.getLastChild(); if (replaceReferencesToThis(body, self)) { compiler.reportChangeToEnclosingScope(body); } // fix type fixFunctionType(functionNode); } /** * Creates a new type based on the original function type by * adding the original this pointer type to the beginning of the * argument type list and replacing the this pointer type with bottom. */ private void fixFunctionType(Node functionNode) { TypeI t = functionNode.getTypeI(); if (t == null) { return; } FunctionTypeI ft = t.toMaybeFunctionType(); if (ft != null) { functionNode.setTypeI(ft.convertMethodToFunction()); } } /** * Replaces references to "this" with references to name. Do not * traverse function boundaries. */ private static boolean replaceReferencesToThis(Node node, String name) { if (node.isFunction()) { return false; } boolean changed = false; for (Node child : node.children()) { if (child.isThis()) { Node newName = IR.name(name); newName.setTypeI(child.getTypeI()); node.replaceChild(child, newName); changed = true; } else { changed |= replaceReferencesToThis(child, name); } } return changed; } }