/* * Copyright 2009 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.js.ast.JsBlock; import com.google.gwt.dev.js.ast.JsContext; import com.google.gwt.dev.js.ast.JsFunction; import com.google.gwt.dev.js.ast.JsInvocation; import com.google.gwt.dev.js.ast.JsModVisitor; import com.google.gwt.dev.js.ast.JsName; import com.google.gwt.dev.js.ast.JsNameOf; import com.google.gwt.dev.js.ast.JsNameRef; import com.google.gwt.dev.js.ast.JsProgram; import com.google.gwt.dev.js.ast.JsVisitor; import com.google.gwt.dev.util.collect.Stack; import com.google.gwt.thirdparty.guava.common.collect.Maps; import com.google.gwt.thirdparty.guava.common.collect.Sets; import java.util.Map; import java.util.Set; /** * Replace references to functions which have post-obfuscation duplicate bodies * by reference to a canonical one. Intended to run only when stack trace * stripping is enabled. */ public class JsDuplicateFunctionRemover { private class DuplicateFunctionBodyRecorder extends JsVisitor { private final Set<JsName> dontReplace = Sets.newIdentityHashSet(); private final Map<JsName, JsName> duplicateOriginalMap = Maps.newIdentityHashMap(); private final Map<JsFunction, JsFunction> duplicateMethodOriginalMap = Maps.newLinkedHashMap(); private final Stack<JsNameRef> invocationQualifiers = new Stack<JsNameRef>(); // static / global methods private final Map<String, JsName> uniqueBodies = Maps.newHashMap(); // vtable methods private final Map<String, JsFunction> uniqueMethodBodies = Maps.newHashMap(); public DuplicateFunctionBodyRecorder() { // Add sentinel to stop Stack.peek() from throwing exception. invocationQualifiers.push(null); } @Override public void endVisit(JsInvocation x, JsContext ctx) { if (x.getQualifier() instanceof JsNameRef) { invocationQualifiers.pop(); } } @Override public void endVisit(JsNameOf x, JsContext ctx) { dontReplace.add(x.getName()); } @Override public void endVisit(JsNameRef x, JsContext ctx) { if (x != invocationQualifiers.peek()) { if (x.getName() != null) { dontReplace.add(x.getName()); } } } public Set<JsName> getBlacklist() { return dontReplace; } public Map<JsName, JsName> getDuplicateMap() { return duplicateOriginalMap; } public Map<JsFunction, JsFunction> getDuplicateMethodMap() { return duplicateMethodOriginalMap; } @Override public boolean visit(JsFunction x, JsContext ctx) { String fnSource = x.toSource(); String body = fnSource.substring(fnSource.indexOf("(")); /* * Static function processed separate from virtual functions */ if (x.getName() != null) { JsName original = uniqueBodies.get(body); if (original != null) { duplicateOriginalMap.put(x.getName(), original); } else { uniqueBodies.put(body, x.getName()); } } else if (x.isFromJava()) { JsFunction original = uniqueMethodBodies.get(body); if (original != null) { duplicateMethodOriginalMap.put(x, original); } else { uniqueMethodBodies.put(body, x); } } return true; } @Override public boolean visit(JsInvocation x, JsContext ctx) { if (x.getQualifier() instanceof JsNameRef) { invocationQualifiers.push((JsNameRef) x.getQualifier()); } return true; } } private class ReplaceDuplicateInvocationNameRefs extends JsModVisitor { private final Set<JsName> blacklist; private final Map<JsFunction, JsFunction> dupMethodMap; private final Map<JsFunction, JsName> hoistMap; private final Map<JsName, JsName> duplicateMap; public ReplaceDuplicateInvocationNameRefs(Map<JsName, JsName> duplicateMap, Set<JsName> blacklist, Map<JsFunction, JsFunction> dupMethodMap, Map<JsFunction, JsName> hoistMap) { this.duplicateMap = duplicateMap; this.blacklist = blacklist; this.dupMethodMap = dupMethodMap; this.hoistMap = hoistMap; } @Override public void endVisit(JsFunction x, JsContext ctx) { if (dupMethodMap.containsKey(x)) { ctx.replaceMe(hoistMap.get(dupMethodMap.get(x)).makeRef(x.getSourceInfo())); } else if (hoistMap.containsKey(x)) { ctx.replaceMe(hoistMap.get(x).makeRef(x.getSourceInfo())); } } @Override public void endVisit(JsNameRef x, JsContext ctx) { JsName orig = duplicateMap.get(x.getName()); if (orig != null && x.getName() != null && x.getName().getEnclosing() == program.getScope() && !blacklist.contains(x.getName()) && !blacklist.contains(orig)) { ctx.replaceMe(orig.makeRef(x.getSourceInfo())); } } } /** * Entry point for the removeDuplicateFunctions optimization. * * This optimization will collapse functions whose JavaScript (output) code is identical. After * collapsing duplicate functions it will remove functions that become unreferenced as a result. * * This pass is safe only for JavaScript functions generated from Java where references to * local function variables can not be extruded by returning a function. E,g. in the next example * * function f1() {return a;} * * funcion f2() { var a; return function() {return a;}} * * f1() and the return of f2() are not duplicates even though the have a syntacticaly identical * parameters and body. The reason is that a in f1() refers to some globally scoped variable a, * whereas a in the return of f2() refers to the local variable a. It would be not correct to * move the return of f2() to the global scope. * * This situation does NOT arise from functions that where generated from Java sources (non * native) * * IMPORTANT NOTE: It is NOT safe to rename JsNames after this pass is performed. E.g. * * Consider an output JavaScript for two unrelated classes: * defineClass(...) //class A * _.a * _.m1 = function() { return this.a; } * * defineClass(...) // class B * _.a * _.m2 = function() { return this.a; } * * Here m1() in class A and m2 in class B have identical parameters and bodies; hence the result * will be * * defineClass(...) //class A * _.a * _.m1 = g1 * * defineClass(...) // class B * _.a * _.m2 = g1 * * function g1() { return this.a; } * * The reference to this.a in g1 will be to either A.a or B.a and as long as those names remain * the same the removal was correct. However if A.a gets renamed then A.m1() and B.m2() would * no longer have been identical hence the dedup that is already done is incorrect. * * @param program the program to optimize * @param nameGenerator a freshNameGenerator to assign fresh names to deduped functions that are * lifted to the global scope * @return {@code true} if it made any changes; {@code false} otherwise. */ public static boolean exec(JsProgram program, FreshNameGenerator nameGenerator) { return new JsDuplicateFunctionRemover(program, nameGenerator).execImpl(); } private final JsProgram program; /** * A FreshNameGenerator instance to obtain fresh top scope names consistent with the * naming strategy used. */ private FreshNameGenerator freshNameGenerator; public JsDuplicateFunctionRemover(JsProgram program, FreshNameGenerator freshNameGenerator) { this.program = program; this.freshNameGenerator = freshNameGenerator; } private boolean execImpl() { boolean changed = false; for (int i = 0; i < program.getFragmentCount(); i++) { JsBlock fragment = program.getFragmentBlock(i); DuplicateFunctionBodyRecorder dfbr = new DuplicateFunctionBodyRecorder(); dfbr.accept(fragment); Map<JsFunction, JsName> newNamesByHoistedFunction = Maps.newHashMap(); // Hoist all anonymous duplicate functions. Map<JsFunction, JsFunction> dupMethodMap = dfbr.getDuplicateMethodMap(); for (JsFunction dupMethod : dupMethodMap.values()) { if (newNamesByHoistedFunction.containsKey(dupMethod)) { continue; } // move function to top scope and re-declaring it with a unique name JsName newName = program.getScope().declareName(freshNameGenerator.getFreshName()); JsFunction newFunc = new JsFunction(dupMethod.getSourceInfo(), program.getScope(), newName, dupMethod.isFromJava()); // we're not using the old function anymore, we can use reuse the body // instead of cloning it newFunc.setBody(dupMethod.getBody()); // also copy the parameters from the old function newFunc.getParameters().addAll(dupMethod.getParameters()); // add the new function to the top level list of statements fragment.getStatements().add(newFunc.makeStmt()); newNamesByHoistedFunction.put(dupMethod, newName); } ReplaceDuplicateInvocationNameRefs rdup = new ReplaceDuplicateInvocationNameRefs( dfbr.getDuplicateMap(), dfbr.getBlacklist(), dupMethodMap, newNamesByHoistedFunction); rdup.accept(fragment); changed = changed || rdup.didChange(); } if (changed) { JsUnusedFunctionRemover.exec(program); } return changed; } }