com.google.javascript.jscomp.RandomNameGenerator Maven / Gradle / Ivy
Show all versions of closure-compiler-unshaded Show documentation
/*
* Copyright 2015 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.annotations.GwtIncompatible;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.primitives.Chars;
import com.google.javascript.rhino.TokenStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.Set;
import org.jspecify.nullness.Nullable;
/**
* A class for generating unique, randomized JavaScript variable/property
* names.
*
* Unlike NameGenerator, names do not follow a predictable sequence such as
* a, b, ... z, aa, ab, ..., az, ba, ...
* but instead they are random, based on an external random seed. We do
* partially compromise for efficiency in that
*
* - Generated names will have the same length as they would with
* NameGenerator
*
- We don't use a completely different alphabet for each name prefix, but
* instead choose among a few with a predictable formula.
*
*
* More precisely:
*
* - We compute a random shuffle of the alphabet for "first characters", and
* a small number of random shuffles of the alphabet for "non-first
* characters". Then we do a typical number-to-text conversion of a name's
* "index", where the alphabet for digits is not just 0 to 9. The least
* significant digit comes first.
*
- We represent each digit using an appropriate alphabet. If it's not the
* first character of the name (i.e. not the least significant one, or there
* is a constant prefix) then we have several appropriate alphabets to choose
* from; we choose one based a hash of the previous digits of this name.
*
*
* This class is not thread safe.
*/
@GwtIncompatible(
"java.util.Collections.shuffle, "
+ "com.google.common.hash.Hasher, "
+ "com.google.common.hash.Hashing")
public final class RandomNameGenerator implements NameGenerator {
/** Generate random names with this first character. */
static final ImmutableSet FIRST_CHAR = asSet(DefaultNameGenerator.FIRST_CHAR);
/** These appear after after the first character */
static final ImmutableSet NONFIRST_CHAR = asSet(DefaultNameGenerator.NONFIRST_CHAR);
/** The possible first characters, after reserved characters are removed */
private ImmutableSet firstChars;
/** Possible non-first characters, after reserved characters are removed */
private ImmutableSet nonFirstChars;
/** Source of randomness */
private final Random random;
/** List of reserved names; these are not returned by generateNextName */
private ImmutableSet reservedNames;
/** Prefix added to all generated names */
private String prefix;
/** How many names have we issued so far (includes names that cannot be used
* because they are reserved through 'reservedNames' or JavaScript
* keywords) */
private int nameCount;
/** How many shuffles of nonFirstChars to generate */
private static final int NUM_SHUFFLES = 16;
/** Randomly-shuffled version of firstChars */
private String shuffledFirst;
/** Randomly-shuffled versions of nonFirstChars (there are NUM_SHUFFLES of them) */
private ImmutableList shuffledNonFirst;
public RandomNameGenerator(Random random) {
this.random = random;
reset(ImmutableSet.of(), "", null);
}
RandomNameGenerator(
Set reservedNames,
String prefix,
char @Nullable [] reservedCharacters,
Random random) {
this.random = random;
reset(reservedNames, prefix, reservedCharacters);
}
/**
* Creates a RandomNameGenerator.
*
* @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 (a name consisting of only this
* prefix, with no suffix, will not be generated)
* @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
* @param random source of randomness when generating random names
*/
RandomNameGenerator(
Set reservedNames,
String prefix,
char @Nullable [] reservedFirstCharacters,
char @Nullable [] reservedNonFirstCharacters,
Random random) {
this.random = random;
reset(reservedNames, prefix, reservedFirstCharacters, reservedNonFirstCharacters);
}
@Override
public void reset(
Set reservedNames, String prefix, char @Nullable [] reservedCharacters) {
reset(reservedNames, prefix, reservedCharacters, reservedCharacters);
}
@Override
public void reset(
Set reservedNames,
String prefix,
char @Nullable [] reservedFirstCharacters,
char @Nullable [] reservedNonFirstCharacters) {
this.reservedNames = ImmutableSet.copyOf(reservedNames);
this.prefix = prefix;
nameCount = 0;
// Build the character arrays to use
firstChars = Sets.difference(FIRST_CHAR, asSet(reservedFirstCharacters)).immutableCopy();
nonFirstChars =
Sets.difference(NONFIRST_CHAR, asSet(reservedNonFirstCharacters)).immutableCopy();
checkPrefix(prefix);
shuffleAlphabets();
}
@Override
public NameGenerator clone(
Set reservedNames, String prefix, char @Nullable [] reservedCharacters) {
return new RandomNameGenerator(
reservedNames, prefix, reservedCharacters, random);
}
private static ImmutableSet asSet(char @Nullable [] chars) {
return chars == null ? ImmutableSet.of() : ImmutableSet.copyOf(Chars.asList(chars));
}
/**
* Validates a name prefix.
*/
private void checkPrefix(String prefix) {
if (prefix.length() > 0) {
// Make sure that prefix starts with a legal character.
if (!firstChars.contains(prefix.charAt(0))) {
throw new IllegalArgumentException(
"prefix must start with one of: "
+ Joiner.on(", ").join(firstChars));
}
for (int pos = 1; pos < prefix.length(); ++pos) {
if (!nonFirstChars.contains(prefix.charAt(pos))) {
throw new IllegalArgumentException(
"prefix has invalid characters, must be one of: "
+ Joiner.on(", ").join(nonFirstChars));
}
}
}
}
private static String shuffleAndCopyAlphabet(Set input, Random random) {
List shuffled = new ArrayList<>(input);
Collections.shuffle(shuffled, random);
return new String(Chars.toArray(shuffled));
}
/** Generates random shuffles of the alphabets. */
private void shuffleAlphabets() {
shuffledFirst = shuffleAndCopyAlphabet(firstChars, random);
ImmutableList.Builder builder = ImmutableList.builder();
for (int i = 0; i < NUM_SHUFFLES; ++i) {
builder.add(shuffleAndCopyAlphabet(nonFirstChars, random));
}
shuffledNonFirst = builder.build();
}
/**
* Computes the length (in digits) for a name suffix.
*/
private int getNameLength(int position, int nameIdx) {
int length = 0;
nameIdx++;
do {
nameIdx--;
int alphabetSize = position == 0
? firstChars.size() : nonFirstChars.size();
nameIdx /= alphabetSize;
position++;
length++;
} while (nameIdx > 0);
return length;
}
/**
* Returns the {@code nameIdx}-th short name. This might be a reserved name.
* A user-requested prefix is not included, but the first returned character
* is supposed to go at position {@code position} in the final name
*/
private String generateSuffix(int position, int nameIdx) {
StringBuilder name = new StringBuilder();
int length = getNameLength(position, nameIdx);
nameIdx++;
do {
nameIdx--;
String alphabet;
if (position == 0) {
alphabet = shuffledFirst;
} else {
Hasher hasher = Hashing.murmur3_128().newHasher();
hasher.putInt(length);
hasher.putUnencodedChars(name);
int alphabetIdx = (hasher.hash().asInt() & 0x7fffffff) % NUM_SHUFFLES;
alphabet = shuffledNonFirst.get(alphabetIdx);
}
int alphabetSize = alphabet.length();
char character = alphabet.charAt(nameIdx % alphabetSize);
name.append(character);
nameIdx /= alphabetSize;
position++;
} while (nameIdx > 0);
return name.toString();
}
/**
* Generates the next short name.
*
* This generates names of increasing length. To minimize output size,
* therefore, it's good to call it for the most used symbols first.
*/
@Override
public String generateNextName() {
while (true) {
String name = prefix + generateSuffix(prefix.length(), nameCount++);
// Make sure it's not a JS keyword or reserved name.
if (TokenStream.isKeyword(name) || reservedNames.contains(name)) {
continue;
}
return name;
}
}
}