blob: 365096dc52ed63264d98589328a077c02ab0ad26 [file] [log] [blame]
/*
* Copyright 2011 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.jjs.JsOutputOption;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.impl.codesplitter.FragmentPartitioningResult;
import com.google.gwt.dev.js.ast.JsProgram;
import com.google.gwt.dev.js.ast.JsProgramFragment;
import com.google.gwt.thirdparty.guava.common.base.Preconditions;
import com.google.gwt.thirdparty.guava.common.base.Throwables;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableList;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import com.google.gwt.thirdparty.guava.common.io.ByteStreams;
import com.google.gwt.thirdparty.javascript.jscomp.CheckLevel;
import com.google.gwt.thirdparty.javascript.jscomp.Compiler;
import com.google.gwt.thirdparty.javascript.jscomp.CompilerInput;
import com.google.gwt.thirdparty.javascript.jscomp.CompilerOptions;
import com.google.gwt.thirdparty.javascript.jscomp.CompilerOptions.Reach;
import com.google.gwt.thirdparty.javascript.jscomp.DiagnosticGroups;
import com.google.gwt.thirdparty.javascript.jscomp.JSError;
import com.google.gwt.thirdparty.javascript.jscomp.JSModule;
import com.google.gwt.thirdparty.javascript.jscomp.JSSourceFile;
import com.google.gwt.thirdparty.javascript.jscomp.PropertyRenamingPolicy;
import com.google.gwt.thirdparty.javascript.jscomp.Result;
import com.google.gwt.thirdparty.javascript.jscomp.SourceAst;
import com.google.gwt.thirdparty.javascript.jscomp.VariableMap;
import com.google.gwt.thirdparty.javascript.jscomp.VariableRenamingPolicy;
import com.google.gwt.thirdparty.javascript.jscomp.WarningLevel;
import com.google.gwt.thirdparty.javascript.rhino.InputId;
import com.google.gwt.thirdparty.javascript.rhino.Node;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* A class that represents an single invocation of the Closure Compiler.
*/
public class ClosureJsRunner {
// The externs expected in externs.zip, in sorted order.
private static final List<String> DEFAULT_EXTERNS_NAMES = ImmutableList.of(
// JS externs
"es3.js",
"es5.js",
// Event APIs
"w3c_event.js", "w3c_event3.js", "w3c_device_sensor_event.js",
"gecko_event.js",
"ie_event.js",
"webkit_event.js",
// DOM apis
"w3c_dom1.js", "w3c_dom2.js", "w3c_dom3.js",
"gecko_dom.js",
"ie_dom.js",
"webkit_dom.js",
// CSS apis
"w3c_css.js",
"gecko_css.js",
"ie_css.js",
"webkit_css.js",
// Top-level namespaces
"chrome.js", "google.js",
"deprecated.js", "fileapi.js", "flash.js", "gears_symbols.js", "gears_types.js",
"gecko_xml.js", "html5.js", "ie_vml.js", "iphone.js", "webstorage.js", "w3c_anim_timing.js",
"w3c_css3d.js", "w3c_elementtraversal.js", "w3c_geolocation.js", "w3c_indexeddb.js",
"w3c_navigation_timing.js", "w3c_range.js", "w3c_selectors.js", "w3c_xml.js", "window.js",
"webkit_notifications.js", "webgl.js");
/**
* @return a mutable list
* @throws IOException
*/
public static List<JSSourceFile> getDefaultExterns() throws IOException {
Class<ClosureJsRunner> clazz = ClosureJsRunner.class;
InputStream input = clazz.getResourceAsStream("/com/google/javascript/jscomp/externs.zip");
if (input == null) {
/*
* HACK - the open source version of the closure compiler maps the resource into a different
* location.
*/
input = clazz.getResourceAsStream("/externs.zip");
}
ZipInputStream zip = new ZipInputStream(input);
Map<String, JSSourceFile> externsMap = Maps.newHashMap();
for (ZipEntry entry = null; (entry = zip.getNextEntry()) != null;) {
BufferedInputStream entryStream =
new BufferedInputStream(ByteStreams.limit(zip, entry.getSize()));
externsMap.put(entry.getName(), JSSourceFile.fromInputStream(
// Give the files an odd prefix, so that they do not conflict
// with the user's files.
"externs.zip//" + entry.getName(), entryStream));
}
Preconditions.checkState(externsMap.keySet().equals(Sets.newHashSet(DEFAULT_EXTERNS_NAMES)),
"Externs zip must match our hard-coded list of externs.");
// Order matters, so the resources must be added to the result list
// in the expected order.
List<JSSourceFile> externs = Lists.newArrayList();
for (String key : DEFAULT_EXTERNS_NAMES) {
externs.add(externsMap.get(key));
}
return externs;
}
/**
* The instance of the Closure Compiler used for the compile.
*/
private Compiler compiler = null;
/**
* The set of external properties discovered in the provided AST.
*/
private Set<String> externalProps = Sets.newHashSet();
/**
* The set of external global variables discovered in the provided AST.
*/
private Set<String> externalVars = Sets.newHashSet();
/**
* The set of internal global variables discovered in the provided AST.
*/
private Set<String> globalVars = Sets.newHashSet();
/**
* Whether AST validation should be performed on the generated
* Closure Compiler AST.
*/
private final boolean validate = true;
/**
* A map of GWT fragment numbers to Closure module indexes.
*/
private int[] closureModuleSequenceMap;
/**
* The number of non-exclusive fragments that are part of the load sequence
* (including the main and leftovers).
*/
private int loadModulesCount;
public ClosureJsRunner() {
}
public void compile(JProgram jprogram, JsProgram program, String[] js,
JsOutputOption jsOutputOption) {
CompilerOptions options;
try {
options = getClosureCompilerOptions(jsOutputOption);
} catch (ParseException e) {
throw new RuntimeException("Error setting closure compiler options", e);
}
// Turn off Closure Compiler logging
Logger.getLogger("com.google.gwt.thirdparty.javascript.jscomp").setLevel(Level.OFF);
// Create a fresh compiler instance.
compiler = new Compiler();
// Translate the ASTs and build the modules
computeFragmentMap(jprogram, program);
List<JSModule> modules = createClosureModules(program);
// Build the externs based on what we discovered building the modules.
List<JSSourceFile> externs = getClosureCompilerExterns();
Result result = compiler.compileModules(externs, modules, options);
if (result.success) {
int fragments = program.getFragmentCount();
for (int i = 0; i < fragments; i++) {
int module = mapFragmentIndexToModuleIndex(i);
js[i] = compiler.toSource(modules.get(module));
}
} else {
for (JSError error : result.errors) {
System.err.println("error optimizing:" + error.toString());
throw new RuntimeException(error.description);
}
}
}
protected List<JSSourceFile> getDefaultExternsList() {
List<JSSourceFile> defaultExterns;
try {
defaultExterns = getDefaultExterns();
return defaultExterns;
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
private void computeFragmentMap(JProgram jprogram, JsProgram jsProgram) {
int fragments = jsProgram.getFragmentCount();
List<Integer> initSeq = jprogram.getInitialFragmentIdSequence();
FragmentPartitioningResult partitionResult = jprogram.getFragmentPartitioningResult();
//
// The fragments are expected in a specific order:
// init, split-1, split-2, ...,
// where the leftovers are dependent on the init module
// and the split modules are dependent on the leftovers
//
// However, Closure Compiler modules must be in dependency order
//
assert closureModuleSequenceMap == null;
closureModuleSequenceMap = new int[fragments];
for (int i = 0; i < fragments; i++) {
closureModuleSequenceMap[i] = -1;
}
int module = 0;
// The initial fragments is always first.
closureModuleSequenceMap[0] = module++;
// Then come the specified load order sequence
for (int i = 0; i < initSeq.size(); i++) {
int initSeqNum = initSeq.get(i);
if (partitionResult != null) {
initSeqNum = partitionResult.getFragmentForRunAsync(initSeqNum);
}
closureModuleSequenceMap[initSeqNum] = module++;
}
// Then the leftovers fragments:
if (fragments > 1) {
int leftoverIndex = fragments - 1;
if (partitionResult != null) {
leftoverIndex = partitionResult.getLeftoverFragmentId();
}
closureModuleSequenceMap[leftoverIndex] = module++;
}
// Finally, the exclusive fragments.
// The order of the remaining fragments doesn't matter.
for (int i = 0; i < fragments; i++) {
if (closureModuleSequenceMap[i] == -1) {
closureModuleSequenceMap[i] = module++;
}
}
loadModulesCount = 1 + initSeq.size() + 1; // main + init sequence + leftovers
}
private CompilerInput createClosureJsAst(JsProgram program, JsProgramFragment fragment,
String source) {
String inputName = source;
InputId inputId = new InputId(inputName);
ClosureJsAstTranslator translator = new ClosureJsAstTranslator(validate, program);
Node root = translator.translate(fragment, inputId, source);
globalVars.addAll(translator.getGlobalVariableNames());
externalProps.addAll(translator.getExternalPropertyReferences());
externalVars.addAll(translator.getExternalVariableReferences());
SourceAst sourceAst = new ClosureJsAst(inputId, root);
CompilerInput input = new CompilerInput(sourceAst, false);
return input;
}
private JSModule createClosureModule(JsProgram program, JsProgramFragment fragment, String source) {
JSModule module = new JSModule(source);
module.add(createClosureJsAst(program, fragment, source));
return module;
}
private List<JSModule> createClosureModules(JsProgram program) {
int fragments = program.getFragmentCount();
JSModule[] modules = new JSModule[fragments];
for (int i = 0; i < fragments; i++) {
modules[mapFragmentIndexToModuleIndex(i)] =
createClosureModule(program, program.getFragment(i), "module" + i);
}
if (fragments > 1) {
//
// The fragments are expected in a specific order:
// init, split-1, split-2, ...,
// where the leftovers are dependent on the init module
// and the split modules are dependent on the leftovers
for (int i = 1; i < loadModulesCount; i++) {
modules[i].addDependency(modules[i - 1]);
}
JSModule leftovers = modules[loadModulesCount - 1];
for (int i = loadModulesCount; i < modules.length; i++) {
Preconditions.checkNotNull(modules[i], "Module: ", i);
modules[i].addDependency(leftovers);
}
}
modules[0].add(JSSourceFile.fromCode("hack", "window['gwtOnLoad'] = gwtOnLoad;\n"));
return Arrays.asList(modules);
}
private List<JSSourceFile> getClosureCompilerExterns() {
List<JSSourceFile> externs = getDefaultExternsList();
externs.add(JSSourceFile.fromCode("gwt_externs",
"var gwtOnLoad;\n"
+ "var $entry;\n"
+ " var $gwt_version;\n"
+ " var $wnd;\n"
+ " var $doc;\n"
+ " var $moduleName\n"
+ " var $moduleBase;\n"
+ " var $gwt\n"
+ " var $strongName;\n"
+ " var $stats;\n"
+ " var $sessionId;\n"
+ " window.prototype.__gwtStatsEvent;\n"
+ " window.prototype.__gwtStatsSessionId;\n"
+ " window.prototype.moduleName;\n"
+ " window.prototype.sessionId;\n"
+ " window.prototype.subSystem;\n"
+ " window.prototype.evtGroup;\n"
+ " window.prototype.millis;\n"
+ " window.prototype.type;\n"
+ " window.prototype.$h;\n"
+ "\n"));
// Generate externs
String generatedExterns = "var gwt_externs;\n";
for (String prop : this.externalProps) {
generatedExterns += "gwt_externs." + prop + ";\n";
}
for (String var : this.externalVars) {
generatedExterns += "var " + var + ";\n";
}
externs.add(JSSourceFile.fromCode("gwt_generated_externs", generatedExterns));
return externs;
}
private CompilerOptions getClosureCompilerOptions(JsOutputOption jsOutputOption)
throws ParseException {
CompilerOptions options = new CompilerOptions();
WarningLevel.QUIET.setOptionsForWarningLevel(options);
// Basically, use CompilationLevel.ADVANCED_OPTIMIZATIONS:
// Build an identity map of variable names to prevent GWT names from
// being renamed while allowing new global variables to be renamed.
HashMap<String, String> varNames = new HashMap<String, String>();
for (String var : globalVars) {
varNames.put(var, var);
}
options.setInputVariableMapSerialized(VariableMap.fromMap(varNames).toBytes());
if (jsOutputOption == JsOutputOption.OBFUSCATED) {
options.setRenamingPolicy(VariableRenamingPolicy.ALL, PropertyRenamingPolicy.OFF);
options.setPrettyPrint(false);
// This can help debug renaming policy changes.
// options.generatePseudoNames = true;
} else {
options.setRenamingPolicy(VariableRenamingPolicy.OFF, PropertyRenamingPolicy.OFF);
options.setPrettyPrint(true);
}
// All the safe optimizations.
options.setClosurePass(true);
options.setFoldConstants(true);
options.setCoalesceVariableNames(true);
options.setDeadAssignmentElimination(true);
options.setExtractPrototypeMemberDeclarations(true);
options.setCollapseVariableDeclarations(true);
options.setConvertToDottedProperties(true);
options.setRewriteFunctionExpressions(true);
options.setLabelRenaming(true);
options.setRemoveDeadCode(true);
options.setOptimizeArgumentsArray(true);
options.setCollapseObjectLiterals(true);
options.setShadowVariables(true);
// All the advance optimizations.
options.setReserveRawExports(true);
options.setRemoveUnusedPrototypeProperties(true);
options.setCollapseAnonymousFunctions(true);
options.setSmartNameRemoval(true); // ?
options.setInlineConstantVars(true);
options.setInlineFunctions(Reach.ALL);
options.setInlineGetters(true);
options.setInlineVariables(Reach.ALL);
options.setFlowSensitiveInlineVariables(true);
options.setComputeFunctionSideEffects(true);
// Remove unused vars also removes unused functions.
options.setRemoveUnusedVariable(Reach.ALL);
options.setOptimizeParameters(true);
options.setOptimizeReturns(true);
options.setOptimizeCalls(true);
// Maybe turn these off as well
options.setCollapseProperties(true); // ?
options.setCrossModuleCodeMotion(true); // ?
options.setCrossModuleMethodMotion(true); // ?
options.setDevirtualizePrototypeMethods(true); // ?
// Advanced optimization, disabled
options.setRemoveClosureAsserts(false);
options.setAliasKeywords(false);
options.setRemoveUnusedPrototypePropertiesInExterns(false);
options.setCheckGlobalThisLevel(CheckLevel.OFF);
options.setRewriteFunctionExpressions(false); // Performance hit
// Kindly tell the user that they have JsDocs that we don't understand.
options.setWarningLevel(DiagnosticGroups.NON_STANDARD_JSDOC, CheckLevel.OFF);
return options;
}
private int mapFragmentIndexToModuleIndex(int index) {
assert closureModuleSequenceMap.length > index;
return closureModuleSequenceMap[index];
}
}