/* * 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.javascript.jscomp.ControlFlowGraph.Branch; import com.google.javascript.jscomp.DefinitionsRemover.Definition; import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback; import com.google.javascript.jscomp.NodeTraversal.ScopedCallback; import com.google.javascript.jscomp.graph.DiGraph.DiGraphEdge; import com.google.javascript.rhino.Node; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Chain calls to functions that return this. * */ class ChainCalls implements CompilerPass { private final AbstractCompiler compiler; private final Set<Node> badFunctionNodes = new HashSet<>(); private final Set<Node> goodFunctionNodes = new HashSet<>(); private final List<CallSite> callSites = new ArrayList<>(); private NameBasedDefinitionProvider defFinder; private GatherFunctions gatherFunctions = new GatherFunctions(); ChainCalls(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { defFinder = new NameBasedDefinitionProvider(compiler, false); defFinder.process(externs, root); NodeTraversal.traverseEs6(compiler, root, new GatherCallSites()); for (CallSite callSite : callSites) { callSite.parent.removeChild(callSite.n); callSite.n.removeChild(callSite.callNode); callSite.nextGetPropNode.replaceChild(callSite.nextGetPropFirstChildNode, callSite.callNode); compiler.reportChangeToEnclosingScope(callSite.parent); } } /** * Determines whether a function always returns this. */ private class GatherFunctions implements ScopedCallback { @Override public void enterScope(NodeTraversal t) { ControlFlowGraph<Node> cfg = t.getControlFlowGraph(); for (DiGraphEdge<Node, Branch> s : cfg.getImplicitReturn().getInEdges()) { Node exitNode = s.getSource().getValue(); if (!exitNode.isReturn() || exitNode.getFirstChild() == null || !exitNode.getFirstChild().isThis()) { badFunctionNodes.add(t.getScopeRoot()); return; } } goodFunctionNodes.add(t.getScopeRoot()); } @Override public void exitScope(NodeTraversal t) { } @Override public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) { return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { } } private class GatherCallSites extends AbstractPostOrderCallback { /** * If the function call returns this and the next statement has the same * target expression, record the call site. */ @Override public void visit(NodeTraversal t, Node n, Node parent) { if (!n.isExprResult()) { return; } Node callNode = n.getFirstChild(); if (!callNode.isCall()) { return; } Node getPropNode = callNode.getFirstChild(); if (!getPropNode.isGetProp()) { return; } Node getPropFirstChildNode = getPropNode.getFirstChild(); Collection<Definition> definitions = defFinder.getDefinitionsReferencedAt(getPropNode); if (definitions == null) { return; } for (Definition definition : definitions) { Node rValue = definition.getRValue(); if (rValue == null) { return; } if (badFunctionNodes.contains(rValue)) { return; } if (!goodFunctionNodes.contains(rValue)) { new NodeTraversal(compiler, gatherFunctions, new Es6SyntacticScopeCreator(compiler)) .traverseInnerNode( rValue, rValue.getParent(), t.getClosestHoistScope().getParent()); if (badFunctionNodes.contains(rValue)) { return; } } } Node nextNode = n.getNext(); if (nextNode == null || !nextNode.isExprResult()) { return; } Node nextCallNode = nextNode.getFirstChild(); if (!nextCallNode.isCall()) { return; } Node nextGetPropNode = nextCallNode.getFirstChild(); if (!nextGetPropNode.isGetProp()) { return; } Node nextGetPropFirstChildNode = nextGetPropNode.getFirstChild(); if (!compiler.areNodesEqualForInlining( nextGetPropFirstChildNode, getPropFirstChildNode)) { return; } if (NodeUtil.mayEffectMutableState(getPropFirstChildNode)) { return; } // We can't chain immediately as it we wouldn't recognize further // opportunities to chain. callSites.add(new CallSite(parent, n, callNode, nextGetPropNode, nextGetPropFirstChildNode)); } } /** Records a call site to chain. */ private static class CallSite { final Node parent; final Node n; final Node callNode; final Node nextGetPropNode; final Node nextGetPropFirstChildNode; CallSite(Node parent, Node n, Node callNode, Node nextGetPropNode, Node nextGetPropFirstChildNode) { this.parent = parent; this.n = n; this.callNode = callNode; this.nextGetPropNode = nextGetPropNode; this.nextGetPropFirstChildNode = nextGetPropFirstChildNode; } } }