com.google.javascript.jscomp.RenameVars Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler Show documentation
Show all versions of closure-compiler 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.
The newest version!
/*
* Copyright 2004 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.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.nullToEmpty;
import static java.util.Comparator.comparingInt;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.ScopedCallback;
import com.google.javascript.rhino.Node;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.jspecify.nullness.Nullable;
/**
* RenameVars renames all the variables names into short names, to reduce code
* size and also to obfuscate the code.
*/
final class RenameVars implements CompilerPass {
/**
* Limit on number of locals in a scope for temporary local renaming
* when {@code preferStableNames} is true.
*/
private static final int MAX_LOCALS_IN_SCOPE_TO_TEMP_RENAME = 1000;
private final AbstractCompiler compiler;
/** List of global NAME nodes */
private final ArrayList globalNameNodes = new ArrayList<>();
/** List of local NAME nodes */
private final ArrayList localNameNodes = new ArrayList<>();
/** Mapping of original names for change detection */
private final Map originalNameByNode = new LinkedHashMap<>();
/**
* Maps a name node to its pseudo name, null if we are not generating so there will be no overhead
* unless we are debugging.
*/
private final @Nullable Map pseudoNameMap;
/** Set of extern variable names */
private ImmutableSet externNames;
/** Set of reserved variable names */
private final Set reservedNames;
/** The renaming map */
private final Map renameMap = new LinkedHashMap<>();
/** The previously used rename map. */
private final VariableMap prevUsedRenameMap;
/** The global name prefix */
private final String prefix;
/** Counter for each assignment */
private int assignmentCount = 0;
// Logic for bleeding functions, where the name leaks into the outer
// scope on IE but not on other browsers.
private final Set localBleedingFunctions = new LinkedHashSet<>();
private final ListMultimap localBleedingFunctionsPerScope =
ArrayListMultimap.create();
class Assignment {
final boolean isLocal;
final String oldName;
final int orderOfOccurrence;
@Nullable String newName;
int count; // Number of times this is referenced
Assignment(String name) {
this.isLocal = name.startsWith(LOCAL_VAR_PREFIX);
this.oldName = name;
this.newName = null;
this.count = 0;
// Represents the order at which a symbol appears in the source.
this.orderOfOccurrence = assignmentCount++;
}
/**
* Assigns the new name.
*/
void setNewName(String newName) {
checkState(this.newName == null);
this.newName = newName;
}
}
/** Maps an old name to a new name assignment */
private final Map assignments = new LinkedHashMap<>();
/** Whether renaming should apply to local variables only. */
private final boolean localRenamingOnly;
private final boolean preferStableNames;
/** Characters that shouldn't be used in variable names. */
private final char[] reservedCharacters;
/** A prefix to distinguish temporary local names from global names */
private static final String LOCAL_VAR_PREFIX = "L ";
// Shared name generator
private final NameGenerator nameGenerator;
/*
* nameGenerator is a shared NameGenerator that this instance can use;
* the instance may reset or reconfigure it, so the caller should
* not expect any state to be preserved.
*/
RenameVars(
AbstractCompiler compiler,
String prefix,
boolean localRenamingOnly,
boolean generatePseudoNames,
boolean preferStableNames,
VariableMap prevUsedRenameMap,
char @Nullable [] reservedCharacters,
@Nullable Set reservedNames,
NameGenerator nameGenerator) {
this.compiler = compiler;
this.prefix = nullToEmpty(prefix);
this.localRenamingOnly = localRenamingOnly;
if (generatePseudoNames) {
this.pseudoNameMap = new LinkedHashMap<>();
} else {
this.pseudoNameMap = null;
}
this.prevUsedRenameMap = prevUsedRenameMap;
this.reservedCharacters = reservedCharacters;
this.preferStableNames = preferStableNames;
if (reservedNames == null) {
this.reservedNames = new LinkedHashSet<>();
} else {
this.reservedNames = new LinkedHashSet<>(reservedNames);
}
this.nameGenerator = nameGenerator;
}
/**
* Iterate through the nodes, collect all the NAME nodes that need to be
* renamed, and count how many times each variable name is referenced.
*
* Keep track of all name references in globalNameNodes, and localNameNodes.
*
* To get shorter local variable renaming, we rename local variables to a
* temporary name "LOCAL_VAR_PREFIX + index" where index is the index of the
* variable declared in the local scope stack.
* e.g.
* Foo(fa, fb) {
* var c = function(d, e) { return fa; }
* }
* The indexes are: fa:0, fb:1, c:2, d:3, e:4
*
* In that way, local variable names are reused in each global function.
* e.g. the final code might look like
* function x(a,b) { ... }
* function y(a,b,c) { ... }
*/
class ProcessVars extends AbstractPostOrderCallback implements ScopedCallback {
@Override
public void enterScope(NodeTraversal t) {
if (t.inGlobalHoistScope() || !shouldTemporarilyRenameLocalsInScope(t.getScope())) {
return;
}
Scope scope = t.getScope();
for (Var current : scope.getVarIterable()) {
if (current.isBleedingFunction()) {
localBleedingFunctions.add(current);
localBleedingFunctionsPerScope.put(
scope.getParent(), current);
}
}
}
@Override
public void exitScope(NodeTraversal t) {}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (!(n.isName() || n.isImportStar())) {
return;
}
String name = n.getString();
// Ignore anonymous functions and classes.
if (name.isEmpty()) {
return;
}
// "import {x as y} from 'm';"
// Skip x because it's not a variable in this scope.
if (parent.isImportSpec() && parent.hasTwoChildren() && parent.getFirstChild() == n) {
return;
}
// Is this local or Global?
// Bleeding functions should be treated as part of their outer
// scope, because IE has bugs in how it handles bleeding
// functions.
Var var = t.getScope().getVar(name);
boolean local =
var != null
&& var.isLocal()
&& (var.getScope().getParent().isLocal() || !var.isBleedingFunction());
// Never rename references to the arguments array
if (var != null && var.isArguments()) {
reservedNames.add(name);
return;
}
// Are we renaming global variables?
if (!local && localRenamingOnly) {
reservedNames.add(name);
return;
}
// Check if we can rename this.
if (!okToRenameVar(name, local)) {
if (local) {
// Blindly de-uniquify for the Prototype library for
// http://blickly.github.io/closure-compiler-issues/#103
String newName = MakeDeclaredNamesUnique.ContextualRenameInverter.getOriginalName(name);
if (!newName.equals(name)) {
n.setString(newName);
}
}
return;
}
if (pseudoNameMap != null) {
recordPseudoName(n);
}
if (local && shouldTemporarilyRenameLocalsInScope(var.getScope())) {
// Give local variables a temporary name based on the
// variable's index in the scope to enable name reuse across
// locals in independent scopes.
String tempName = LOCAL_VAR_PREFIX + getLocalVarIndex(var);
incCount(tempName);
localNameNodes.add(n);
// Remember the original string in a name before it's temporarily filled with an "L".
originalNameByNode.put(n, n.getString());
n.setString(tempName);
} else if (var != null) { // Not an extern
// If it's global, increment global count
incCount(name);
globalNameNodes.add(n);
}
}
// Increment count of an assignment
void incCount(String name) {
Assignment s = assignments.computeIfAbsent(name, Assignment::new);
s.count++;
}
}
/**
* Sorts Assignment objects by their count, breaking ties by their order of occurrence in the
* source to ensure a deterministic total ordering.
*/
private static int frequencyComparator(Assignment a1, Assignment a2) {
if (a1.count != a2.count) {
return a2.count - a1.count;
}
// Break a tie using the order in which the variable first appears in
// the source.
return ORDER_OF_OCCURRENCE_COMPARATOR.compare(a1, a2);
}
/** Sorts Assignment objects by the order the variable name first appears in the source. */
private static final Comparator ORDER_OF_OCCURRENCE_COMPARATOR =
comparingInt((Assignment arg) -> arg.orderOfOccurrence);
@Override
public void process(Node externs, Node root) {
this.externNames = NodeUtil.collectExternVariableNames(this.compiler, externs);
originalNameByNode.clear();
// Do variable reference counting.
NodeTraversal.traverse(compiler, root, new ProcessVars());
// Make sure that new names don't overlap with extern names.
reservedNames.addAll(externNames);
// Rename vars, sorted by frequency of occurrence to minimize code size.
SortedSet varsByFrequency = new TreeSet<>(RenameVars::frequencyComparator);
varsByFrequency.addAll(assignments.values());
// First try to reuse names from an earlier compilation.
if (prevUsedRenameMap != null) {
reusePreviouslyUsedVariableMap(varsByFrequency);
}
// Assign names, sorted by descending frequency to minimize code size.
assignNames(varsByFrequency);
// Rename the globals!
for (Node n : globalNameNodes) {
setNameAndReport(n, getNewGlobalName(n));
}
// Rename the locals!
for (Node n : localNameNodes) {
setNameAndReport(n, getNewLocalName(n));
}
}
private void setNameAndReport(Node n, @Nullable String newName) {
// A null newName, indicates it should not be renamed.
if (newName != null && !newName.equals(n.getString())) {
n.setString(newName);
// Only mark changes if the final name change is different than it was original before being
// filled with the "L" temporary name.
if (!newName.equals(originalNameByNode.get(n))) {
compiler.reportChangeToEnclosingScope(n);
Node parent = n.getParent();
if (parent.isFunction() && NodeUtil.isFunctionDeclaration(parent)) {
// If we are renaming a function declaration, make sure the containing scope
// has the opportunity to act on the change.
compiler.reportChangeToEnclosingScope(parent);
}
}
}
}
private @Nullable String getNewGlobalName(Node n) {
String oldName = n.getString();
Assignment a = assignments.get(oldName);
if (a.newName != null && !a.newName.equals(oldName)) {
if (pseudoNameMap != null) {
return pseudoNameMap.get(n);
}
return a.newName;
} else {
return null;
}
}
private @Nullable String getNewLocalName(Node n) {
String oldTempName = n.getString();
Assignment a = assignments.get(oldTempName);
if (!a.newName.equals(oldTempName)) {
if (pseudoNameMap != null) {
return pseudoNameMap.get(n);
}
return a.newName;
}
return null;
}
private void recordPseudoName(Node n) {
// Variable names should be in a different name space than
// property pseudo names.
pseudoNameMap.put(n, '$' + n.getString() + "$$");
}
/**
* Runs through the assignments and reuses as many names as possible from the previously used
* variable map. Updates reservedNames with the set of names that were reused.
*/
private void reusePreviouslyUsedVariableMap(SortedSet varsToRename) {
// If prevUsedRenameMap had duplicate values then this pass would be
// non-deterministic.
// In such a case, the following will throw an IllegalArgumentException.
checkNotNull(prevUsedRenameMap.getNewNameToOriginalNameMap());
for (Assignment a : varsToRename) {
String prevNewName = prevUsedRenameMap.lookupNewName(a.oldName);
if (prevNewName == null || reservedNames.contains(prevNewName)) {
continue;
}
if (a.isLocal
|| (!externNames.contains(a.oldName)
&& prevNewName.startsWith(prefix))) {
reservedNames.add(prevNewName);
finalizeNameAssignment(a, prevNewName);
}
}
}
/**
* Determines which new names to substitute for the original names.
*/
private void assignNames(SortedSet varsToRename) {
nameGenerator.reset(reservedNames, prefix, reservedCharacters);
NameGenerator globalNameGenerator = nameGenerator;
// Local variables never need a prefix.
// Also, we need to avoid conflicts between global and local variable
// names; we do this by having using the same generator (not two
// instances). The case where global variables have a prefix (and
// therefore we use two different generators) but a local variable name
// might nevertheless conflict with a global one is not handled.
NameGenerator localNameGenerator =
prefix.isEmpty()
? globalNameGenerator
: nameGenerator.clone(reservedNames, "", reservedCharacters);
// Generated names and the assignments for non-local vars.
List pendingAssignments = new ArrayList<>();
List generatedNamesForAssignments = new ArrayList<>();
for (Assignment a : varsToRename) {
if (a.newName != null) {
continue;
}
if (externNames.contains(a.oldName)) {
continue;
}
String newName;
if (a.isLocal) {
// For local variable, we make the assignment right away.
newName = localNameGenerator.generateNextName();
finalizeNameAssignment(a, newName);
} else {
// For non-local variable, delay finalizing the name assignment
// until we know how many new names we'll have of length 2, 3, etc.
newName = globalNameGenerator.generateNextName();
pendingAssignments.add(a);
generatedNamesForAssignments.add(newName);
}
reservedNames.add(newName);
}
// Now that we have a list of generated names, and a list of variable
// Assignment objects, we assign the generated names to the vars as
// follows:
// 1) The most frequent vars get the shorter names.
// 2) If N number of vars are going to be assigned names of the same
// length, we assign the N names based on the order at which the vars
// first appear in the source. This makes the output somewhat less
// random, because symbols declared close together are assigned names
// that are quite similar. With this heuristic, the output is more
// compressible.
// For instance, the output may look like:
// var da = "..", ea = "..";
// function fa() { .. } function ga() { .. }
int numPendingAssignments = generatedNamesForAssignments.size();
for (int i = 0; i < numPendingAssignments;) {
SortedSet varsByOrderOfOccurrence =
new TreeSet<>(ORDER_OF_OCCURRENCE_COMPARATOR);
// Add k number of Assignment to the set, where k is the number of
// generated names of the same length.
int len = generatedNamesForAssignments.get(i).length();
for (int j = i; j < numPendingAssignments
&& generatedNamesForAssignments.get(j).length() == len; j++) {
varsByOrderOfOccurrence.add(pendingAssignments.get(j));
}
// Now, make the assignments
for (Assignment a : varsByOrderOfOccurrence) {
finalizeNameAssignment(a, generatedNamesForAssignments.get(i));
++i;
}
}
}
/**
* Makes a final name assignment.
*/
private void finalizeNameAssignment(Assignment a, String newName) {
a.setNewName(newName);
// Keep track of the mapping
renameMap.put(a.oldName, newName);
}
/**
* Gets the variable map.
*/
VariableMap getVariableMap() {
return new VariableMap(ImmutableMap.copyOf(renameMap));
}
/**
* Determines whether a variable name is okay to rename.
*/
private boolean okToRenameVar(String name, boolean isLocal) {
return !compiler.getCodingConvention().isExported(name, /* local= */ isLocal);
}
/**
* Returns the index within the scope stack.
* e.g. function Foo(a) { var b; function c(d) { } }
* a = 0, b = 1, c = 2, d = 3
*/
private int getLocalVarIndex(Var v) {
int num = v.getIndex();
Scope s = v.getScope().getParent();
if (s == null) {
throw new IllegalArgumentException("Var is not local");
}
boolean isBleedingIntoScope = s.getParent() != null && localBleedingFunctions.contains(v);
while (s.getParent() != null) {
if (isBleedingIntoScope) {
num += localBleedingFunctionsPerScope.get(s).indexOf(v) + 1;
isBleedingIntoScope = false;
} else {
num += localBleedingFunctionsPerScope.get(s).size();
}
if (shouldTemporarilyRenameLocalsInScope(s)) {
num += s.getVarCount();
}
s = s.getParent();
}
return num;
}
/**
* Returns true if the local variables in a scope should be given
* temporary names (eg, 'L 123') prior to renaming to allow reuse of
* names across scopes. With {@code preferStableNames}, temporary
* renaming is disabled if the number of locals in the scope is
* above a heuristic threshold to allow effective reuse of rename
* maps (see {@code prevUsedRenameMap}). In scopes with many
* variables the temporary name given to a variable is unlikely to
* be the same temporary name used when the rename map was created.
*/
private boolean shouldTemporarilyRenameLocalsInScope(Scope s) {
return (!preferStableNames || s.getVarCount() <= MAX_LOCALS_IN_SCOPE_TO_TEMP_RENAME);
}
}