com.google.javascript.jscomp.MakeDeclaredNamesUnique Maven / Gradle / Ivy
Show all versions of closure-compiler-linter Show documentation
/*
* 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 static com.google.common.base.MoreObjects.toStringHelper;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Multiset;
import com.google.javascript.jscomp.NodeTraversal.ScopedCallback;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.TokenStream;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Find all Functions, VARs, and Exception names and make them
* unique. Specifically, it will not modify object properties.
* @author [email protected] (John Lenz)
* TODO(johnlenz): Try to merge this with the ScopeCreator.
* TODO(moz): Handle more ES6 features, such as default parameters.
*/
class MakeDeclaredNamesUnique implements NodeTraversal.ScopedCallback {
// Arguments is special cased to handle cases where a local name shadows
// the arguments declaration.
public static final String ARGUMENTS = "arguments";
// The name stack is similar to how we model scopes but handles some
// additional cases that are not handled by the current Scope object.
// Specifically, a Scope currently has only two concepts of scope (global,
// and function local). But there are in reality a couple of additional
// case to worry about:
// catch expressions
// function expressions names
// Both belong to a scope by themselves.
// In addition, ES6 introduced block scopes, which we also need to handle.
private final Deque nameStack = new ArrayDeque<>();
private final Renamer rootRenamer;
MakeDeclaredNamesUnique() {
this(new ContextualRenamer());
}
MakeDeclaredNamesUnique(Renamer renamer) {
this.rootRenamer = renamer;
}
static CompilerPass getContextualRenameInverter(AbstractCompiler compiler) {
return new ContextualRenameInverter(compiler);
}
@Override
public void enterScope(NodeTraversal t) {
Node declarationRoot = t.getScopeRoot();
// Function bodies are handled along with PARAM_LIST
if (NodeUtil.isFunctionBlock(declarationRoot)) {
return;
}
Renamer renamer;
if (nameStack.isEmpty()) {
// If the contextual renamer is being used, the starting context can not
// be a function.
Preconditions.checkState(
!declarationRoot.isFunction() || !(rootRenamer instanceof ContextualRenamer));
Preconditions.checkState(t.inGlobalScope());
renamer = rootRenamer;
} else {
renamer = nameStack.peek().createForChildScope(!NodeUtil.createsBlockScope(declarationRoot));
}
if (!declarationRoot.isFunction()) {
// Add the block declarations
findDeclaredNames(t, declarationRoot, renamer, false);
}
nameStack.push(renamer);
}
@Override
public void exitScope(NodeTraversal t) {
// ES6 function blocks are handled along with PARAM_LIST
if (NodeUtil.isFunctionBlock(t.getScopeRoot())) {
return;
}
if (!t.inGlobalScope()) {
nameStack.pop();
}
}
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
switch (n.getToken()) {
case FUNCTION: {
// Add recursive function name, if needed.
// NOTE: "enterScope" is called after we need to pick up this name.
Renamer renamer = nameStack.peek().createForChildScope(false);
// If needed, add the function recursive name.
String name = n.getFirstChild().getString();
if (!name.isEmpty() && parent != null && !NodeUtil.isFunctionDeclaration(n)) {
renamer.addDeclaredName(name, false);
}
nameStack.push(renamer);
break;
}
case PARAM_LIST: {
Renamer renamer = nameStack.peek().createForChildScope(true);
// Add the function parameters
for (Node c = n.getFirstChild(); c != null; c = c.getNext()) {
String name = c.getString();
renamer.addDeclaredName(name, true);
}
Node functionBody = n.getNext();
findDeclaredNames(t, functionBody, renamer, false);
nameStack.push(renamer);
break;
}
default:
break;
}
return true;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
switch (n.getToken()) {
case NAME:
String newName = getReplacementName(n.getString());
if (newName != null) {
Renamer renamer = nameStack.peek();
if (renamer.stripConstIfReplaced()) {
// TODO(johnlenz): Do we need to do anything about the Javadoc?
n.removeProp(Node.IS_CONSTANT_NAME);
}
n.setString(newName);
t.getCompiler().reportChangeToEnclosingScope(n);
}
break;
case FUNCTION:
// Remove the function body scope
nameStack.pop();
// Remove function recursive name (if any).
nameStack.pop();
break;
case PARAM_LIST:
// Note: The parameters and function body variables live in the
// same scope, we introduce the scope when in the "shouldTraverse"
// visit of PARAM_LIST, but remove it when when we exit the function above.
break;
default:
break;
}
}
/**
* Walks the stack of name maps and finds the replacement name for the
* current scope.
*/
private String getReplacementName(String oldName) {
for (Renamer names : nameStack) {
String newName = names.getReplacementName(oldName);
if (newName != null) {
return newName;
}
}
return null;
}
/**
* Traverses the current scope and collects declared names.
*
* @param recursive Whether this is being called recursively.
*/
private void findDeclaredNames(NodeTraversal t, Node n, Renamer renamer, boolean recursive) {
Node parent = n.getParent();
// Do a shallow traversal: Don't traverse into the function param list or body; just its name.
if (recursive && parent.isFunction() && n != parent.getFirstChild()) {
return;
}
if (NodeUtil.isVarDeclaration(n)) {
renamer.addDeclaredName(n.getString(), true);
} else if (NodeUtil.isBlockScopedDeclaration(n)) {
if (t.getScopeRoot() == NodeUtil.getEnclosingScopeRoot(n)) {
renamer.addDeclaredName(n.getString(), false);
// For functions, findDeclaredNames is called from enterScope when entering the function
// scope, rather than when entering the function body scope, so we need to check for that
// case as well.
} else if (t.getScopeRoot().isFunction()
&& NodeUtil.getEnclosingScopeRoot(n) == NodeUtil.getFunctionBody(t.getScopeRoot())) {
renamer.addDeclaredName(n.getString(), false);
}
} else if (NodeUtil.isFunctionDeclaration(n)) {
Node nameNode = n.getFirstChild();
renamer.addDeclaredName(nameNode.getString(), true);
}
for (Node c = n.getFirstChild(); c != null; c = c.getNext()) {
findDeclaredNames(t, c, renamer, true);
}
}
/**
* Declared names renaming policy interface.
*/
interface Renamer {
/**
* Called when a declared name is found in the local current scope.
*/
void addDeclaredName(String name, boolean hoisted);
/**
* @return A replacement name, null if oldName is unknown or should not
* be replaced.
*/
String getReplacementName(String oldName);
/**
* @return Whether the constant-ness of a name should be removed.
*/
boolean stripConstIfReplaced();
/**
* @return A Renamer for a scope within the scope of the current Renamer.
*/
Renamer createForChildScope(boolean hoisted);
/**
* @return The closest hoisting target for var and function declarations.
*/
Renamer getHoistRenamer();
}
/**
* Inverts the transformation by {@link ContextualRenamer}, when possible.
*/
static class ContextualRenameInverter
implements ScopedCallback, CompilerPass {
private final AbstractCompiler compiler;
// The set of names referenced in the current scope.
private Set referencedNames = ImmutableSet.of();
// Stack reference sets.
private Deque> referenceStack = new ArrayDeque<>();
// Name are globally unique initially, so we don't need a per-scope map.
private final ListMultimap nameMap =
MultimapBuilder.hashKeys().arrayListValues().build();
private ContextualRenameInverter(AbstractCompiler compiler) {
this.compiler = compiler;
}
@Override
public void process(Node externs, Node js) {
NodeTraversal.traverseEs6(compiler, js, this);
}
public static String getOriginalName(String name) {
int index = indexOfSeparator(name);
return (index == -1) ? name : name.substring(0, index);
}
private static int indexOfSeparator(String name) {
return name.lastIndexOf(ContextualRenamer.UNIQUE_ID_SEPARATOR);
}
private static boolean containsSeparator(String name) {
return name.contains(ContextualRenamer.UNIQUE_ID_SEPARATOR);
}
/**
* Prepare a set for the new scope.
*/
@Override
public void enterScope(NodeTraversal t) {
if (t.inGlobalScope()) {
return;
}
referenceStack.push(referencedNames);
referencedNames = new HashSet<>();
}
/**
* Rename vars for the current scope, and merge any referenced
* names into the parent scope reference set.
*/
@Override
public void exitScope(NodeTraversal t) {
if (t.inGlobalScope()) {
return;
}
for (Var v : t.getScope().getVarIterable()) {
handleScopeVar(v);
}
// Merge any names that were referenced but not declared in the current
// scope.
Set current = referencedNames;
referencedNames = referenceStack.pop();
// If there isn't anything left in the stack we will be going into the
// global scope: don't try to build a set of referenced names for the
// global scope.
if (!referenceStack.isEmpty()) {
referencedNames.addAll(current);
}
}
/**
* For the Var declared in the current scope determine if it is possible
* to revert the name to its original form without conflicting with other
* values.
*/
void handleScopeVar(Var v) {
String name = v.getName();
if (containsSeparator(name) && !getOriginalName(name).isEmpty()) {
String newName = findReplacementName(name);
referencedNames.remove(name);
// Adding a reference to the new name to prevent either the parent
// scopes or the current scope renaming another var to this new name.
referencedNames.add(newName);
List references = nameMap.get(name);
for (Node n : references) {
Preconditions.checkState(n.isName(), n);
n.setString(newName);
compiler.reportChangeToEnclosingScope(n);
}
nameMap.removeAll(name);
}
}
/**
* Find a name usable in the local scope.
*/
private String findReplacementName(String name) {
String original = getOriginalName(name);
String newName = original;
int i = 0;
while (!isValidName(newName)) {
newName = original + ContextualRenamer.UNIQUE_ID_SEPARATOR + i++;
}
return newName;
}
/**
* @return Whether the name is valid to use in the local scope.
*/
private boolean isValidName(String name) {
return TokenStream.isJSIdentifier(name) && !referencedNames.contains(name)
&& !name.equals(ARGUMENTS);
}
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
return true;
}
@Override
public void visit(NodeTraversal t, Node node, Node parent) {
if (t.inGlobalScope()) {
return;
}
if (NodeUtil.isReferenceName(node)) {
String name = node.getString();
// Add all referenced names to the set so it is possible to check for
// conflicts.
referencedNames.add(name);
// Store only references to candidate names in the node map.
if (containsSeparator(name)) {
addCandidateNameReference(name, node);
}
}
}
private void addCandidateNameReference(String name, Node n) {
nameMap.put(name, n);
}
}
/**
* Renames every local name to be unique. The first encountered declaration of a given name
* (specifically a global declaration) is left in its original form. Those that are renamed are
* made unique by giving them a unique suffix based on the number of declarations of the name.
*
* The root ContextualRenamer is assumed to be in GlobalScope.
*
*
Used by the Normalize pass.
*
* @see Normalize
*/
static class ContextualRenamer implements Renamer {
// This multiset is shared between this ContextualRenamer and its parent (and its parent's
// parent, etc.) because it tracks counts of variables across the entire JS program.
private final Multiset nameUsage;
// By contrast, this is a different map for each ContextualRenamer because it's just keeping
// track of the names used by this renamer.
private final Map declarations = new HashMap<>();
private final boolean global;
private final Renamer hoistRenamer;
static final String UNIQUE_ID_SEPARATOR = "$jscomp$";
@Override
public String toString() {
return toStringHelper(this)
.add("nameUsage", nameUsage)
.add("declarations", declarations)
.add("global", global)
.toString();
}
ContextualRenamer() {
global = true;
nameUsage = HashMultiset.create();
hoistRenamer = this;
}
/**
* Constructor for child scopes.
*/
private ContextualRenamer(
Multiset nameUsage, boolean hoistingTargetScope, Renamer parent) {
this.global = false;
this.nameUsage = nameUsage;
if (hoistingTargetScope) {
hoistRenamer = this;
} else {
hoistRenamer = parent.getHoistRenamer();
}
}
/**
* Create a ContextualRenamer
*/
@Override
public Renamer createForChildScope(boolean hoistingTargetScope) {
return new ContextualRenamer(nameUsage, hoistingTargetScope, this);
}
/**
* Adds a name to the map of names declared in this scope.
*/
@Override
public void addDeclaredName(String name, boolean hoisted) {
if (hoisted && hoistRenamer != this) {
hoistRenamer.addDeclaredName(name, true);
} else {
if (!name.equals(ARGUMENTS)) {
if (global) {
reserveName(name);
} else {
// It hasn't been declared locally yet, so increment the count.
if (!declarations.containsKey(name)) {
int id = incrementNameCount(name);
String newName = null;
if (id != 0) {
newName = getUniqueName(name, id);
}
declarations.put(name, newName);
}
}
}
}
}
@Override
public String getReplacementName(String oldName) {
return declarations.get(oldName);
}
/**
* Given a name and the associated id, create a new unique name.
*/
private static String getUniqueName(String name, int id) {
return name + UNIQUE_ID_SEPARATOR + id;
}
private void reserveName(String name) {
nameUsage.setCount(name, 0, 1);
}
private int incrementNameCount(String name) {
return nameUsage.add(name, 1);
}
@Override
public boolean stripConstIfReplaced() {
return false;
}
@Override
public Renamer getHoistRenamer() {
return hoistRenamer;
}
}
/**
* Rename every declared name to be unique. Typically this would be used
* when injecting code to insure that names do not conflict with existing
* names.
*
* Used by the FunctionInjector
* @see FunctionInjector
*/
static class InlineRenamer implements Renamer {
private final Map declarations = new HashMap<>();
private final Supplier uniqueIdSupplier;
private final String idPrefix;
private final boolean removeConstness;
private final CodingConvention convention;
private final Renamer hoistRenamer;
InlineRenamer(
CodingConvention convention,
Supplier uniqueIdSupplier,
String idPrefix,
boolean removeConstness,
boolean hoistingTargetScope,
Renamer parent) {
this.convention = convention;
this.uniqueIdSupplier = uniqueIdSupplier;
// To ensure that the id does not conflict with the id from the
// ContextualRenamer some prefix is needed.
Preconditions.checkArgument(!idPrefix.isEmpty());
this.idPrefix = idPrefix;
this.removeConstness = removeConstness;
if (hoistingTargetScope) {
hoistRenamer = this;
} else {
hoistRenamer = parent.getHoistRenamer();
}
}
@Override
public void addDeclaredName(String name, boolean hoisted) {
Preconditions.checkState(!name.equals(ARGUMENTS));
if (hoisted && hoistRenamer != this) {
hoistRenamer.addDeclaredName(name, hoisted);
} else {
if (!declarations.containsKey(name)) {
declarations.put(name, getUniqueName(name));
}
}
}
private String getUniqueName(String name) {
if (name.isEmpty()) {
return name;
}
if (name.contains(ContextualRenamer.UNIQUE_ID_SEPARATOR)) {
name = name.substring(
0, name.lastIndexOf(ContextualRenamer.UNIQUE_ID_SEPARATOR));
}
if (convention.isExported(name)) {
// The google internal coding convention includes a naming convention
// to export names starting with "_". Simply strip "_" those to avoid
// exporting names.
name = "JSCompiler_" + name;
}
// By using the same separator the id will be stripped if it isn't
// needed when variable renaming is turned off.
return name + ContextualRenamer.UNIQUE_ID_SEPARATOR
+ idPrefix + uniqueIdSupplier.get();
}
@Override
public String getReplacementName(String oldName) {
return declarations.get(oldName);
}
@Override
public Renamer createForChildScope(boolean hoistingTargetScope) {
return new InlineRenamer(
convention, uniqueIdSupplier, idPrefix, removeConstness, hoistingTargetScope, this);
}
@Override
public boolean stripConstIfReplaced() {
return removeConstness;
}
@Override
public Renamer getHoistRenamer() {
return hoistRenamer;
}
}
/**
* For injecting boilerplate libraries. Leaves global names alone
* and renames local names like InlineRenamer.
*/
static class BoilerplateRenamer extends ContextualRenamer {
private final Supplier uniqueIdSupplier;
private final String idPrefix;
private final CodingConvention convention;
BoilerplateRenamer(
CodingConvention convention,
Supplier uniqueIdSupplier,
String idPrefix) {
this.convention = convention;
this.uniqueIdSupplier = uniqueIdSupplier;
this.idPrefix = idPrefix;
}
@Override
public Renamer createForChildScope(boolean hoisted) {
return new InlineRenamer(convention, uniqueIdSupplier, idPrefix, false, hoisted, this);
}
}
/** Only rename things that match the whitelist. Wraps another renamer. */
static class WhitelistedRenamer implements Renamer {
private Renamer delegate;
private Set whitelist;
WhitelistedRenamer(Renamer delegate, Set whitelist) {
this.delegate = delegate;
this.whitelist = whitelist;
}
@Override
public void addDeclaredName(String name, boolean hoisted) {
if (whitelist.contains(name)) {
delegate.addDeclaredName(name, hoisted);
}
}
@Override
public String getReplacementName(String oldName) {
return whitelist.contains(oldName)
? delegate.getReplacementName(oldName) : null;
}
@Override
public boolean stripConstIfReplaced() {
return delegate.stripConstIfReplaced();
}
@Override
public Renamer createForChildScope(boolean hoistingTargetScope) {
return new WhitelistedRenamer(delegate.createForChildScope(hoistingTargetScope), whitelist);
}
@Override
public Renamer getHoistRenamer() {
return delegate.getHoistRenamer();
}
}
}