blob: 54a3ab586fe16fcd0580c511524b96ec987320bd [file] [log] [blame]
/*
* Copyright 2014 Google Inc.
*
* 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.gwt.dev.js;
import com.google.gwt.dev.cfg.ConfigurationProperties;
import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap;
import com.google.gwt.dev.js.ast.JsName;
import com.google.gwt.dev.js.ast.JsProgram;
import com.google.gwt.dev.js.ast.JsScope;
import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting;
import com.google.gwt.thirdparty.guava.common.base.Objects;
import com.google.gwt.thirdparty.guava.common.collect.HashMultiset;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.collect.Multiset;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import java.io.Serializable;
import java.util.Map;
import java.util.Set;
/**
* A namer that creates short but readable identifiers wherever possible. The old ident ->
* new ident mappings are recorded and can be persisted across multiple compiles.
*/
public class JsIncrementalNamer extends JsNamer {
/**
* Encapsulates the complete state of this namer so that state can be persisted and reused.
*/
public static class JsIncrementalNamerState implements Serializable {
private int nextObfuscatedId = -1;
private Map<String, String> renamedIdentByOriginalIdent = Maps.newHashMap();
private Multiset<String> shortIdentCollisionCounts = HashMultiset.create();
private Set<String> usedIdents = Sets.newHashSet();
public void copyFrom(JsIncrementalNamerState that) {
this.shortIdentCollisionCounts.clear();
this.renamedIdentByOriginalIdent.clear();
this.usedIdents.clear();
this.shortIdentCollisionCounts.addAll(that.shortIdentCollisionCounts);
this.renamedIdentByOriginalIdent.putAll(that.renamedIdentByOriginalIdent);
this.usedIdents.addAll(that.usedIdents);
this.nextObfuscatedId = that.nextObfuscatedId;
}
@VisibleForTesting
public boolean hasSameContent(JsIncrementalNamerState that) {
return Objects.equal(this.shortIdentCollisionCounts, that.shortIdentCollisionCounts)
&& Objects.equal(this.renamedIdentByOriginalIdent, that.renamedIdentByOriginalIdent)
&& Objects.equal(this.usedIdents, that.usedIdents)
&& Objects.equal(this.nextObfuscatedId, that.nextObfuscatedId);
}
}
@VisibleForTesting
public static final String RESERVED_IDENT_SUFFIX = "_g$";
public static void exec(JsProgram program, ConfigurationProperties config, JsIncrementalNamerState state,
JavaToJavaScriptMap jjsmap, boolean minifyFunctionNames) throws IllegalNameException {
new JsIncrementalNamer(program, config, state, jjsmap, minifyFunctionNames).execImpl();
}
private final JavaToJavaScriptMap jjsmap;
private final JsIncrementalNamerState state;
private final boolean minifyFunctionNames;
public JsIncrementalNamer(JsProgram program, ConfigurationProperties config,
JsIncrementalNamerState state, JavaToJavaScriptMap jjsmap, boolean minifyFunctionNames) {
super(program, config);
this.state = state;
this.jjsmap = jjsmap;
this.minifyFunctionNames = minifyFunctionNames;
}
@Override
protected void reset() {
// Nothing to do.
}
@Override
protected void visit(JsScope scope) throws IllegalNameException {
// Visit children.
for (JsScope child : scope.getChildren()) {
visit(child);
}
// Visit all my idents.
for (JsName name : scope.getAllNames()) {
if (!name.isObfuscatable()) {
// Unobfuscatable names become themselves.
String ident = name.getIdent();
if (ident.endsWith(RESERVED_IDENT_SUFFIX)) {
throw new IllegalNameException("Identifier " + ident + " ends with "
+ RESERVED_IDENT_SUFFIX
+ ". This is not allowed since that suffix is used to separate obfuscatable and "
+ "nonobfuscatable names in per-file compiles.");
}
name.setShortIdent(ident);
continue;
}
name.setShortIdent(getOrCreateIdent(name));
}
}
private String getOrCreateIdent(JsName name) {
String originalIdent = name.getIdent();
String shortIdent = name.getShortIdent();
// Reuse previous names.
if (state.renamedIdentByOriginalIdent.containsKey(originalIdent)) {
return state.renamedIdentByOriginalIdent.get(originalIdent);
}
// If the name is for a method.
if (minifyFunctionNames && jjsmap != null && jjsmap.nameToMethod(name) != null) {
// Come up with an obfuscated name (since OptionDisplayName will show it properly) and cache
// it for reuse.
String obfuscatedIdent = makeObfuscatedIdent();
state.usedIdents.add(obfuscatedIdent);
state.renamedIdentByOriginalIdent.put(originalIdent, obfuscatedIdent);
return obfuscatedIdent;
}
// Otherwise come up with a new pretty name and cache it for reuse.
String prettyIdent = makePrettyName(shortIdent);
state.usedIdents.add(prettyIdent);
state.renamedIdentByOriginalIdent.put(originalIdent, prettyIdent);
return prettyIdent;
}
private String makeObfuscatedIdent() {
while (true) {
String obfuscatedIdent =
JsObfuscateNamer.makeObfuscatedIdent(++state.nextObfuscatedId) + RESERVED_IDENT_SUFFIX;
if (reserved.isAvailable(obfuscatedIdent) && !state.usedIdents.contains(obfuscatedIdent)) {
return obfuscatedIdent;
}
}
}
private String makePrettyName(String shortIdent) {
while (true) {
String prettyIdent = shortIdent + "_" + state.shortIdentCollisionCounts.count(shortIdent)
+ RESERVED_IDENT_SUFFIX;
state.shortIdentCollisionCounts.add(shortIdent);
if (reserved.isAvailable(prettyIdent) && !state.usedIdents.contains(prettyIdent)) {
return prettyIdent;
}
}
}
}