com.google.javascript.jscomp.DefaultNameGenerator Maven / Gradle / Ivy
Show all versions of closure-compiler-unshaded Show documentation
/*
* Copyright 2005 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.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.primitives.Chars;
import com.google.javascript.rhino.TokenStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.jspecify.nullness.Nullable;
/**
* A simple class for generating unique JavaScript variable/property names.
*
* This class is not thread safe.
*/
public final class DefaultNameGenerator implements NameGenerator {
/**
* Represents a char that can be used in renaming as well as how often that char appears in the
* generated code.
*/
private static final class CharPriority implements Comparable, Serializable {
final char name;
int occurrence;
// This is a tie-breaker when two chars occurrence count is the same.
// When that happens, the 'natural' order prevails.
final int order;
CharPriority(char name, int order) {
this.name = name;
this.order = order;
this.occurrence = 0;
}
// @Override removed for GWT compatibility
public CharPriority clone() {
CharPriority result = new CharPriority(name, order);
result.occurrence = occurrence;
return result;
}
@Override
public int compareTo(CharPriority other) {
// Start out by putting the element with more occurrence first.
int result = other.occurrence - this.occurrence;
if (result != 0) {
return result;
}
// If there is a tie, follow the order of FIRST_CHAR and NONFIRST_CHAR.
result = this.order - other.order;
return result;
}
}
// TODO(user): Maybe we don't need a LinkedHashMap to look up.
// I started writing a skip-list like data-structure that would let us
// have O(1) favors() and O(1) reset() but the code got very messy.
// Lets start with a logical implementation first until performance becomes
// a problem.
private Map priorityLookupMap;
// It is important that the ordering of FIRST_CHAR is as close to NONFIRST_CHAR
// as possible. Using the ASCII ordering is not a good idea. The reason
// is that we cannot use numbers as FIRST_CHAR yet the ACSII value of numbers
// is very small. If we picked numbers first in NONFIRST_CHAR, we would
// end up balancing the huffman tree and result is bad compression.
/** Generate short name with this first character */
static final char[] FIRST_CHAR =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$".toCharArray();
/** These appear after the first character */
static final char[] NONFIRST_CHAR =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789$"
.toCharArray();
/**
* Initializes names that must not be generated. TokenStream.isKeyword() skips scanning new
* keywords such as "let", "await", etc. Hence, those names must be explicitly checked for on top
* of the successfully scanned ES3 reservedNames
*/
private static final ImmutableSet BAD_NAMES =
ImmutableSet.of("let", "yield", "await", "eval");
private Set reservedNames;
private String prefix;
private int nameCount;
private CharPriority[] firstChars;
private CharPriority[] nonFirstChars;
public DefaultNameGenerator() {
buildPriorityLookupMap();
Set reservedNames = Sets.newLinkedHashSetWithExpectedSize(0);
reset(reservedNames, "", null);
}
public DefaultNameGenerator(
Set reservedNames, String prefix, char @Nullable [] reservedCharacters) {
this(reservedNames, prefix, reservedCharacters, reservedCharacters);
}
/**
* Creates a DefaultNameGenerator.
*
* @param reservedNames set of names that are reserved; generated names will not include these
* names. This set is referenced rather than copied, so changes to the set will be reflected
* in how names are generated.
* @param prefix all generated names begin with this prefix.
* @param reservedFirstCharacters If specified these characters won't be used in generated names
* for the first character
* @param reservedNonFirstCharacters If specified these characters won't be used in generated
* names for characters after the first
*/
public DefaultNameGenerator(
Set reservedNames,
String prefix,
char @Nullable [] reservedFirstCharacters,
char @Nullable [] reservedNonFirstCharacters) {
buildPriorityLookupMap();
reset(reservedNames, prefix, reservedFirstCharacters, reservedNonFirstCharacters);
}
private DefaultNameGenerator(
Set reservedNames,
String prefix,
char @Nullable [] reservedCharacters,
Map priorityLookupMap) {
// Clone the priorityLookupMap to preserve information about how often
// characters are used.
this.priorityLookupMap = Maps.newLinkedHashMapWithExpectedSize(NONFIRST_CHAR.length);
for (Map.Entry entry :
priorityLookupMap.entrySet()) {
this.priorityLookupMap.put(entry.getKey(), entry.getValue().clone());
}
reset(reservedNames, prefix, reservedCharacters, reservedCharacters);
}
private void buildPriorityLookupMap() {
priorityLookupMap = Maps.newLinkedHashMapWithExpectedSize(NONFIRST_CHAR.length);
int order = 0;
for (char c : NONFIRST_CHAR) {
priorityLookupMap.put(c, new CharPriority(c, order));
order++;
}
}
/** Returns if name is a badName (let, yield, eval, etc) that must not be generated */
private static boolean isBadName(String name) {
return BAD_NAMES.contains(name);
}
@Override
public void reset(
Set reservedNames, String prefix, char @Nullable [] reservedCharacters) {
reset(reservedNames, prefix, reservedCharacters, reservedCharacters);
}
/**
* Note that the history of what characters are most used in the program (set through calls to
* 'favor') is not deleted. Upon 'reset', that history is taken into account for the names that
* will be generated later: it re-calculates how characters are prioritized based on how often the
* they appear in the final output.
*/
@Override
public void reset(
Set reservedNames,
String prefix,
char @Nullable [] reservedFirstCharacters,
char @Nullable [] reservedNonFirstCharacters) {
this.reservedNames = reservedNames;
this.prefix = prefix;
this.nameCount = 0;
// build the character arrays to use
this.firstChars = reserveCharacters(FIRST_CHAR, reservedFirstCharacters);
this.nonFirstChars = reserveCharacters(NONFIRST_CHAR, reservedNonFirstCharacters);
Arrays.sort(firstChars);
Arrays.sort(nonFirstChars);
checkPrefix(prefix);
}
@Override
public NameGenerator clone(
Set reservedNames, String prefix, char @Nullable [] reservedCharacters) {
return new DefaultNameGenerator(reservedNames, prefix, reservedCharacters,
priorityLookupMap);
}
/**
* Increase the prioritization of all the chars in a String. This information
* is not used until {@link #reset} is called. A compiler would be
* able to generate names while changing the prioritization of the name
* generator for the next pass.
*/
void favors(CharSequence sequence) {
for (int i = 0; i < sequence.length(); i++) {
CharPriority c = priorityLookupMap.get(sequence.charAt(i));
if (c != null) {
c.occurrence++;
}
}
}
/**
* Provides the array of available characters based on the specified arrays.
* @param chars The list of characters that are legal
* @param reservedCharacters The characters that should not be used
* @return An array of characters to use. Will return the chars array if
* reservedCharacters is null or empty, otherwise creates a new array.
*/
CharPriority[] reserveCharacters(char[] chars, char[] reservedCharacters) {
if (reservedCharacters == null || reservedCharacters.length == 0) {
CharPriority[] result = new CharPriority[chars.length];
for (int i = 0; i < chars.length; i++) {
result[i] = priorityLookupMap.get(chars[i]);
}
return result;
}
Set charSet = new LinkedHashSet<>(Chars.asList(chars));
for (char reservedCharacter : reservedCharacters) {
charSet.remove(reservedCharacter);
}
CharPriority[] result = new CharPriority[charSet.size()];
int index = 0;
for (char c : charSet) {
result[index++] = priorityLookupMap.get(c);
}
return result;
}
/** Validates a name prefix. */
private void checkPrefix(String prefix) {
if (prefix.length() > 0) {
// Make sure that prefix starts with a legal character.
if (!contains(firstChars, prefix.charAt(0))) {
char[] chars = new char[firstChars.length];
for (int i = 0; i < chars.length; i++) {
chars[i] = firstChars[i].name;
}
throw new IllegalArgumentException(
"prefix must start with one of: " + Arrays.toString(chars));
}
for (int pos = 1; pos < prefix.length(); ++pos) {
char[] chars = new char[nonFirstChars.length];
for (int i = 0; i < chars.length; i++) {
chars[i] = nonFirstChars[i].name;
}
if (!contains(nonFirstChars, prefix.charAt(pos))) {
throw new IllegalArgumentException(
"prefix has invalid characters, must be one of: "
+ Arrays.toString(chars));
}
}
}
}
private static boolean contains(CharPriority[] arr, char c) {
for (int i = 0; i < arr.length; i++) {
if (arr[i].name == c) {
return true;
}
}
return false;
}
/**
* Generates the next short name.
*/
@Override
public String generateNextName() {
String name;
do {
StringBuilder sb = new StringBuilder();
int i = nameCount;
if (!prefix.isEmpty()) {
sb.append(prefix);
}
if (sb.length() == 0) {
int pos = i % firstChars.length;
sb.append(firstChars[pos].name);
i /= firstChars.length;
}
while (i > 0) {
i--;
int pos = i % nonFirstChars.length;
sb.append(nonFirstChars[pos].name);
i /= nonFirstChars.length;
}
nameCount++;
name = sb.toString();
// Make sure it's not a JS keyword or reserved name.
} while (TokenStream.isKeyword(name) || reservedNames.contains(name) || isBadName(name));
return name;
}
}