/* * 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; } } } }