blob: 89ba74a644f84531ca6b77a59726810b8a3408a5 [file] [log] [blame]
/*
* 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.checkState;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.javascript.jscomp.AbstractCompiler.LifeCycleStage;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.TokenStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
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.TreeSet;
import javax.annotation.Nullable;
/**
* Apache Royale copied RenameProperties and modified it to handle
* Royale modules.
*
* RenameProperties renames properties (including methods) of all JavaScript
* objects. This includes prototypes, functions, object literals, etc.
*
* <p> If provided a VariableMap of previously used names, it tries to reuse
* those names.
*
* <p> To prevent a property from getting renamed you may extern it (add it to
* your externs file) or put it in quotes.
*
* <p> To avoid run-time JavaScript errors, use quotes when accessing properties
* that are defined using quotes.
*
* <pre>
* var a = {'myprop': 0}, b = a['myprop']; // correct
* var x = {'myprop': 0}, y = x.myprop; // incorrect
* </pre>
*
* This pass also recognizes and replaces special renaming functions. They supply
* a property name as the string literal for the first argument.
* This pass will replace them as though they were JS property
* references. Here are two examples:
* JSCompiler_renameProperty('propertyName') -> 'jYq'
* JSCompiler_renameProperty('myProp.nestedProp.innerProp') -> 'e4.sW.C$'
*
*/
class RenamePropertiesWithModuleSupport implements CompilerPass {
private static final Splitter DOT_SPLITTER = Splitter.on('.');
private final AbstractCompiler compiler;
private final boolean generatePseudoNames;
/** Property renaming map from a previous compilation. */
private final VariableMap prevUsedPropertyMap;
/** Original names of properties that were renamed */
private final List<String> prevUsedPropertyOldNames = new ArrayList<String>();
private final List<Node> toRemove = new ArrayList<Node>();
private final List<Node> stringNodesToRename = new ArrayList<Node>();
private final Map<Node, Node> callNodeToParentMap =
new LinkedHashMap<Node, Node>();
private final char[] reservedFirstCharacters;
private final char[] reservedNonFirstCharacters;
// Map from property name to Property object
private final Map<String, Property> propertyMap = new LinkedHashMap<String, Property>();
// Property names that don't get renamed
private final Set<String> externedNames = new LinkedHashSet<String>(
Arrays.asList("prototype"));
// Names to which properties shouldn't be renamed, to avoid name conflicts
private final Set<String> quotedNames = new LinkedHashSet<String>();
// Shared name generator
private final NameGenerator nameGenerator;
private static final Comparator<Property> FREQUENCY_COMPARATOR =
new Comparator<Property>() {
@Override
public int compare(Property p1, Property p2) {
/**
* First a frequently used names would always be picked first.
*/
if (p1.numOccurrences != p2.numOccurrences) {
return p2.numOccurrences - p1.numOccurrences;
}
/**
* Finally, for determinism, we compare them based on the old name.
*/
return p1.oldName.compareTo(p2.oldName);
}
};
static final DiagnosticType BAD_CALL = DiagnosticType.error(
"JSC_BAD_RENAME_PROPERTY_FUNCTION_NAME_CALL",
"Bad {0} call - the first argument must be a string literal");
static final DiagnosticType BAD_ARG = DiagnosticType.error(
"JSC_BAD_RENAME_PROPERTY_FUNCTION_NAME_ARG",
"Bad {0} argument - ''{1}'' is not a valid JavaScript identifier");
/**
* Creates an instance.
*
* @param compiler The JSCompiler
* @param generatePseudoNames Generate pseudo names. e.g foo -> $foo$ instead
* of compact obfuscated names. This is used for debugging.
* @param nameGenerator 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
*/
RenamePropertiesWithModuleSupport(AbstractCompiler compiler, boolean generatePseudoNames,
NameGenerator nameGenerator) {
this(compiler, generatePseudoNames, null, null, null, nameGenerator);
}
/**
* Creates an instance.
*
* @param compiler The JSCompiler.
* @param generatePseudoNames Generate pseudo names. e.g foo -> $foo$ instead
* of compact obfuscated names. This is used for debugging.
* @param prevUsedPropertyMap The property renaming map used in a previous
* compilation.
* @param nameGenerator 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
*/
RenamePropertiesWithModuleSupport(AbstractCompiler compiler,
boolean generatePseudoNames, VariableMap prevUsedPropertyMap,
NameGenerator nameGenerator) {
this(compiler, generatePseudoNames, prevUsedPropertyMap, null, null, nameGenerator);
}
/**
* Creates an instance.
*
* @param compiler The JSCompiler.
* @param generatePseudoNames Generate pseudo names. e.g foo -> $foo$ instead of compact
* obfuscated names. This is used for debugging.
* @param prevUsedPropertyMap The property renaming map used in a previous compilation.
* @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 nameGenerator 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
*/
RenamePropertiesWithModuleSupport(
AbstractCompiler compiler,
boolean generatePseudoNames,
VariableMap prevUsedPropertyMap,
@Nullable char[] reservedFirstCharacters,
@Nullable char[] reservedNonFirstCharacters,
NameGenerator nameGenerator) {
this.compiler = compiler;
this.generatePseudoNames = generatePseudoNames;
this.prevUsedPropertyMap = prevUsedPropertyMap;
this.reservedFirstCharacters = reservedFirstCharacters;
this.reservedNonFirstCharacters = reservedNonFirstCharacters;
this.nameGenerator = nameGenerator;
externedNames.addAll(compiler.getExternProperties());
}
@Override
public void process(Node externs, Node root) {
checkState(compiler.getLifeCycleStage().isNormalized());
if (prevUsedPropertyMap != null) {
prevUsedPropertyOldNames.addAll(prevUsedPropertyMap.getOriginalNameToNewNameMap().keySet());
}
NodeTraversal.traverse(compiler, root, new ProcessProperties());
Set<String> reservedNames =
Sets.newHashSetWithExpectedSize(externedNames.size() + quotedNames.size());
// Assign names, sorted by descending frequency to minimize code size.
Set<Property> propsByFreq = new TreeSet<Property>(FREQUENCY_COMPARATOR);
propsByFreq.addAll(propertyMap.values());
// First, try and reuse as many property names from the previous compilation
// as possible.
if (prevUsedPropertyMap != null) {
reusePropertyNames(reservedNames, propsByFreq);
}
/* add externed and quoted names after previously renamed
* properties are told to re-use their renames from the main
* app. The original code added these names before
* trying to reuse previous renames because those
* externs are typically from 3rd-party libraries, but
* in modules, externs are from the main loading app
* and we want to reuse previous renames as much as possible
* because classes in the main loading app will make calls
* using the renames instead of the exported name so any
* override or implementations of interface methods must
* also have the same rename.
*/
reservedNames.addAll(externedNames);
reservedNames.addAll(quotedNames);
generateNames(propsByFreq, reservedNames);
// Update the string nodes.
for (Node n : stringNodesToRename) {
String oldName = n.getString();
Property p = propertyMap.get(oldName);
if (p != null && p.newName != null) {
checkState(oldName.equals(p.oldName));
n.setString(p.newName);
if (!p.newName.equals(oldName)) {
compiler.reportChangeToEnclosingScope(n);
}
}
}
// Update the call nodes.
for (Map.Entry<Node, Node> nodeEntry : callNodeToParentMap.entrySet()) {
Node parent = nodeEntry.getValue();
Node firstArg = nodeEntry.getKey().getSecondChild();
StringBuilder sb = new StringBuilder();
for (String oldName : DOT_SPLITTER.split(firstArg.getString())) {
Property p = propertyMap.get(oldName);
String replacement;
if (p != null && p.newName != null) {
checkState(oldName.equals(p.oldName));
replacement = p.newName;
} else {
replacement = oldName;
}
if (sb.length() > 0) {
sb.append('.');
}
sb.append(replacement);
}
parent.replaceChild(nodeEntry.getKey(), IR.string(sb.toString()));
compiler.reportChangeToEnclosingScope(parent);
}
// Complete queued removals.
for (Node n : toRemove) {
Node parent = n.getParent();
compiler.reportChangeToEnclosingScope(n);
n.detach();
NodeUtil.markFunctionsDeleted(n, compiler);
if (!parent.hasChildren() && !parent.isScript()) {
parent.detach();
}
}
compiler.setLifeCycleStage(LifeCycleStage.NORMALIZED_OBFUSCATED);
// This pass may rename getter or setter properties
GatherGettersAndSetterProperties.update(compiler, externs, root);
}
/**
* Runs through the list of properties and renames as many as possible with
* names from the previous compilation. Also, updates reservedNames with the
* set of reused names.
* @param reservedNames Reserved names to use during renaming.
* @param allProps Properties to rename.
*/
private void reusePropertyNames(Set<String> reservedNames,
Collection<Property> allProps) {
for (Property prop : allProps) {
// Check if this node can reuse a name from a previous compilation - if
// it can set the newName for the property too.
String prevName = prevUsedPropertyMap.lookupNewName(prop.oldName);
if (!generatePseudoNames && prevName != null) {
// We can reuse prevName if it's not reserved.
if (reservedNames.contains(prevName)) {
continue;
}
prop.newName = prevName;
reservedNames.add(prevName);
}
}
}
/**
* Generates new names for properties.
*
* @param props Properties to generate new names for
* @param reservedNames A set of names to which properties should not be
* renamed
*/
private void generateNames(Set<Property> props, Set<String> reservedNames) {
nameGenerator.reset(reservedNames, "", reservedFirstCharacters, reservedNonFirstCharacters);
for (Property p : props) {
if (generatePseudoNames) {
p.newName = "$" + p.oldName + "$";
} else {
// If we haven't already given this property a reusable name.
if (p.newName == null) {
p.newName = nameGenerator.generateNextName();
}
}
reservedNames.add(p.newName);
}
}
/**
* Gets the property renaming map (the "answer key").
*
* @return A mapping from original names to new names
*/
VariableMap getPropertyMap() {
ImmutableMap.Builder<String, String> map = ImmutableMap.builder();
for (Property p : propertyMap.values()) {
if (p.newName != null) {
map.put(p.oldName, p.newName);
}
}
return new VariableMap(map.build());
}
// -------------------------------------------------------------------------
/**
* A traversal callback that collects property names and counts how
* frequently each property name occurs.
*/
private class ProcessProperties extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
switch (n.getToken()) {
case COMPUTED_PROP:
break;
case GETPROP:
Node propNode = n.getSecondChild();
if (propNode.isString()) {
if (compiler.getCodingConvention().blockRenamingForProperty(
propNode.getString())) {
externedNames.add(propNode.getString());
break;
}
maybeMarkCandidate(propNode);
}
break;
case OBJECTLIT:
for (Node key = n.getFirstChild(); key != null; key = key.getNext()) {
if (key.isComputedProp()) {
// We don't want to rename computed properties
continue;
} else if (key.isQuotedString()) {
// Ensure that we never rename some other property in a way
// that could conflict with this quoted key.
quotedNames.add(key.getString());
} else if (compiler.getCodingConvention().blockRenamingForProperty(key.getString())) {
externedNames.add(key.getString());
} else {
maybeMarkCandidate(key);
}
}
break;
case OBJECT_PATTERN:
// Iterate through all the nodes in the object pattern
for (Node key = n.getFirstChild(); key != null; key = key.getNext()) {
if (key.isComputedProp()) {
// We don't want to rename computed properties
continue;
} else if (key.isQuotedString()) {
// Ensure that we never rename some other property in a way
// that could conflict with this quoted key.
quotedNames.add(key.getString());
} else if (compiler.getCodingConvention().blockRenamingForProperty(key.getString())) {
externedNames.add(key.getString());
} else {
maybeMarkCandidate(key);
}
}
break;
case GETELEM:
// If this is a quoted property access (e.g. x['myprop']), we need to
// ensure that we never rename some other property in a way that
// could conflict with this quoted name.
Node child = n.getLastChild();
if (child != null && child.isString()) {
quotedNames.add(child.getString());
}
break;
case CALL: {
// We replace property renaming function calls with a string
// containing the renamed property.
Node fnName = n.getFirstChild();
if (compiler
.getCodingConvention()
.isPropertyRenameFunction(fnName.getOriginalQualifiedName())) {
callNodeToParentMap.put(n, parent);
countCallCandidates(t, n);
}
break;
}
case CLASS_MEMBERS:
{
// Replace function names defined in a class scope
for (Node key = n.getFirstChild(); key != null; key = key.getNext()) {
if (key.isComputedProp()) {
// We don't want to rename computed properties.
continue;
} else {
Node member = key.getFirstChild();
String memberDefName = key.getString();
if (member.isFunction()) {
Node fnName = member.getFirstChild();
if (compiler.getCodingConvention().blockRenamingForProperty(memberDefName)) {
externedNames.add(fnName.getString());
} else if (memberDefName.equals("constructor")
|| memberDefName.equals("superClass_")) {
// TODO (simarora) is there a better way to identify these externs?
externedNames.add(fnName.getString());
} else {
maybeMarkCandidate(key);
}
}
}
}
break;
}
case FUNCTION:
{
// We eliminate any stub implementations of JSCompiler_renameProperty
// that we encounter.
if (NodeUtil.isFunctionDeclaration(n)) {
String name = n.getFirstChild().getString();
if (NodeUtil.JSC_PROPERTY_NAME_FN.equals(name)) {
toRemove.add(n);
}
} else if (parent.isName()
&& NodeUtil.JSC_PROPERTY_NAME_FN.equals(parent.getString())) {
Node varNode = parent.getParent();
if (varNode.isVar()) {
toRemove.add(parent);
}
} else if (NodeUtil.isFunctionExpression(n)
&& parent.isAssign()
&& parent.getFirstChild().isGetProp()
&& compiler
.getCodingConvention()
.isPropertyRenameFunction(parent.getFirstChild().getOriginalQualifiedName())) {
Node exprResult = parent.getParent();
if (exprResult.isExprResult()
&& NodeUtil.isStatementBlock(exprResult.getParent())
&& exprResult.getFirstChild().isAssign()) {
toRemove.add(exprResult);
}
}
break;
}
default:
break;
}
}
/**
* If a property node is eligible for renaming, stashes a reference to it
* and increments the property name's access count.
*
* @param n The STRING node for a property
*/
private void maybeMarkCandidate(Node n) {
String name = n.getString();
if (!externedNames.contains(name) || prevUsedPropertyOldNames.contains(name)) {
stringNodesToRename.add(n);
countPropertyOccurrence(name);
}
}
/**
* Counts references to property names that occur in a special function
* call.
*
* @param callNode The CALL node for a property
* @param t The traversal
*/
private void countCallCandidates(NodeTraversal t, Node callNode) {
String fnName = callNode.getFirstChild().getOriginalName();
if (fnName == null) {
fnName = callNode.getFirstChild().getString();
}
Node firstArg = callNode.getSecondChild();
if (!firstArg.isString()) {
t.report(callNode, BAD_CALL, fnName);
return;
}
for (String name : DOT_SPLITTER.split(firstArg.getString())) {
if (!TokenStream.isJSIdentifier(name)) {
t.report(callNode, BAD_ARG, fnName);
continue;
}
if (!externedNames.contains(name) || prevUsedPropertyOldNames.contains(name)) {
countPropertyOccurrence(name);
}
}
}
/**
* Increments the occurrence count for a property name.
*
* @param name The property name
*/
private void countPropertyOccurrence(String name) {
Property prop = propertyMap.get(name);
if (prop == null) {
prop = new Property(name);
propertyMap.put(name, prop);
}
prop.numOccurrences++;
}
}
// -------------------------------------------------------------------------
/**
* Encapsulates the information needed for renaming a property.
*/
private static class Property {
final String oldName;
String newName;
int numOccurrences;
Property(String name) {
this.oldName = name;
}
}
}