/* * Copyright 2006 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.common.collect.ImmutableSet; import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback; import com.google.javascript.rhino.JSDocInfo; import com.google.javascript.rhino.JSDocInfoBuilder; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Token; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Collapses multiple variable declarations into a single one. i.e the * following: * * <pre> * var a; * var b = 1; * var c = 2; * </pre> * * becomes: * * <pre>var a, b = 1, c = 2;</pre> * * This reduces the generated code size. More optimizations are possible: * <li>Group all variable declarations inside a function into one such variable. * declaration block.</li> * <li>Re-use variables instead of declaring a new one if they are used for * only part of a function.</li> * * Similarly, also collapses assigns like: * * <pre> * a = true; * b = true; * var c = true; * </pre> * * becomes: * * <pre>var c = b = a = true;</pre> * * @author nicksantos@google.com (Nick Santos) */ class CollapseVariableDeclarations implements CompilerPass { /** Reference to JS Compiler */ private final AbstractCompiler compiler; /** Encapsulation of information about a variable declaration collapse */ private static class Collapse { /** * Variable declaration that any following var nodes should be * collapsed into */ final Node startNode; /** * Last node (non-inclusive) of the chain of nodes to collapse. */ final Node endNode; /** Parent of the nodes to the collapse */ final Node parent; Collapse(Node startNode, Node endNode, Node parent) { this.startNode = startNode; this.endNode = endNode; this.parent = parent; } } /** * Collapses to do in this pass. */ private final List<Collapse> collapses = new ArrayList<>(); /** * Nodes we've already looked at for collapsing, so that we don't look at them * again (we look ahead when examining what nodes can be collapsed, and the * node traversal may give them to us again) */ private final Set<Node> nodesToCollapse = new HashSet<>(); CollapseVariableDeclarations(AbstractCompiler compiler) { Preconditions.checkState(!compiler.getLifeCycleStage().isNormalized()); this.compiler = compiler; } @Override public void process(Node externs, Node root) { collapses.clear(); nodesToCollapse.clear(); NodeTraversal.traverseEs6(compiler, root, new GatherCollapses()); if (!collapses.isEmpty()) { applyCollapses(); } } /** * Gathers all of the variable declarations / assignments that should be * collapsed into one. * * We do not do the collapsing as we go since node traversal would be affected * by the changes we are making to the parse tree. */ private class GatherCollapses extends AbstractPostOrderCallback { // If a VAR is declared like // var x; // then we should not create new VAR nodes for it later in the tree. // This is a workaround for a bug in Firefox. private final Set<Var> blacklistedVars = new HashSet<>(); @Override public void visit(NodeTraversal t, Node n, Node parent) { if (n.isVar()) { blacklistStubVars(t, n); } // Only care about var nodes if (!n.isVar() && !canBeRedeclared(n, t.getScope())) { return; } // If we've already looked at this node, skip it if (nodesToCollapse.contains(n)) { return; } // Adjacent VAR children of an IF node are the if and else parts and can't // be collapsed if (parent.isIf()) { return; } Node varNode = n; boolean hasVar = n.isVar(); // Find variable declarations that follow this one (if any) n = n.getNext(); boolean hasNodesToCollapse = false; while (n != null && (n.isVar() || canBeRedeclared(n, t.getScope()))) { if (n.isVar()) { blacklistStubVars(t, n); hasVar = true; } nodesToCollapse.add(n); hasNodesToCollapse = true; n = n.getNext(); } if (hasNodesToCollapse && hasVar) { nodesToCollapse.add(varNode); collapses.add(new Collapse(varNode, n, parent)); } } private void blacklistStubVars(NodeTraversal t, Node varNode) { for (Node child = varNode.getFirstChild(); child != null; child = child.getNext()) { if (child.getFirstChild() == null) { blacklistedVars.add(t.getScope().getVar(child.getString())); } } } private boolean canBeRedeclared(Node n, Scope s) { if (!NodeUtil.isExprAssign(n)) { return false; } Node assign = n.getFirstChild(); Node lhs = assign.getFirstChild(); if (!lhs.isName()) { return false; } Var var = s.getVar(lhs.getString()); return var != null && (NodeUtil.getEnclosingFunction(var.getScope().getRootNode()) == NodeUtil.getEnclosingFunction(s.getRootNode())) && !isNamedParameter(var) && !blacklistedVars.contains(var); } } private static boolean isNamedParameter(Var v) { return v.isParam(); } private void applyCollapses() { for (Collapse collapse : collapses) { Node var = new Node(Token.VAR); var.useSourceInfoIfMissingFrom(collapse.startNode); collapse.parent.addChildBefore(var, collapse.startNode); compiler.reportChangeToEnclosingScope(var); boolean redeclaration = false; for (Node n = collapse.startNode; n != collapse.endNode;) { Node next = n.getNext(); Preconditions.checkState(var.getNext() == n); collapse.parent.removeChild(var.getNext()); if (n.isVar()) { while (n.hasChildren()) { var.addChildToBack(n.removeFirstChild()); } } else { Node assign = n.getFirstChild(); Node lhs = assign.getFirstChild(); Preconditions.checkState(lhs.isName()); Node rhs = assign.getLastChild(); lhs.addChildToBack(rhs.detach()); var.addChildToBack(lhs.detach()); redeclaration = true; } n = next; } if (redeclaration) { // TODO(johnlenz): share the JSDocInfo here rather than building // a new one each time. JSDocInfoBuilder builder = new JSDocInfoBuilder(false); builder.recordSuppressions(ImmutableSet.of("duplicate")); JSDocInfo info = builder.build(); var.setJSDocInfo(info); } } } }