com.google.javascript.jscomp.ShadowVariables Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler-unshaded Show documentation
Show all versions of closure-compiler-unshaded Show documentation
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.
/*
* Copyright 2011 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.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.ScopedCallback;
import com.google.javascript.jscomp.RenameVars.Assignment;
import com.google.javascript.rhino.Node;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.SortedSet;
/**
* Tries to compute a list of variables that can shadow a variable in the
* outer scope.
*
* For example:
*
*
* var a = function() {
* var b = getB();
* b();
* return function(y) {};
* };
*
*
* Normally, b would be mapped to variable L0, y would be L1.
*
* Instead we are going to make y shadows L0 in hope of using less variables
* and reusing frequently used local names.
*
*/
class ShadowVariables implements CompilerPass {
// Keep a map of Upward Referencing name nodes of each scope.
// A name is upward referencing name of a scope if:
//
// 1) It refers to (or defines) a name that is defined in the current
// scope or any scope above the current scope that isn't the
// global scope.
//
// 2) It is a upward referencing name of a child scope of this scope.
//
// Example:
// var x; var y; function foo(a) { function bar(b) { x, a } }
// The upward referencing names in scope 'foo' is bar, b, x and a;
// The key to this map is the root node of the scope.
//
// We can see that for any variable x in the current scope, we can shadow
// a variable y in an outer scope given that y is not a upward referencing
// name of the current scope.
// TODO(user): Maps scope to string instead of Node to string.
// Make sure of scope memorization to minimize scope creation cost.
private final Multimap scopeUpRefMap = HashMultimap.create();
// Maps each local variable to all of its referencing NAME nodes in any scope.
private final Multimap varToNameUsage = HashMultimap.create();
private final AbstractCompiler compiler;
// All the information used for renaming.
private final SortedSet varsByFrequency;
private final Map assignments;
private final Map oldPseudoNameMap;
private final Map deltaPseudoNameMap;
/**
* @param assignments Map of old variable names to its assignment Objects.
* @param varsByFrequency Sorted variable assignments by Frequency.
* @param pseudoNameMap The current pseudo name map so this pass can update
* it accordingly.
*/
ShadowVariables(
AbstractCompiler compiler,
Map assignments,
SortedSet varsByFrequency,
Map pseudoNameMap) {
this.compiler = compiler;
this.assignments = assignments;
this.varsByFrequency = varsByFrequency;
this.oldPseudoNameMap = pseudoNameMap;
this.deltaPseudoNameMap = new LinkedHashMap<>();
}
@Override
public void process(Node externs, Node root) {
// The algorithm is divided into two stages:
//
// 1. Information gathering (variable usage, upward referencing)
//
// 2. Tries to find shadows for each variables, updates the
// variable usage frequency map.
//
// 3. Updates the pseudo naming map if needed.
NodeTraversal.traverse(compiler, root, new GatherReferenceInfo());
NodeTraversal.traverse(compiler, root, new DoShadowVariables());
if (oldPseudoNameMap != null) {
oldPseudoNameMap.putAll(deltaPseudoNameMap);
}
}
private class GatherReferenceInfo extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
// Skipping over non-name nodes and empty function names.
if (!NodeUtil.isReferenceName(n)) {
return;
}
// We focus on shadowing local variables as their name occurs much more
// than global names.
// TODO(user): Alternatively, we could experiment with using a local
// name to shadow a global variable.
if (t.inGlobalScope()) {
return;
}
Scope scope = t.getScope();
Var var = scope.getVar(n.getString());
if (var == null) {
// extern name or undefined name.
return;
}
if (var.getScope().isGlobal()) {
// We will not shadow a global variable name.
return;
}
// Using the definition of upward referencing, fill in the map.
if (var.getScope() != scope) {
for (Scope s = scope; s != var.getScope() && s.isLocal(); s = s.getParent()) {
scopeUpRefMap.put(s.getRootNode(), var.name);
}
} else {
scopeUpRefMap.put(t.getScopeRoot(), var.name);
}
// Make sure that we don't shadow function parameters or function names from a function block
// scope, eg.:
// function f(a) { ... var a; ... } // Unsafe
if (scope.isFunctionScope() && var.getScope() == scope) {
scopeUpRefMap.put(scope.getRootNode().getLastChild(), var.name);
}
// Find in the usage map that tracks a var and all of its usage.
varToNameUsage.put(var, new Reference(n, scope));
}
}
private class DoShadowVariables extends AbstractPostOrderCallback
implements ScopedCallback {
@Override
public void enterScope(NodeTraversal t) {
if (t.inGlobalScope()) {
return;
}
// Since we don't shadow global, there is nothing to be done in the
// first immediate local scope as well.
Node scopeRoot = t.getScopeRoot();
if ((scopeRoot.isFunction()
&& NodeUtil.getEnclosingFunction(scopeRoot.getParent()) == null)
|| (NodeUtil.isFunctionBlock(scopeRoot)
&& NodeUtil.getEnclosingFunction(scopeRoot.getGrandparent()) == null)) {
return;
}
Scope s = t.getScope();
for (Var var : s.getVarIterable()) {
// Don't shadow variables that are bleed-out functions or caught exceptions to workaround
// IE8 bugs.
// TODO(moz): Gate this behind languageMode=ES3.
if (var.isBleedingFunction() || var.isCatch()) {
continue;
}
// Don't shadow an exported local.
if (compiler.getCodingConvention().isExported(var.name, s.isLocal())) {
continue;
}
// The name assignment being shadowed.
Assignment localAssignment = assignments.get(var.getName());
if (localAssignment == null) {
continue;
}
// Try to run this check last as it is more expensive than the above checks.
// Try to look for the best shadow for the current candidate.
Assignment bestShadow = findBestShadow(s, var);
if (bestShadow == null) {
continue;
}
// Only shadow if this increases the number of occurrences of the
// shadowed variable.
if (bestShadow.count < localAssignment.count) {
continue; // Hope the next local variable would have a smaller count.
}
doShadow(localAssignment, bestShadow, var);
if (oldPseudoNameMap != null) {
String targetPseudoName =
oldPseudoNameMap.get(s.getVar(bestShadow.oldName).nameNode);
for (Reference use : varToNameUsage.get(var)) {
deltaPseudoNameMap.put(use.nameNode, targetPseudoName);
}
}
}
}
@Override
public void exitScope(NodeTraversal t) {}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {}
/**
* @return An assignment that can be used as a shadow for a local variable
* in the scope defined by curScopeRoot.
*/
private Assignment findBestShadow(Scope curScope, Var var) {
// Search for the candidate starting from the most used local.
for (Assignment assignment : varsByFrequency) {
if (assignment.isLocal) {
if (!scopeUpRefMap.containsEntry(curScope.getRootNode(), assignment.oldName)) {
if (curScope.hasSlot(assignment.oldName)) {
// Don't shadow if the scopes are the same eg.:
// function f() { var a = 1; { var a = 2; } } // Unsafe
Var toShadow = curScope.getVar(assignment.oldName);
if (var.getScope() != toShadow.getScope()) {
return assignment;
}
}
}
}
}
return null;
}
private void doShadow(Assignment original, Assignment toShadow, Var var) {
Scope s = var.getScope();
// We are now shadowing 'bestShadow' with localAssignment.
// All of the reference NAME node of this variable.
Collection references = varToNameUsage.get(var);
// First remove both assignments from the sorted list since they need
// to be re-sorted.
varsByFrequency.remove(original);
varsByFrequency.remove(toShadow);
// Adjust the count offset by the inner scope variable.
original.count -= references.size();
toShadow.count += references.size();
// Add it back to the sorted list after re-adjustment.
varsByFrequency.add(original);
varsByFrequency.add(toShadow);
// This is an important step. If variable L7 is going to be renamed to
// L1, by definition of upward referencing, The name L1 is now in the
// set of upward referencing names of the current scope up to the
// declaring scope of the best shadow variable.
Var shadowed = s.getVar(toShadow.oldName);
if (shadowed != null) {
if (s.isFunctionScope() && s.getRootNode().getLastChild().isBlock()) {
scopeUpRefMap.put(s.getRootNode().getLastChild(), toShadow.oldName);
scopeUpRefMap.remove(s.getRootNode().getLastChild(), original.oldName);
}
for (Scope curScope = s; curScope != shadowed.scope; curScope = curScope.getParent()) {
scopeUpRefMap.put(curScope.getRootNode(), toShadow.oldName);
scopeUpRefMap.remove(curScope.getRootNode(), original.oldName);
}
}
// Mark all the references as shadowed.
for (Reference ref : references) {
Node n = ref.nameNode;
n.setString(toShadow.oldName);
if (ref.scope.getRootNode() == s.getRootNode()) {
if (var.getNameNode() != ref.nameNode) {
scopeUpRefMap.put(s.getRootNode(), toShadow.oldName);
scopeUpRefMap.remove(s.getRootNode(), original.oldName);
}
} else {
for (Scope curScope = ref.scope;
curScope.getRootNode() != s.getRootNode();
curScope = curScope.getParent()) {
scopeUpRefMap.put(curScope.getRootNode(), toShadow.oldName);
scopeUpRefMap.remove(curScope.getRootNode(), original.oldName);
}
}
}
}
}
private static final class Reference {
private final Node nameNode;
private final Scope scope;
private Reference(Node nameNode, Scope scope) {
this.nameNode = nameNode;
this.scope = scope;
}
}
}