| /* |
| * 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 com.google.common.collect.ArrayListMultimap; |
| import com.google.common.collect.ImmutableMap; |
| 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.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.SortedSet; |
| import java.util.TreeSet; |
| import javax.annotation.Nullable; |
| |
| /** |
| * Apache Royale copied RenameVars and modified it to handle |
| * Royale modules. |
| * |
| * RenameVars renames all the variables names into short names, to reduce code |
| * size and also to obfuscate the code. |
| * |
| */ |
| final class RenameVarsWithModuleSupport 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<Node> globalNameNodes = new ArrayList<Node>(); |
| |
| /** List of local NAME nodes */ |
| private final ArrayList<Node> localNameNodes = new ArrayList<Node>(); |
| |
| /** Mapping of original names for change detection */ |
| private final Map<Node, String> originalNameByNode = new HashMap<Node, String>(); |
| |
| /** |
| * 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 Map<Node, String> pseudoNameMap; |
| |
| /** Set of extern variable names */ |
| private Set<String> externNames; |
| |
| /** Set of reserved variable names */ |
| private final Set<String> reservedNames; |
| |
| /** The renaming map */ |
| private final Map<String, String> renameMap = new HashMap<String, String>(); |
| |
| /** 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<Var> localBleedingFunctions = new HashSet<Var>(); |
| private final ListMultimap<Scope, Var> localBleedingFunctionsPerScope = |
| ArrayListMultimap.create(); |
| |
| class Assignment { |
| final boolean isLocal; |
| final String oldName; |
| final int orderOfOccurrence; |
| 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<String, Assignment> assignments = |
| new HashMap<String, Assignment>(); |
| |
| /** Whether renaming should apply to local variables only. */ |
| private final boolean localRenamingOnly; |
| |
| /** |
| * Whether function expression names should be preserved. Typically, for |
| * debugging purposes. |
| * |
| * @see NameAnonymousFunctions |
| */ |
| private final boolean preserveFunctionExpressionNames; |
| |
| private final boolean shouldShadow; |
| |
| 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; |
| |
| /* list of aliases from the externs */ |
| private List<String> externAliases; |
| |
| /* |
| * 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. |
| */ |
| RenameVarsWithModuleSupport(AbstractCompiler compiler, String prefix, |
| boolean localRenamingOnly, boolean preserveFunctionExpressionNames, |
| boolean generatePseudoNames, boolean shouldShadow, |
| boolean preferStableNames, VariableMap prevUsedRenameMap, |
| @Nullable char[] reservedCharacters, |
| @Nullable Set<String> reservedNames, |
| NameGenerator nameGenerator) { |
| this.compiler = compiler; |
| this.prefix = nullToEmpty(prefix); |
| this.localRenamingOnly = localRenamingOnly; |
| this.preserveFunctionExpressionNames = preserveFunctionExpressionNames; |
| if (generatePseudoNames) { |
| this.pseudoNameMap = new HashMap<Node, String>(); |
| } else { |
| this.pseudoNameMap = null; |
| } |
| this.prevUsedRenameMap = prevUsedRenameMap; |
| this.reservedCharacters = reservedCharacters; |
| this.shouldShadow = shouldShadow; |
| this.preferStableNames = preferStableNames; |
| if (reservedNames == null) { |
| this.reservedNames = new HashSet<String>(); |
| } else { |
| this.reservedNames = new HashSet<String>(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.scope.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; |
| } |
| |
| // Are we renaming function expression names? |
| if (preserveFunctionExpressionNames && var != null |
| && NodeUtil.isFunctionExpression(var.getParentNode())) { |
| 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.get(name); |
| if (s == null) { |
| s = new Assignment(name); |
| assignments.put(name, s); |
| } |
| 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 final Comparator<Assignment> FREQUENCY_COMPARATOR = |
| new Comparator<Assignment>() { |
| @Override |
| public int compare(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<Assignment> ORDER_OF_OCCURRENCE_COMPARATOR = |
| new Comparator<Assignment>() { |
| @Override |
| public int compare(Assignment a1, Assignment a2) { |
| return a1.orderOfOccurrence - a2.orderOfOccurrence; |
| } |
| }; |
| |
| @Override |
| public void process(Node externs, Node root) { |
| externAliases = ProcessClosurePrimitivesWithModuleSupport.externedAliases.get(externs); |
| 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<Assignment> varsByFrequency = new TreeSet<Assignment>(FREQUENCY_COMPARATOR); |
| varsByFrequency.addAll(assignments.values()); |
| |
| if (shouldShadow) { |
| new ShadowVariablesWithModuleSupport( |
| compiler, assignments, varsByFrequency, pseudoNameMap).process( |
| externs, root); |
| } |
| |
| // First try to reuse names from an earlier compilation. |
| if (prevUsedRenameMap != null) { |
| reusePreviouslyUsedVariableMap(varsByFrequency); |
| } |
| |
| // now that previous names have been assigned, |
| // add those previous renames to reserved names |
| // so those variable names don't get re-used in assignNames. |
| if (prevUsedRenameMap != null) { |
| reservedNames.addAll(prevUsedRenameMap.getNewNameToOriginalNameMap().keySet()); |
| } |
| |
| // Assign names, sorted by descending frequency to minimize code size. |
| assignNames(varsByFrequency); |
| |
| // Rename the globals! |
| for (Node n : globalNameNodes) { |
| /* A module may have a reference to a class |
| * but no variable defining it since the class |
| * is defined in the main loading app. Closure |
| * expects every reference to have a var so in |
| * CollapseProperties, some vars are added so that |
| * they can be collapsed. However, we don't want those |
| * added vars in the output so here we move them |
| * to externs (if we added them to the externs in |
| * CollapseProperties they wouldn't get renamed). |
| */ |
| boolean moveToExternsAfterRename = false; |
| String renamedVar = n.getString(); |
| if (prevUsedRenameMap != null && |
| externAliases.contains(renamedVar) && |
| n.getParent().isVar()) |
| moveToExternsAfterRename = true; |
| setNameAndReport(n, getNewGlobalName(n)); |
| if (moveToExternsAfterRename) { |
| Node p = n.getParent(); |
| p.detach(); |
| Node externsInput = compiler.getSynthesizedExternsInput().getAstRoot(compiler); |
| externsInput.addChildToBack(p); |
| compiler.reportChangeToEnclosingScope(p); |
| } |
| } |
| |
| // 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); |
| } |
| } |
| } |
| } |
| |
| @Nullable |
| private 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; |
| } |
| } |
| |
| @Nullable |
| private 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<Assignment> 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<Assignment> varsToRename) { |
| NameGenerator globalNameGenerator = null; |
| NameGenerator localNameGenerator = null; |
| |
| globalNameGenerator = nameGenerator; |
| nameGenerator.reset(reservedNames, prefix, reservedCharacters); |
| |
| // 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. |
| localNameGenerator = |
| prefix.isEmpty() |
| ? globalNameGenerator |
| : nameGenerator.clone(reservedNames, "", reservedCharacters); |
| |
| // Generated names and the assignments for non-local vars. |
| List<Assignment> pendingAssignments = new ArrayList<Assignment>(); |
| List<String> generatedNamesForAssignments = new ArrayList<String>(); |
| |
| 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<Assignment> varsByOrderOfOccurrence = |
| new TreeSet<Assignment>(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, 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.index; |
| Scope s = v.scope.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); |
| } |
| } |