com.google.javascript.jscomp.AbstractCommandLineRunner Maven / Gradle / Ivy
Show all versions of com.liferay.frontend.js.minifier
/*
* Copyright 2009 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.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.lang.Math.min;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.io.ByteStreams;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.google.javascript.jscomp.CompilerOptions.JsonStreamMode;
import com.google.javascript.jscomp.CompilerOptions.OutputJs;
import com.google.javascript.jscomp.CompilerOptions.TweakProcessing;
import com.google.javascript.jscomp.deps.ModuleLoader;
import com.google.javascript.jscomp.deps.SourceCodeEscapers;
import com.google.javascript.jscomp.ijs.IjsErrors;
import com.google.javascript.jscomp.parsing.Config;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.StaticSourceFile.SourceKind;
import com.google.javascript.rhino.TokenStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Flushable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.annotation.Nullable;
/**
* Implementations of AbstractCommandLineRunner translate flags into Java API calls on the Compiler.
* AbstractCompiler contains common flags and logic to make that happen.
*
* This class may be extended and used to create other Java classes that behave the same as
* running the Compiler from the command line. Example:
*
*
* class MyCommandLineRunner extends
* AbstractCommandLineRunner<MyCompiler, MyOptions> {
* MyCommandLineRunner(String[] args) {
* super(args);
* }
*
* @Override
* protected MyOptions createOptions() {
* MyOptions options = new MyOptions();
* CompilerFlagTranslator.setOptionsFromFlags(options);
* addMyCrazyCompilerPassThatOutputsAnExtraFile(options);
* return options;
* }
*
* @Override
* protected MyCompiler createCompiler() {
* return new MyCompiler();
* }
*
* public static void main(String[] args) {
* (new MyCommandLineRunner(args)).run();
* }
* }
*
*/
public abstract class AbstractCommandLineRunner {
static final DiagnosticType OUTPUT_SAME_AS_INPUT_ERROR =
DiagnosticType.error(
"JSC_OUTPUT_SAME_AS_INPUT_ERROR", "Bad output file (already listed as input file): {0}");
static final DiagnosticType COULD_NOT_SERIALIZE_AST =
DiagnosticType.error("JSC_COULD_NOT_SERIALIZE_AST", "Could not serialize ast to: {0}");
static final DiagnosticType COULD_NOT_DESERIALIZE_AST =
DiagnosticType.error("JSC_COULD_NOT_DESERIALIZE_AST", "Could not deserialize ast from: {0}");
static final DiagnosticType NO_TREE_GENERATED_ERROR =
DiagnosticType.error(
"JSC_NO_TREE_GENERATED_ERROR", "Code contains errors. No tree was generated.");
static final DiagnosticType INVALID_MODULE_SOURCEMAP_PATTERN =
DiagnosticType.error(
"JSC_INVALID_MODULE_SOURCEMAP_PATTERN",
"When using --module flags, the --create_source_map flag must contain "
+ "%outname% in the value.");
static final String WAITING_FOR_INPUT_WARNING = "The compiler is waiting for input via stdin.";
@GwtIncompatible("Unnecessary")
private final CommandLineConfig config;
@GwtIncompatible("Unnecessary")
private final InputStream in;
@GwtIncompatible("Unnecessary")
private final PrintStream defaultJsOutput;
@GwtIncompatible("Unnecessary")
private final PrintStream err;
@GwtIncompatible("Unnecessary")
private A compiler;
@GwtIncompatible("Unnecessary")
private Charset inputCharset;
// NOTE(nicksantos): JSCompiler has always used ASCII as the default
// output charset. This was done to solve legacy problems with
// bad proxies, etc. We are not sure if these issues are still problems,
// and changing the encoding would require a big obnoxious migration plan.
//
// New outputs should use outputCharset2, which is how I would have
// designed this if I had a time machine.
@GwtIncompatible("Unnecessary")
private Charset outputCharset2;
@GwtIncompatible("Unnecessary")
private Charset legacyOutputCharset;
@GwtIncompatible("Unnecessary")
private boolean testMode = false;
@GwtIncompatible("Unnecessary")
private Supplier> externsSupplierForTesting = null;
@GwtIncompatible("Unnecessary")
private Supplier> inputsSupplierForTesting = null;
@GwtIncompatible("Unnecessary")
private Supplier> modulesSupplierForTesting = null;
@GwtIncompatible("Unnecessary")
private Function exitCodeReceiver = SystemExitCodeReceiver.INSTANCE;
@GwtIncompatible("Unnecessary")
private Map rootRelativePathsMap = null;
@GwtIncompatible("Unnecessary")
private Map parsedModuleWrappers = null;
@GwtIncompatible("Unnecessary")
private Map parsedModuleOutputFiles = null;
@GwtIncompatible("Unnecessary")
private final Gson gson;
static final String OUTPUT_MARKER = "%output%";
private static final String OUTPUT_MARKER_JS_STRING = "%output|jsstring%";
@GwtIncompatible("Unnecessary")
private final List filesToStreamOut = new ArrayList<>();
@GwtIncompatible("Unnecessary")
AbstractCommandLineRunner() {
this(System.in, System.out, System.err);
}
@GwtIncompatible("Unnecessary")
AbstractCommandLineRunner(PrintStream out, PrintStream err) {
this(System.in, out, err);
}
@GwtIncompatible("Unnecessary")
AbstractCommandLineRunner(InputStream in, PrintStream out, PrintStream err) {
this.config = new CommandLineConfig();
this.in = checkNotNull(in);
this.defaultJsOutput = checkNotNull(out);
this.err = checkNotNull(err);
this.gson = new Gson();
}
/**
* Put the command line runner into test mode. In test mode, all outputs will be blackholed.
*
* @param externsSupplier A provider for externs.
* @param inputsSupplier A provider for source inputs.
* @param modulesSupplier A provider for modules. Only one of inputsSupplier and modulesSupplier
* may be non-null.
* @param exitCodeReceiver A receiver for the status code that would have been passed to
* System.exit in non-test mode.
*/
@VisibleForTesting
@GwtIncompatible("Unnecessary")
void enableTestMode(
Supplier> externsSupplier,
Supplier> inputsSupplier,
Supplier> modulesSupplier,
Function exitCodeReceiver) {
checkArgument(inputsSupplier == null != (modulesSupplier == null));
testMode = true;
this.externsSupplierForTesting = externsSupplier;
this.inputsSupplierForTesting = inputsSupplier;
this.modulesSupplierForTesting = modulesSupplier;
this.exitCodeReceiver = exitCodeReceiver;
}
/**
* @param newExitCodeReceiver receives a non-zero integer to indicate a problem during execution
* or 0i to indicate success.
*/
@GwtIncompatible("Unnecessary")
public void setExitCodeReceiver(Function newExitCodeReceiver) {
this.exitCodeReceiver = checkNotNull(newExitCodeReceiver);
}
/** Returns whether we're in test mode. */
@GwtIncompatible("Unnecessary")
protected boolean isInTestMode() {
return testMode;
}
/** Returns whether output should be a JSON stream. */
@GwtIncompatible("Unnecessary")
private boolean isOutputInJson() {
return config.jsonStreamMode == JsonStreamMode.OUT
|| config.jsonStreamMode == JsonStreamMode.BOTH;
}
/** Get the command line config, so that it can be initialized. */
@GwtIncompatible("Unnecessary")
protected CommandLineConfig getCommandLineConfig() {
return config;
}
/** Returns the instance of the Compiler to use when {@link #run()} is called. */
@GwtIncompatible("Unnecessary")
protected abstract A createCompiler();
/**
* Performs any transformation needed on the given compiler input and appends it to the given
* output bundle.
*/
@GwtIncompatible("Unnecessary")
protected abstract void prepForBundleAndAppendTo(
Appendable out, CompilerInput input, String content) throws IOException;
/** Writes whatever runtime libraries are needed to bundle. */
@GwtIncompatible("Unnecessary")
protected abstract void appendRuntimeTo(Appendable out) throws IOException;
/**
* Returns the instance of the Options to use when {@link #run()} is called. createCompiler() is
* called before createOptions(), so getCompiler() will not return null when createOptions() is
* called.
*/
@GwtIncompatible("Unnecessary")
protected abstract B createOptions();
/** The warning classes that are available from the command-line. */
@GwtIncompatible("Unnecessary")
protected DiagnosticGroups getDiagnosticGroups() {
if (compiler == null) {
return new DiagnosticGroups();
}
return compiler.getDiagnosticGroups();
}
@GwtIncompatible("Unnecessary")
protected abstract void addAllowlistWarningsGuard(CompilerOptions options, File allowlistFile);
@GwtIncompatible("Unnecessary")
protected static void setWarningGuardOptions(
CompilerOptions options,
ArrayList> warningGuards,
DiagnosticGroups diagnosticGroups) {
if (warningGuards != null) {
final Set groupNames = DiagnosticGroups.getRegisteredGroups().keySet();
for (FlagEntry entry : warningGuards) {
if ("*".equals(entry.value)) {
for (String groupName : groupNames) {
if (!DiagnosticGroups.wildcardExcludedGroups.contains(groupName)) {
diagnosticGroups.setWarningLevel(options, groupName, entry.flag);
}
}
} else if (groupNames.contains(entry.value)) {
diagnosticGroups.setWarningLevel(options, entry.value, entry.flag);
} else {
throw new FlagUsageException("Unknown diagnostic group: '" + entry.value + "'");
}
}
}
}
/**
* Sets options based on the configurations set flags API. Called during the run() run() method.
* If you want to ignore the flags API, or interpret flags your own way, then you should override
* this method.
*/
@GwtIncompatible("Unnecessary")
protected void setRunOptions(CompilerOptions options) throws IOException {
DiagnosticGroups diagnosticGroups = getDiagnosticGroups();
if (config.shouldSaveAfterStage1() || config.shouldContinueCompilation()) {
if (options.checksOnly) {
throw new FlagUsageException(
"checks_only mode is incompatible with multi-stage compilation");
}
if (options.getExternExportsPath() != null) {
throw new FlagUsageException(
"generating externs from exports is incompatible with multi-stage compilation");
}
}
setWarningGuardOptions(options, config.warningGuards, diagnosticGroups);
if (!config.warningsAllowFile.isEmpty()) {
addAllowlistWarningsGuard(options, new File(config.warningsAllowFile));
}
if (!config.hideWarningsFor.isEmpty()) {
options.addWarningsGuard(
new ShowByPathWarningsGuard(
config.hideWarningsFor.toArray(new String[] {}),
ShowByPathWarningsGuard.ShowType.EXCLUDE));
}
List define = new ArrayList<>(config.define);
if (config.browserFeaturesetYear != 0) {
try {
options.setBrowserFeaturesetYear(config.browserFeaturesetYear);
} catch (IllegalStateException e) {
throw new FlagUsageException(e.getMessage());
}
}
if (config.emitAsyncFunctionsWithZonejs) {
options.setAllowZoneJsWithAsyncFunctionsInOutput(true);
}
createDefineReplacements(define, options);
options.setTweakProcessing(config.tweakProcessing);
// TODO(tjgq): Unconditionally set the options.
if (config.dependencyOptions != null) {
options.setDependencyOptions(config.dependencyOptions);
}
options.devMode = config.jscompDevMode;
options.setCodingConvention(config.codingConvention);
options.setSummaryDetailLevel(config.summaryDetailLevel);
options.setTrustedStrings(true);
legacyOutputCharset = options.outputCharset = getLegacyOutputCharset();
outputCharset2 = getOutputCharset2();
inputCharset = getInputCharset();
if (config.jsOutputFile.length() > 0) {
if (config.skipNormalOutputs) {
throw new FlagUsageException(
"skip_normal_outputs and js_output_file" + " cannot be used together.");
}
}
if (config.skipNormalOutputs && config.printAst) {
throw new FlagUsageException(
"skip_normal_outputs and print_ast cannot" + " be used together.");
}
if (config.skipNormalOutputs && config.printTree) {
throw new FlagUsageException(
"skip_normal_outputs and print_tree cannot" + " be used together.");
}
if (config.createSourceMap.length() > 0) {
options.sourceMapOutputPath = config.createSourceMap;
} else if (isOutputInJson()) {
options.sourceMapOutputPath = "%outname%";
}
options.sourceMapDetailLevel = config.sourceMapDetailLevel;
options.sourceMapFormat = config.sourceMapFormat;
options.sourceMapLocationMappings = config.sourceMapLocationMappings;
options.parseInlineSourceMaps = config.parseInlineSourceMaps;
options.applyInputSourceMaps = config.applyInputSourceMaps;
ImmutableMap.Builder inputSourceMaps = new ImmutableMap.Builder<>();
for (Map.Entry files : config.sourceMapInputFiles.entrySet()) {
SourceFile sourceMap =
SourceFile.builder().withKind(SourceKind.NON_CODE).withPath(files.getValue()).build();
inputSourceMaps.put(files.getKey(), new SourceMapInput(sourceMap));
}
options.inputSourceMaps = inputSourceMaps.buildOrThrow();
if (!config.variableMapInputFile.isEmpty()) {
options.inputVariableMap = VariableMap.load(config.variableMapInputFile);
}
if (!config.propertyMapInputFile.isEmpty()) {
options.inputPropertyMap = VariableMap.load(config.propertyMapInputFile);
}
if (!config.outputManifests.isEmpty()) {
Set uniqueNames = new HashSet<>();
for (String filename : config.outputManifests) {
if (!uniqueNames.add(filename)) {
throw new FlagUsageException(
"output_manifest flags specify duplicate file names: " + filename);
}
}
}
if (!config.outputBundles.isEmpty()) {
Set uniqueNames = new HashSet<>();
for (String filename : config.outputBundles) {
if (!uniqueNames.add(filename)) {
throw new FlagUsageException(
"output_bundle flags specify duplicate file names: " + filename);
}
}
}
options.transformAMDToCJSModules = config.transformAMDToCJSModules;
options.setProcessCommonJSModules(config.processCommonJSModules);
options.moduleRoots = config.moduleRoots;
options.angularPass = config.angularPass;
if (!config.jsonWarningsFile.isEmpty()) {
options.addReportGenerator(
new JsonErrorReportGenerator(new PrintStream(config.jsonWarningsFile), compiler));
}
if (config.errorFormat == CommandLineConfig.ErrorFormatOption.JSON) {
JsonErrorReportGenerator errorGenerator =
new JsonErrorReportGenerator(getErrorPrintStream(), compiler);
compiler.setErrorManager(new SortingErrorManager(ImmutableSet.of(errorGenerator)));
}
if (config.printTree) {
options.setParseJsDocDocumentation(Config.JsDocParsing.INCLUDE_ALL_COMMENTS);
}
}
@GwtIncompatible("Unnecessary")
protected final A getCompiler() {
return compiler;
}
/**
* @return a mutable list
* @throws IOException
*/
@GwtIncompatible("Unnecessary")
public static List getBuiltinExterns(CompilerOptions.Environment env)
throws IOException {
try (InputStream input = getExternsInput()) {
ZipInputStream zip = new ZipInputStream(input);
String envPrefix = Ascii.toLowerCase(env.toString()) + "/";
Map mapFromExternsZip = new HashMap<>();
for (ZipEntry entry = null; (entry = zip.getNextEntry()) != null; ) {
String filename = entry.getName();
// Always load externs in the root folder.
// If the non-core-JS externs are organized in subfolders, only load
// the ones in a subfolder matching the specified environment. Strip the subfolder.
if (filename.contains("/")) {
if (!filename.startsWith(envPrefix)) {
continue;
}
filename = filename.substring(envPrefix.length()); // remove envPrefix, including '/'
}
BufferedInputStream entryStream =
new BufferedInputStream(ByteStreams.limit(zip, entry.getSize()));
mapFromExternsZip.put(
filename,
// Give the files an odd prefix, so that they do not conflict
// with the user's files.
SourceFile.builder()
.withPath("externs.zip//" + filename)
.withContent(entryStream)
.build());
}
return DefaultExterns.prepareExterns(env, mapFromExternsZip);
}
}
/**
* Some text identifying this binary and its version.
*
* At minimum, this is what will be printed when `--version` is passed.
*/
protected abstract String getVersionText();
@GwtIncompatible("Unnecessary")
private static InputStream getExternsInput() {
InputStream input = AbstractCommandLineRunner.class.getResourceAsStream("/externs.zip");
if (input == null) {
// In some environments, the externs.zip is relative to this class.
input = AbstractCommandLineRunner.class.getResourceAsStream("externs.zip");
}
checkNotNull(input);
return input;
}
/** Runs the Compiler and calls System.exit() with the exit status of the compiler. */
@GwtIncompatible("Unnecessary")
public final void run() {
int result;
try {
result = doRun();
} catch (FlagUsageException e) {
err.println(e.getMessage());
result = -1;
} catch (Throwable t) {
t.printStackTrace(err);
result = -2;
}
exitCodeReceiver.apply(result);
}
/** Returns the PrintStream for writing errors associated with this AbstractCommandLineRunner. */
@GwtIncompatible("Unnecessary")
protected final PrintStream getErrorPrintStream() {
return err;
}
@GwtIncompatible("Unnecessary")
public List parseJsonFilesFromInputStream() throws IOException {
List jsonFiles = new ArrayList<>();
try (JsonReader reader = new JsonReader(new InputStreamReader(this.in, inputCharset))) {
reader.beginArray();
while (reader.hasNext()) {
JsonFileSpec jsonFile = gson.fromJson(reader, JsonFileSpec.class);
jsonFiles.add(jsonFile);
}
reader.endArray();
}
return jsonFiles;
}
/**
* Creates inputs from a list of files.
*
* @param files A list of flag entries indicates js and zip file names.
* @param allowStdIn Whether '-' is allowed appear as a filename to represent stdin. If true, '-'
* is only allowed to appear once.
* @param jsChunkSpecs A list chunk specs.
* @return An array of inputs
*/
@GwtIncompatible("Unnecessary")
private List createInputs(
List> files, boolean allowStdIn, List jsChunkSpecs)
throws IOException {
return createInputs(files, /* jsonFiles= */ null, allowStdIn, jsChunkSpecs);
}
/**
* Creates inputs from a list of source files and json files.
*
* @param files A list of flag entries indicates js and zip file names.
* @param jsonFiles A list of json encoded files.
* @param jsChunkSpecs A list chunk specs.
* @return An array of inputs
*/
@GwtIncompatible("Unnecessary")
private List createInputs(
List> files,
List jsonFiles,
List jsChunkSpecs)
throws IOException {
return createInputs(files, jsonFiles, /* allowStdIn= */ false, jsChunkSpecs);
}
/**
* Creates inputs from a list of source files, zips and json files.
*
* Can be overridden by subclasses who want to pull files from different places.
*
* @param files A list of flag entries indicates js and zip file names
* @param jsonFiles A list of json encoded files.
* @param allowStdIn Whether '-' is allowed appear as a filename to represent stdin. If true, '-'
* is only allowed to appear once.
* @param jsChunkSpecs A list chunk specs.
* @return An array of inputs
*/
@GwtIncompatible("Unnecessary")
protected List createInputs(
List> files,
List jsonFiles,
boolean allowStdIn,
List jsChunkSpecs)
throws IOException {
List inputs = new ArrayList<>(files.size());
boolean usingStdin = false;
int jsChunkIndex = 0;
JsChunkSpec jsChunkSpec = Iterables.getFirst(jsChunkSpecs, null);
int cumulatedInputFilesExpected =
jsChunkSpec == null ? Integer.MAX_VALUE : jsChunkSpec.getNumInputs();
for (int i = 0; i < files.size(); i++) {
FlagEntry file = files.get(i);
String filename = file.value;
if (file.flag == JsSourceType.JS_ZIP) {
if (this.config.typedAstListInputFilename != null) {
throw new FlagUsageException("Can't use TypedASTs with --zip.");
}
if (!"-".equals(filename)) {
List newFiles = SourceFile.fromZipFile(filename, inputCharset);
// Update the manifest maps for new zip entries.
if (rootRelativePathsMap.containsKey(filename)) {
String rootFilename = rootRelativePathsMap.get(filename);
for (SourceFile zipEntry : newFiles) {
String zipEntryName = zipEntry.getName();
checkState(zipEntryName.contains(filename));
String zipmap = zipEntryName.replace(filename, rootFilename);
rootRelativePathsMap.put(zipEntryName, zipmap);
}
}
inputs.addAll(newFiles);
if (jsChunkSpec != null) {
jsChunkSpec.numJsFiles += newFiles.size() - 1;
}
}
} else if (!"-".equals(filename)) {
SourceKind kind = file.flag == JsSourceType.WEAKDEP ? SourceKind.WEAK : SourceKind.STRONG;
SourceFile newFile =
SourceFile.builder()
.withPath(filename)
.withCharset(inputCharset)
.withKind(kind)
.build();
inputs.add(newFile);
} else {
if (this.config.typedAstListInputFilename != null) {
throw new FlagUsageException("Can't use TypedASTs with stdin.");
}
if (!config.defaultToStdin) {
throw new FlagUsageException("Can't specify stdin.");
}
if (usingStdin) {
throw new FlagUsageException("Can't specify stdin twice.");
}
if (!config.outputManifests.isEmpty()) {
throw new FlagUsageException(
"Manifest files cannot be generated when the input is from stdin.");
}
if (!config.outputBundles.isEmpty()) {
throw new FlagUsageException(
"Bundle files cannot be generated when the input is from stdin.");
}
this.err.println(WAITING_FOR_INPUT_WARNING);
inputs.add(
SourceFile.builder()
.withPath("stdin")
.withContent(this.in)
.withCharset(inputCharset)
.build());
usingStdin = true;
}
if (i >= cumulatedInputFilesExpected - 1) {
jsChunkIndex++;
if (jsChunkIndex < jsChunkSpecs.size()) {
jsChunkSpec = jsChunkSpecs.get(jsChunkIndex);
cumulatedInputFilesExpected += jsChunkSpec.getNumInputs();
}
}
}
if (jsonFiles != null) {
for (JsonFileSpec jsonFile : jsonFiles) {
inputs.add(SourceFile.fromCode(jsonFile.getPath(), jsonFile.getSrc()));
}
}
return inputs;
}
/**
* Removes input --ijs files whose basename matches an input --js or --weakdep file.
*
* @throws FlagUsageException If there are both input --ijs files and module specs.
*/
@GwtIncompatible("Unnecessary")
private ImmutableList deduplicateIjsFiles(
List> files, List moduleRoots, boolean hasModuleSpecs) {
ImmutableList.Builder errors = ImmutableList.builder();
// First pass: collect the (module root relative) names of --js and --weakdep files.
Map relativeToAbsoluteName = new HashMap<>();
for (FlagEntry file : files) {
// TODO(tjgq): Handle zip files.
if (file.flag == JsSourceType.JS || file.flag == JsSourceType.WEAKDEP) {
String absoluteName = file.value;
String relativeName = getModuleRootRelativeName(absoluteName, moduleRoots);
relativeToAbsoluteName.put(relativeName, absoluteName);
}
}
// Second pass: drop --ijs files whose (module root relative) name matches a --js or --weakdep
// file.
Iterator> iterator = files.iterator();
while (iterator.hasNext()) {
FlagEntry file = iterator.next();
if (file.flag == JsSourceType.IJS) {
if (hasModuleSpecs) {
throw new FlagUsageException("--ijs is incompatible with --chunk or --module.");
}
String absoluteName = file.value;
if (!absoluteName.endsWith(".i.js")) {
errors.add(JSError.make(IjsErrors.BAD_IJS_FILE_NAME, absoluteName));
continue;
}
String relativeName = getModuleRootRelativeName(absoluteName, moduleRoots);
String relativeNonIjsName =
relativeName.substring(0, relativeName.length() - ".i.js".length());
if (relativeToAbsoluteName.containsKey(relativeNonIjsName)) {
errors.add(
JSError.make(
IjsErrors.CONFLICTING_IJS_FILE,
relativeToAbsoluteName.get(relativeNonIjsName),
absoluteName));
iterator.remove();
}
}
}
return errors.build();
}
private String getModuleRootRelativeName(String filename, List moduleRoots) {
for (String moduleRoot : moduleRoots) {
if (filename.startsWith(moduleRoot + "/")) {
return filename.substring(moduleRoot.length() + 1);
}
}
return filename;
}
/** Creates JS source code inputs from a list of files. */
@GwtIncompatible("Unnecessary")
private List createSourceInputs(
List jsChunkSpecs,
List> files,
List jsonFiles,
List moduleRoots)
throws IOException {
if (isInTestMode()) {
return inputsSupplierForTesting != null ? inputsSupplierForTesting.get() : null;
}
if (files.isEmpty() && jsonFiles == null && config.defaultToStdin) {
// Request to read from stdin.
files = ImmutableList.of(new FlagEntry(JsSourceType.JS, "-"));
}
for (JSError error : deduplicateIjsFiles(files, moduleRoots, !jsChunkSpecs.isEmpty())) {
compiler.report(error);
}
try {
if (jsonFiles != null) {
return createInputs(files, jsonFiles, jsChunkSpecs);
} else {
return createInputs(files, true, jsChunkSpecs);
}
} catch (FlagUsageException e) {
throw new FlagUsageException("Bad --js flag. " + e.getMessage());
}
}
/** Creates JS extern inputs from a list of files. */
@GwtIncompatible("Unnecessary")
private List createExternInputs(List files) throws IOException {
List> externFiles = new ArrayList<>();
for (String file : files) {
externFiles.add(new FlagEntry(JsSourceType.EXTERN, file));
}
try {
return createInputs(externFiles, false, new ArrayList());
} catch (FlagUsageException e) {
throw new FlagUsageException("Bad --externs flag. " + e.getMessage());
}
}
/**
* Creates module objects from a list of chunk specifications.
*
* @param specs A list of chunk specifications, not null or empty.
* @param inputs A list of JS file paths, not null
* @return An array of module objects
*/
public static List createJsModules(List specs, List inputs)
throws IOException {
checkState(specs != null);
checkState(!specs.isEmpty());
checkState(inputs != null);
List moduleNames = new ArrayList<>(specs.size());
Map modulesByName = new LinkedHashMap<>();
Map modulesFileCountMap = new LinkedHashMap<>();
int numJsFilesExpected = 0;
int minJsFilesRequired = 0;
for (JsChunkSpec spec : specs) {
if (modulesByName.containsKey(spec.name)) {
throw new FlagUsageException("Duplicate module name: " + spec.name);
}
JSChunk module = new JSChunk(spec.name);
for (String dep : spec.deps) {
JSChunk other = modulesByName.get(dep);
if (other == null) {
throw new FlagUsageException(
"Module '"
+ spec.name
+ "' depends on unknown module '"
+ dep
+ "'. Be sure to list modules in dependency order.");
}
module.addDependency(other);
}
// We will allow modules of zero input.
if (spec.numJsFiles < 0) {
numJsFilesExpected = -1;
} else {
minJsFilesRequired += spec.numJsFiles;
}
if (numJsFilesExpected >= 0) {
numJsFilesExpected += spec.numJsFiles;
}
// Add modules in reverse order so that source files are allocated to
// modules in reverse order. This allows the first module
// (presumably the base module) to have a size of 'auto'
moduleNames.add(0, spec.name);
modulesFileCountMap.put(spec.name, spec.numJsFiles);
modulesByName.put(spec.name, module);
}
final int totalNumJsFiles = inputs.size();
if (numJsFilesExpected >= 0 || minJsFilesRequired > totalNumJsFiles) {
if (minJsFilesRequired > totalNumJsFiles) {
numJsFilesExpected = minJsFilesRequired;
}
if (numJsFilesExpected > totalNumJsFiles) {
throw new FlagUsageException(
"Not enough JS files specified. Expected "
+ numJsFilesExpected
+ " but found "
+ totalNumJsFiles);
} else if (numJsFilesExpected < totalNumJsFiles) {
throw new FlagUsageException(
"Too many JS files specified. Expected "
+ numJsFilesExpected
+ " but found "
+ totalNumJsFiles);
}
}
int numJsFilesLeft = totalNumJsFiles;
int moduleIndex = 0;
for (String moduleName : moduleNames) {
// Parse module inputs.
int numJsFiles = modulesFileCountMap.get(moduleName);
JSChunk module = modulesByName.get(moduleName);
// Check if the first chunk specified 'auto' for the number of files
if (moduleIndex == moduleNames.size() - 1 && numJsFiles == -1) {
numJsFiles = numJsFilesLeft;
}
List moduleFiles = inputs.subList(numJsFilesLeft - numJsFiles, numJsFilesLeft);
for (CompilerInput input : moduleFiles) {
module.add(input);
}
numJsFilesLeft -= numJsFiles;
moduleIndex++;
}
return new ArrayList<>(modulesByName.values());
}
/**
* Validates the module name. Can be overridden by subclasses.
*
* @param name The module name
*/
@GwtIncompatible("Unnecessary")
protected void checkModuleName(String name) {
if (!TokenStream.isJSIdentifier(name)) {
throw new FlagUsageException("Invalid module name: '" + name + "'");
}
}
/**
* Parses module wrapper specifications.
*
* @param specs A list of module wrapper specifications, not null. The spec format is:
* name:wrapper
. Wrappers.
* @param chunks The JS chunks whose wrappers are specified
* @return A map from module name to module wrapper. Modules with no wrapper will have the empty
* string as their value in this map.
*/
public static Map parseModuleWrappers(
List specs, Iterable chunks) {
checkState(specs != null);
Map wrappers = new HashMap<>();
// Prepopulate the map with module names.
for (JSChunk c : chunks) {
wrappers.put(c.getName(), "");
}
for (String spec : specs) {
// Format is ":".
int pos = spec.indexOf(':');
if (pos == -1) {
throw new FlagUsageException(
"Expected module wrapper to have " + ": format: " + spec);
}
// Parse module name.
String name = spec.substring(0, pos);
if (!wrappers.containsKey(name)) {
throw new FlagUsageException("Unknown module: '" + name + "'");
}
String wrapper = spec.substring(pos + 1);
// Support for %n% and %output%
wrapper = wrapper.replace("%output%", "%s").replace("%n%", "\n");
if (!wrapper.contains("%s")) {
throw new FlagUsageException("No %s placeholder in module wrapper: '" + wrapper + "'");
}
wrappers.put(name, wrapper);
}
return wrappers;
}
/**
* Parses module output name specifications.
*
* @param specs A list of module output name specifications, not null. The spec format is: {@code
* name:output_file}.
* @return A map from module name to module output file name, if declared. Modules with no
* predeclared output file name will have no entry in this map.
*/
private static ImmutableMap parseModuleOutputFiles(List specs) {
checkArgument(specs != null);
ImmutableMap.Builder outputFilesBuilder = ImmutableMap.builder();
for (String spec : specs) {
// Format is ":".
int pos = spec.indexOf(':');
if (pos == -1) {
throw new FlagUsageException(
"Expected chunk_output_file to have " + ": format: " + spec);
}
String name = spec.substring(0, pos);
String filename = spec.substring(pos + 1);
outputFilesBuilder.put(name, filename);
}
return outputFilesBuilder.buildOrThrow();
}
/**
* Returns the output file name for a chunk.
*
* For chunks with predeclared output file names specified using {@code --chunk_output_file},
* the the output file name is {@code /}
*
* Otherwise, the output file name is {@code /.js}
*/
@GwtIncompatible("Unnecessary")
@VisibleForTesting
String getModuleOutputFileName(JSChunk m) {
if (parsedModuleOutputFiles == null) {
parsedModuleOutputFiles = parseModuleOutputFiles(config.moduleOutputFiles);
}
String outputFile = parsedModuleOutputFiles.get(m.getName());
if (outputFile != null) {
return config.moduleOutputPathPrefix + outputFile;
}
return config.moduleOutputPathPrefix + m.getName() + ".js";
}
@VisibleForTesting
@GwtIncompatible("Unnecessary")
void writeModuleOutput(String fileName, Appendable out, JSChunk m) throws IOException {
if (parsedModuleWrappers == null) {
parsedModuleWrappers =
parseModuleWrappers(
config.moduleWrapper, ImmutableList.copyOf(compiler.getModuleGraph().getAllChunks()));
}
if (!isOutputInJson()) {
maybeCreateDirsForPath(fileName);
}
String baseName = new File(fileName).getName();
writeOutput(
out,
compiler,
m,
parsedModuleWrappers.get(m.getName()).replace("%basename%", baseName),
"%s",
null,
fileName);
}
/**
* Writes code to an output stream, optionally wrapping it in an arbitrary wrapper that contains a
* placeholder where the code should be inserted.
*
* @param module Which module to write. If this is null, write the entire AST.
*/
@GwtIncompatible("Unnecessary")
void writeOutput(
Appendable out,
Compiler compiler,
@Nullable JSChunk module,
String wrapper,
String codePlaceholder,
@Nullable Function escaper,
String filename)
throws IOException {
if (compiler.getOptions().outputJs == OutputJs.SENTINEL) {
out.append("// No JS output because the compiler was run in checks-only mode.\n");
return;
}
checkState(compiler.getOptions().outputJs == OutputJs.NORMAL);
String code = module == null ? compiler.toSource() : compiler.toSource(module);
writeOutput(out, compiler, code, wrapper, codePlaceholder, escaper, filename);
}
/**
* Writes code to an output stream, optionally wrapping it in an arbitrary wrapper that contains a
* placeholder where the code should be inserted.
*/
@GwtIncompatible("Unnecessary")
@VisibleForTesting
void writeOutput(
Appendable out,
Compiler compiler,
String code,
String wrapper,
String codePlaceholder,
@Nullable Function escaper,
String filename)
throws IOException {
int pos = wrapper.indexOf(codePlaceholder);
if (pos != -1) {
String prefix = "";
if (pos > 0) {
prefix = wrapper.substring(0, pos);
out.append(prefix);
}
out.append(escaper == null ? code : escaper.apply(code));
int suffixStart = pos + codePlaceholder.length();
if (suffixStart != wrapper.length()) {
// Something after placeholder?
out.append(wrapper.substring(suffixStart));
}
// Make sure we always end output with a line feed.
out.append('\n');
// If we have a source map, adjust its offsets to match
// the code WITHIN the wrapper.
if (compiler != null && compiler.getSourceMap() != null) {
compiler.getSourceMap().setWrapperPrefix(prefix);
}
} else {
out.append(code);
out.append('\n');
}
}
/** Creates any directories necessary to write a file that will have a given path prefix. */
@GwtIncompatible("Unnecessary")
private static void maybeCreateDirsForPath(String pathPrefix) {
if (!Strings.isNullOrEmpty(pathPrefix)) {
String dirName =
pathPrefix.charAt(pathPrefix.length() - 1) == File.separatorChar
? pathPrefix.substring(0, pathPrefix.length() - 1)
: new File(pathPrefix).getParent();
if (dirName != null) {
new File(dirName).mkdirs();
}
}
}
@GwtIncompatible("Unnecessary")
private Appendable createDefaultOutput() throws IOException {
boolean writeOutputToFile = !config.jsOutputFile.isEmpty();
if (writeOutputToFile) {
return fileNameToLegacyOutputWriter(config.jsOutputFile);
} else {
return streamToLegacyOutputWriter(defaultJsOutput);
}
}
@GwtIncompatible("Unnecessary")
private static void closeAppendable(Appendable output) throws IOException {
if (output instanceof Flushable) {
((Flushable) output).flush();
}
if (output instanceof Closeable) {
((Closeable) output).close();
}
}
/**
* Parses command-line arguments and runs the compiler.
*
* @return system exit status
*/
@GwtIncompatible("Unnecessary")
protected int doRun() throws IOException {
CompileMetricsRecorderInterface metricsRecorder = getCompileMetricsRecorder();
metricsRecorder.recordActionStart();
if (this.config.printVersion) {
this.defaultJsOutput.println(this.getVersionText());
this.defaultJsOutput.flush();
return 0;
}
Compiler.setLoggingLevel(Level.parse(config.loggingLevel));
compiler = createCompiler();
B options = createOptions();
setRunOptions(options);
@Nullable String typedAstListInputFilename = config.typedAstListInputFilename;
List externs = createExterns(options);
List modules = null;
rootRelativePathsMap = constructRootRelativePathsMap();
boolean writeOutputToFile = !config.jsOutputFile.isEmpty();
List outputFileNames = new ArrayList<>();
if (writeOutputToFile) {
outputFileNames.add(config.jsOutputFile);
}
boolean createCommonJsModules = false;
if (options.getProcessCommonJSModules()
&& (config.module.size() == 1 && "auto".equals(config.module.get(0)))) {
createCommonJsModules = true;
config.module.remove(0);
}
List jsChunkSpecs = new ArrayList<>();
for (int i = 0; i < config.module.size(); i++) {
jsChunkSpecs.add(JsChunkSpec.create(config.module.get(i), i == 0));
}
List jsonFiles = null;
if (config.jsonStreamMode == JsonStreamMode.IN
|| config.jsonStreamMode == JsonStreamMode.BOTH) {
jsonFiles = parseJsonFilesFromInputStream();
ImmutableMap.Builder inputSourceMaps = new ImmutableMap.Builder<>();
ImmutableMap.Builder inputPathByWebpackId = new ImmutableMap.Builder<>();
boolean foundJsonInputSourceMap = false;
for (JsonFileSpec jsonFile : jsonFiles) {
if (jsonFile.getSourceMap() != null && jsonFile.getSourceMap().length() > 0) {
String sourceMapPath = jsonFile.getPath() + ".map";
SourceFile sourceMap = SourceFile.fromCode(sourceMapPath, jsonFile.getSourceMap());
inputSourceMaps.put(jsonFile.getPath(), new SourceMapInput(sourceMap));
foundJsonInputSourceMap = true;
}
if (jsonFile.getWebpackId() != null) {
inputPathByWebpackId.put(jsonFile.getWebpackId(), jsonFile.getPath());
}
}
if (foundJsonInputSourceMap) {
inputSourceMaps.putAll(options.inputSourceMaps);
options.inputSourceMaps = inputSourceMaps.buildOrThrow();
}
compiler.initWebpackMap(inputPathByWebpackId.buildOrThrow());
} else {
ImmutableMap emptyMap = ImmutableMap.of();
compiler.initWebpackMap(emptyMap);
}
options.setDoLateLocalization(config.shouldDoLateLocalization());
compiler.initOptions(options);
List sources =
createSourceInputs(jsChunkSpecs, config.mixedJsSources, jsonFiles, config.moduleRoots);
if (!jsChunkSpecs.isEmpty()) {
if (isInTestMode()) {
modules = modulesSupplierForTesting.get();
} else {
if (JsonStreamMode.IN.equals(config.jsonStreamMode)
|| JsonStreamMode.NONE.equals(config.jsonStreamMode)) {
for (JsChunkSpec m : jsChunkSpecs) {
checkModuleName(m.getName());
}
}
ImmutableList.Builder inputs = ImmutableList.builder();
for (SourceFile source : sources) {
inputs.add(new CompilerInput(source, /* isExtern= */ false));
}
modules = createJsModules(jsChunkSpecs, inputs.build());
}
for (JSChunk m : modules) {
outputFileNames.add(getModuleOutputFileName(m));
}
if (typedAstListInputFilename != null) {
this.initModulesWithTypedAstFilesystem(
externs, modules, options, typedAstListInputFilename);
} else {
compiler.initModules(externs, modules, options);
}
} else {
if (typedAstListInputFilename != null) {
this.initWithTypedAstFilesystem(externs, sources, options, typedAstListInputFilename);
} else {
compiler.init(externs, sources, options);
}
}
// Release temporary data structures now that the compiler has
// been initialized
jsChunkSpecs = null;
jsonFiles = null;
externs = null;
sources = null;
if (options.printConfig) {
compiler.printConfig();
}
Result result;
// We won't want to process results for cases where compilation is only partially done.
boolean shouldProcessResults = true;
if (config.skipNormalOutputs) {
metricsRecorder.recordActionName("skip normal outputs");
// TODO(bradfordcsmith): Should we be ignoring possible init/initModules() errors here?
compiler.orderInputsWithLargeStack();
result = null;
} else if (compiler.hasErrors()) {
metricsRecorder.recordActionName("initialization errors occurred");
// init() or initModules() encountered an error.
compiler.generateReport();
result = compiler.getResult();
} else if (options.getInstrumentForCoverageOnly()) {
result = instrumentForCoverage(metricsRecorder);
} else if (config.shouldSaveAfterStage1()) {
result = performStage1andSave(config.getSaveCompilationStateToFilename(), metricsRecorder);
// Don't output any results, since compilation isn't done yet.
shouldProcessResults = false;
} else if (typedAstListInputFilename != null) {
result = parseAndPerformStages2and3(metricsRecorder);
} else if (config.shouldRestoreAndPerformStage2AndSave()) {
result =
restoreAndPerformStage2AndSave(
config.getContinueSavedCompilationFileName(),
config.getSaveCompilationStateToFilename(),
metricsRecorder);
// Don't output any results, since compilation isn't done yet.
shouldProcessResults = false;
} else if (config.shouldRestoreAndPerformStages2And3()) {
result =
restoreAndPerformStages2and3(
config.getContinueSavedCompilationFileName(), metricsRecorder);
if (modules != null) {
modules = ImmutableList.copyOf(compiler.getModules());
}
} else if (config.shouldRestoreAndPerformStage3()) {
result =
restoreAndPerformStage3(config.getContinueSavedCompilationFileName(), metricsRecorder);
if (modules != null) {
modules = ImmutableList.copyOf(compiler.getModules());
}
} else {
result = performFullCompilation(metricsRecorder);
}
if (createCommonJsModules) {
// For Commonchunks construct modules from actual inputs.
modules = ImmutableList.copyOf(compiler.getModules());
for (JSChunk m : modules) {
outputFileNames.add(getModuleOutputFileName(m));
}
}
for (String outputFileName : outputFileNames) {
if (compiler.getSourceFileByName(outputFileName) != null) {
compiler.report(JSError.make(OUTPUT_SAME_AS_INPUT_ERROR, outputFileName));
return 1;
}
}
int exitStatus = shouldProcessResults ? processResults(result, modules, options) : 0;
metricsRecorder.recordResultMetrics(compiler, result);
return exitStatus;
}
/**
* Child classes should override this if they want to actually record metrics about the
* compilation.
*/
protected CompileMetricsRecorderInterface getCompileMetricsRecorder() {
return new DummyCompileMetricsRecorder();
}
@GwtIncompatible("Unnecessary")
private Result performStage1andSave(
String filename, CompileMetricsRecorderInterface metricsRecorder) {
metricsRecorder.recordActionName("stage 1");
try (BufferedOutputStream serializedOutputStream =
new BufferedOutputStream(new FileOutputStream(filename))) {
compiler.parseForCompilation();
if (!compiler.hasErrors()) {
metricsRecorder.recordStartState(compiler);
compiler.stage1Passes();
}
if (!compiler.hasErrors()) {
compiler.saveState(serializedOutputStream);
}
compiler.performPostCompilationTasks();
} catch (IOException e) {
compiler.report(JSError.make(COULD_NOT_SERIALIZE_AST, filename));
} finally {
// Make sure we generate a report of errors and warnings even if the compiler throws an
// exception somewhere.
compiler.generateReport();
}
return compiler.getResult();
}
@GwtIncompatible("Unnecessary")
private void initWithTypedAstFilesystem(
List externs,
List sources,
CompilerOptions options,
String filename) {
try (BufferedInputStream typedAstListStream =
new BufferedInputStream(new FileInputStream(filename))) {
compiler.initWithTypedAstFilesystem(externs, sources, options, typedAstListStream);
} catch (IOException e) {
compiler.report(JSError.make(COULD_NOT_DESERIALIZE_AST, filename));
}
}
@GwtIncompatible("Unnecessary")
private void initModulesWithTypedAstFilesystem(
List externs, List chunks, CompilerOptions options, String filename) {
try (BufferedInputStream typedAstListStream =
new BufferedInputStream(new FileInputStream(filename))) {
compiler.initModulesWithTypedAstFilesystem(externs, chunks, options, typedAstListStream);
} catch (IOException e) {
compiler.report(JSError.make(COULD_NOT_DESERIALIZE_AST, filename));
}
}
@GwtIncompatible("Unnecessary")
private Result parseAndPerformStages2and3(CompileMetricsRecorderInterface metricsRecorder) {
metricsRecorder.recordActionName("skip-checks compile");
try {
compiler.parseForCompilation();
if (!compiler.hasErrors()) {
metricsRecorder.recordStartState(compiler);
compiler.stage2Passes();
if (!compiler.hasErrors()) {
compiler.stage3Passes();
}
compiler.performPostCompilationTasks();
}
} finally {
// Make sure we generate a report of errors and warnings even if the compiler throws an
// exception somewhere.
compiler.generateReport();
}
return compiler.getResult();
}
@GwtIncompatible("Unnecessary")
private Result restoreAndPerformStage2AndSave(
String inputFilename,
String outputFilename,
CompileMetricsRecorderInterface metricsRecorder) {
metricsRecorder.recordActionName("stage 2/3");
try (BufferedInputStream serializedInputStream =
new BufferedInputStream(new FileInputStream(inputFilename))) {
compiler.restoreState(serializedInputStream);
if (!compiler.hasErrors()) {
metricsRecorder.recordStartState(compiler);
compiler.stage2Passes();
if (!compiler.hasErrors()) {
try (BufferedOutputStream serializedOutputStream =
new BufferedOutputStream(new FileOutputStream(outputFilename))) {
compiler.saveState(serializedOutputStream);
} catch (IOException e) {
compiler.report(JSError.make(COULD_NOT_SERIALIZE_AST, outputFilename));
}
compiler.performPostCompilationTasks();
}
}
} catch (IOException | ClassNotFoundException e) {
compiler.report(JSError.make(COULD_NOT_DESERIALIZE_AST, inputFilename));
} finally {
// Make sure we generate a report of errors and warnings even if the compiler throws an
// exception somewhere.
compiler.generateReport();
}
return compiler.getResult();
}
@GwtIncompatible("Unnecessary")
private Result restoreAndPerformStages2and3(
String filename, CompileMetricsRecorderInterface metricsRecorder) {
// From the outside this looks like the second stage of a 2-stage compile.
metricsRecorder.recordActionName("stage 2/2");
try (BufferedInputStream serializedInputStream =
new BufferedInputStream(new FileInputStream(filename))) {
compiler.restoreState(serializedInputStream);
if (!compiler.hasErrors()) {
metricsRecorder.recordStartState(compiler);
compiler.stage2Passes();
if (!compiler.hasErrors()) {
compiler.stage3Passes();
}
}
compiler.performPostCompilationTasks();
} catch (IOException | ClassNotFoundException e) {
compiler.report(JSError.make(COULD_NOT_DESERIALIZE_AST, filename));
} finally {
// Make sure we generate a report of errors and warnings even if the compiler throws an
// exception somewhere.
compiler.generateReport();
}
return compiler.getResult();
}
@GwtIncompatible("Unnecessary")
private Result restoreAndPerformStage3(
String filename, CompileMetricsRecorderInterface metricsRecorder) {
metricsRecorder.recordActionName("stage 3/3");
try (BufferedInputStream serializedInputStream =
new BufferedInputStream(new FileInputStream(filename))) {
compiler.restoreState(serializedInputStream);
if (!compiler.hasErrors()) {
metricsRecorder.recordStartState(compiler);
compiler.stage3Passes();
}
compiler.performPostCompilationTasks();
} catch (IOException | ClassNotFoundException e) {
compiler.report(JSError.make(COULD_NOT_DESERIALIZE_AST, filename));
} finally {
// Make sure we generate a report of errors and warnings even if the compiler throws an
// exception somewhere.
compiler.generateReport();
}
return compiler.getResult();
}
@GwtIncompatible("Unnecessary")
private Result performFullCompilation(CompileMetricsRecorderInterface metricsRecorder) {
// This is the code path taken when "building" a library by just checking it for errors
// and generating an .ijs file and also when doing a full compilation.
metricsRecorder.recordActionName(
compiler.getOptions().checksOnly ? "checks-only" : "full compile");
try {
compiler.parseForCompilation();
if (!compiler.hasErrors()) {
metricsRecorder.recordStartState(compiler);
compiler.stage1Passes();
if (!compiler.hasErrors()) {
compiler.stage2Passes();
if (!compiler.hasErrors()) {
compiler.stage3Passes();
}
}
compiler.performPostCompilationTasks();
}
} finally {
// Make sure we generate a report of errors and warnings even if the compiler throws an
// exception somewhere.
compiler.generateReport();
}
return compiler.getResult();
}
@GwtIncompatible("Unnecessary")
private Result instrumentForCoverage(CompileMetricsRecorderInterface metricsRecorder) {
metricsRecorder.recordActionName("instrument for coverage");
try {
compiler.parseForCompilation();
if (!compiler.hasErrors()) {
metricsRecorder.recordStartState(compiler);
compiler.instrumentForCoverage();
}
} finally {
compiler.generateReport();
}
return compiler.getResult();
}
/** Processes the results of the compile job, and returns an error code. */
@GwtIncompatible("Unnecessary")
int processResults(Result result, List modules, B options) throws IOException {
if (config.printAst) {
if (compiler.getRoot() == null) {
return 1;
} else {
Appendable jsOutput = createDefaultOutput();
ControlFlowGraph cfg = compiler.computeCFG();
DotFormatter.appendDot(compiler.getRoot().getLastChild(), cfg, jsOutput);
jsOutput.append('\n');
closeAppendable(jsOutput);
return 0;
}
}
if (config.printTree) {
if (compiler.getRoot() == null) {
compiler.report(JSError.make(NO_TREE_GENERATED_ERROR));
return 1;
} else {
Appendable jsOutput = createDefaultOutput();
compiler.getRoot().appendStringTree(jsOutput);
jsOutput.append("\n");
closeAppendable(jsOutput);
return 0;
}
}
if (config.skipNormalOutputs) {
// Output the manifest and bundle files if requested.
outputManifest();
outputBundle();
outputModuleGraphJson();
return 0;
} else if (options.outputJs != OutputJs.NONE && result.success) {
outputModuleGraphJson();
if (modules == null) {
outputSingleBinary(options);
// Output the source map if requested.
// If output files are being written to stdout as a JSON string,
// outputSingleBinary will have added the sourcemap to the output file
if (!isOutputInJson()) {
outputSourceMap(options, config.jsOutputFile);
}
} else {
DiagnosticType error = outputModuleBinaryAndSourceMaps(compiler.getModules(), options);
if (error != null) {
compiler.report(JSError.make(error));
return 1;
}
}
// Output the externs if required.
if (options.getExternExportsPath() != null) {
try (Writer eeOut = openExternExportsStream(options, config.jsOutputFile)) {
eeOut.append(result.externExport);
}
}
// Output the variable and property name maps if requested.
outputNameMaps();
// Output the ReplaceStrings map if requested
outputStringMap();
// Output the manifest and bundle files if requested.
outputManifest();
outputBundle();
// Output the production instrumentation param mapping if requested.
outputInstrumentationMapping();
if (isOutputInJson()) {
outputJsonStream();
}
}
// return 0 if no errors, the error count otherwise
return min(result.errors.size(), 0x7f);
}
@GwtIncompatible("Unnecessary")
Function getJavascriptEscaper() {
return SourceCodeEscapers.javascriptEscaper().asFunction();
}
@GwtIncompatible("Unnecessary")
void outputSingleBinary(B options) throws IOException {
Function escaper = null;
String marker = OUTPUT_MARKER;
if (config.outputWrapper.contains(OUTPUT_MARKER_JS_STRING)) {
marker = OUTPUT_MARKER_JS_STRING;
escaper = getJavascriptEscaper();
}
if (isOutputInJson()) {
this.filesToStreamOut.add(createJsonFile(options, marker, escaper));
} else {
if (!config.jsOutputFile.isEmpty()) {
maybeCreateDirsForPath(config.jsOutputFile);
}
Appendable jsOutput = createDefaultOutput();
writeOutput(
jsOutput,
compiler,
(JSChunk) null,
config.outputWrapper,
marker,
escaper,
config.jsOutputFile);
closeAppendable(jsOutput);
}
}
/** Save the compiler output to a JsonFileSpec to be later written to stdout */
@GwtIncompatible("Unnecessary")
JsonFileSpec createJsonFile(B options, String outputMarker, Function escaper)
throws IOException {
Appendable jsOutput = new StringBuilder();
writeOutput(
jsOutput,
compiler,
(JSChunk) null,
config.outputWrapper,
outputMarker,
escaper,
config.jsOutputFile);
JsonFileSpec jsonOutput =
new JsonFileSpec(
jsOutput.toString(),
Strings.isNullOrEmpty(config.jsOutputFile) ? "compiled.js" : config.jsOutputFile);
if (!Strings.isNullOrEmpty(options.sourceMapOutputPath)) {
StringBuilder sourcemap = new StringBuilder();
compiler.getSourceMap().appendTo(sourcemap, jsonOutput.getPath());
jsonOutput.setSourceMap(sourcemap.toString());
}
return jsonOutput;
}
@GwtIncompatible("Unnecessary")
void outputJsonStream() throws IOException {
try (JsonWriter jsonWriter =
new JsonWriter(new BufferedWriter(new OutputStreamWriter(defaultJsOutput, UTF_8)))) {
Gson gsonOut = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create();
Type filesCollectionType = new TypeToken>() {}.getType();
gsonOut.toJson(this.filesToStreamOut, filesCollectionType, jsonWriter);
}
}
@GwtIncompatible("Unnecessary")
private DiagnosticType outputModuleBinaryAndSourceMaps(Iterable modules, B options)
throws IOException {
parsedModuleWrappers = parseModuleWrappers(config.moduleWrapper, modules);
if (!isOutputInJson()) {
maybeCreateDirsForPath(config.moduleOutputPathPrefix);
}
// If the source map path is in fact a pattern for each
// module, create a stream per-module. Otherwise, create
// a single source map.
Writer mapFileOut = null;
// When the json_streams flag is specified, sourcemaps are always generated
// per module
if (!(shouldGenerateMapPerModule(options)
|| options.sourceMapOutputPath == null
|| config.jsonStreamMode == JsonStreamMode.OUT
|| config.jsonStreamMode == JsonStreamMode.BOTH)) {
// warn that this is not supported
return INVALID_MODULE_SOURCEMAP_PATTERN;
}
for (JSChunk m : modules) {
if (m.getName().equals(JSChunk.WEAK_CHUNK_NAME)) {
// Skip the weak module, which is always empty.
continue;
}
if (isOutputInJson()) {
this.filesToStreamOut.add(createJsonFileFromModule(m));
} else {
if (shouldGenerateMapPerModule(options)) {
mapFileOut = fileNameToOutputWriter2(expandSourceMapPath(options, m));
}
String moduleFilename = getModuleOutputFileName(m);
maybeCreateDirsForPath(moduleFilename);
try (Writer writer = fileNameToLegacyOutputWriter(moduleFilename)) {
if (options.sourceMapOutputPath != null) {
compiler.resetAndIntitializeSourceMap();
}
writeModuleOutput(moduleFilename, writer, m);
if (options.sourceMapOutputPath != null) {
compiler.getSourceMap().appendTo(mapFileOut, moduleFilename);
}
}
if (shouldGenerateMapPerModule(options) && mapFileOut != null) {
mapFileOut.close();
mapFileOut = null;
}
}
}
if (mapFileOut != null) {
mapFileOut.close();
}
return null;
}
/** Given an output module, convert it to a JSONFileSpec with associated sourcemap */
@GwtIncompatible("Unnecessary")
private JsonFileSpec createJsonFileFromModule(JSChunk module) throws IOException {
compiler.resetAndIntitializeSourceMap();
String filename = getModuleOutputFileName(module);
StringBuilder output = new StringBuilder();
writeModuleOutput(filename, output, module);
JsonFileSpec jsonFile = new JsonFileSpec(output.toString(), filename);
StringBuilder moduleSourceMap = new StringBuilder();
compiler.getSourceMap().appendTo(moduleSourceMap, getModuleOutputFileName(module));
jsonFile.setSourceMap(moduleSourceMap.toString());
return jsonFile;
}
/**
* Query the flag for the input charset, and return a Charset object representing the selection.
*
* @return Charset to use when reading inputs
* @throws FlagUsageException if flag is not a valid Charset name.
*/
@GwtIncompatible("Unnecessary")
private Charset getInputCharset() {
if (!config.charset.isEmpty()) {
if (!Charset.isSupported(config.charset)) {
throw new FlagUsageException(config.charset + " is not a valid charset name.");
}
return Charset.forName(config.charset);
}
return UTF_8;
}
/**
* Query the flag for the output charset.
*
* Let the outputCharset be the same as the input charset... except if we're reading in UTF-8
* by default. By tradition, we've always output ASCII to avoid various hiccups with different
* browsers, proxies and firewalls.
*
* @return Name of the charset to use when writing outputs. Guaranteed to be a supported charset.
* @throws FlagUsageException if flag is not a valid Charset name.
*/
@GwtIncompatible("Unnecessary")
private Charset getLegacyOutputCharset() {
if (!config.charset.isEmpty()) {
if (!Charset.isSupported(config.charset)) {
throw new FlagUsageException(config.charset + " is not a valid charset name.");
}
return Charset.forName(config.charset);
}
return US_ASCII;
}
/**
* Query the flag for the output charset. Defaults to UTF-8.
*
* @throws FlagUsageException if flag is not a valid Charset name.
*/
@GwtIncompatible("Unnecessary")
private Charset getOutputCharset2() {
if (!config.charset.isEmpty()) {
if (!Charset.isSupported(config.charset)) {
throw new FlagUsageException(config.charset + " is not a valid charset name.");
}
return Charset.forName(config.charset);
}
return UTF_8;
}
@GwtIncompatible("Unnecessary")
protected List createExterns(CompilerOptions options) throws IOException {
return isInTestMode() ? externsSupplierForTesting.get() : createExternInputs(config.externs);
}
/**
* Returns true if and only if a source map file should be generated for each module, as opposed
* to one unified map. This is specified by having the source map pattern include the %outname%
* variable.
*/
@GwtIncompatible("Unnecessary")
protected boolean shouldGenerateMapPerModule(B options) {
return options.sourceMapOutputPath != null && options.sourceMapOutputPath.contains("%outname%");
}
/**
* Returns a stream for outputting the generated externs file.
*
* @param options The options to the Compiler.
* @param path The path of the generated JS source file.
* @return The stream or null if no extern-ed exports are being generated.
*/
@GwtIncompatible("Unnecessary")
private Writer openExternExportsStream(B options, String path) throws IOException {
final String externExportsPath = options.getExternExportsPath();
if (externExportsPath == null) {
return null;
}
String exPath = externExportsPath;
if (!exPath.contains(File.separator)) {
File outputFile = new File(path);
exPath = outputFile.getParent() + File.separatorChar + exPath;
}
return fileNameToOutputWriter2(exPath);
}
/**
* Expand a file path specified on the command-line.
*
* Most file paths on the command-line allow an %outname% placeholder. The placeholder will
* expand to a different value depending on the current output mode. There are three scenarios:
*
*
1) Single JS output, single extra output: sub in jsOutputPath. 2) Multiple JS output, single
* extra output: sub in the base module name. 3) Multiple JS output, multiple extra output: sub in
* the module output file.
*
*
Passing a JSChunk to this function automatically triggers case #3. Otherwise, we'll use
* strategy #1 or #2 based on the current output mode.
*/
@GwtIncompatible("Unnecessary")
private String expandCommandLinePath(String path, JSChunk forModule) {
String sub;
if (forModule != null) {
sub = getModuleOutputFileName(forModule);
} else if (!config.module.isEmpty()) {
sub = config.moduleOutputPathPrefix;
} else {
sub = config.jsOutputFile;
}
return path.replace("%outname%", sub);
}
/** Expansion function for source map. */
@VisibleForTesting
@GwtIncompatible("Unnecessary")
String expandSourceMapPath(B options, JSChunk forModule) {
if (Strings.isNullOrEmpty(options.sourceMapOutputPath)) {
return null;
}
return expandCommandLinePath(options.sourceMapOutputPath, forModule);
}
/**
* Converts a file name into a Writer taking in account the output charset. Returns null if the
* file name is null.
*/
@GwtIncompatible("Unnecessary")
private Writer fileNameToLegacyOutputWriter(String fileName) throws IOException {
if (fileName == null) {
return null;
}
if (isInTestMode()) {
return new StringWriter();
}
return streamToLegacyOutputWriter(filenameToOutputStream(fileName));
}
/**
* Converts a file name into a Writer taking in account the output charset. Returns null if the
* file name is null.
*/
@GwtIncompatible("Unnecessary")
private Writer fileNameToOutputWriter2(String fileName) throws IOException {
if (fileName == null) {
return null;
}
if (isInTestMode()) {
return new StringWriter();
}
return streamToOutputWriter2(filenameToOutputStream(fileName));
}
/** Converts a file name into a Outputstream. Returns null if the file name is null. */
@GwtIncompatible("Unnecessary")
protected OutputStream filenameToOutputStream(String fileName) throws IOException {
if (fileName == null) {
return null;
}
return new FileOutputStream(fileName);
}
/** Create a writer with the legacy output charset. */
@GwtIncompatible("Unnecessary")
private Writer streamToLegacyOutputWriter(OutputStream stream) throws IOException {
if (legacyOutputCharset == null) {
return new BufferedWriter(new OutputStreamWriter(stream, UTF_8));
} else {
return new BufferedWriter(new OutputStreamWriter(stream, legacyOutputCharset));
}
}
/** Create a writer with the newer output charset. */
@GwtIncompatible("Unnecessary")
private Writer streamToOutputWriter2(OutputStream stream) {
if (outputCharset2 == null) {
return new BufferedWriter(new OutputStreamWriter(stream, UTF_8));
} else {
return new BufferedWriter(new OutputStreamWriter(stream, outputCharset2));
}
}
/**
* Outputs the source map found in the compiler to the proper path if one exists.
*
* @param options The options to the Compiler.
*/
@GwtIncompatible("Unnecessary")
private void outputSourceMap(B options, String associatedName) throws IOException {
if (Strings.isNullOrEmpty(options.sourceMapOutputPath)
|| options.sourceMapOutputPath.equals("/dev/null")) {
return;
}
String outName = expandSourceMapPath(options, null);
maybeCreateDirsForPath(outName);
try (Writer out = fileNameToOutputWriter2(outName)) {
compiler.getSourceMap().appendTo(out, associatedName);
}
}
/**
* Returns the path at which to output map file(s) based on the path at which the JS binary will
* be placed.
*
* @return The path in which to place the generated map file(s).
*/
@GwtIncompatible("Unnecessary")
private String getMapPath(String outputFile) {
String basePath = "";
if (outputFile.isEmpty()) {
// If we have a js_module_binary rule, output the maps
// at modulename_props_map.out, etc.
if (!config.moduleOutputPathPrefix.isEmpty()) {
basePath = config.moduleOutputPathPrefix;
} else {
basePath = "jscompiler";
}
} else {
// Get the path of the output file.
File file = new File(outputFile);
String outputFileName = file.getName();
// Strip the .js from the name.
if (outputFileName.endsWith(".js")) {
outputFileName = outputFileName.substring(0, outputFileName.length() - 3);
}
String fileParent = file.getParent();
if (fileParent == null) {
basePath = outputFileName;
} else {
basePath = file.getParent() + File.separatorChar + outputFileName;
}
}
return basePath;
}
/**
* Outputs the variable and property name maps for the specified compiler if the proper FLAGS are
* set.
*/
@GwtIncompatible("Unnecessary")
private void outputNameMaps() throws IOException {
String propertyMapOutputPath = null;
String variableMapOutputPath = null;
// Check the create_name_map_files FLAG.
if (config.createNameMapFiles) {
String basePath = getMapPath(config.jsOutputFile);
propertyMapOutputPath = basePath + "_props_map.out";
variableMapOutputPath = basePath + "_vars_map.out";
}
// Check the individual FLAGS.
if (!config.variableMapOutputFile.isEmpty()) {
if (variableMapOutputPath != null) {
throw new FlagUsageException(
"The flags variable_map_output_file and "
+ "create_name_map_files cannot both be used simultaneously.");
}
variableMapOutputPath = config.variableMapOutputFile;
}
if (!config.propertyMapOutputFile.isEmpty()) {
if (propertyMapOutputPath != null) {
throw new FlagUsageException(
"The flags property_map_output_file and "
+ "create_name_map_files cannot both be used simultaneously.");
}
propertyMapOutputPath = config.propertyMapOutputFile;
}
// Output the maps.
if (variableMapOutputPath != null && compiler.getVariableMap() != null) {
compiler.getVariableMap().save(variableMapOutputPath);
}
if (propertyMapOutputPath != null && compiler.getPropertyMap() != null) {
compiler.getPropertyMap().save(propertyMapOutputPath);
}
}
/**
* Outputs the string map generated by the {@link ReplaceStrings} pass if an output path exists.
*/
@GwtIncompatible("Unnecessary")
private void outputStringMap() throws IOException {
if (!config.stringMapOutputPath.isEmpty()) {
if (compiler.getStringMap() == null) {
// Ensure an empty file is created if there is no string map.
// This avoids confusing some build tools that expect to see the file, even if it is empty.
if (!(new File(config.stringMapOutputPath).createNewFile())) {
throw new IOException("Could not create file: " + config.stringMapOutputPath);
}
} else {
compiler.getStringMap().save(config.stringMapOutputPath);
}
}
}
/**
* Create a map of constant names to constant values from a textual description of the map.
*
* @param definitions A list of overriding definitions for defines in the form {@code
* [=]}, where {@code } is a number, boolean, or single-quoted string without
* single quotes.
*/
@VisibleForTesting
public static void createDefineReplacements(List definitions, CompilerOptions options) {
// Parse the definitions
for (String override : definitions) {
String[] assignment = override.split("=", 2);
String defName = assignment[0];
if (defName.length() > 0) {
String defValue = assignment.length == 1 ? "true" : assignment[1];
boolean isTrue = defValue.equals("true");
boolean isFalse = defValue.equals("false");
if (isTrue || isFalse) {
options.setDefineToBooleanLiteral(defName, isTrue);
continue;
} else if (defValue.length() > 1
&& ((defValue.charAt(0) == '\'' && defValue.charAt(defValue.length() - 1) == '\'')
|| (defValue.charAt(0) == '\"'
&& defValue.charAt(defValue.length() - 1) == '\"'))) {
// If the value starts and ends with a single quote,
// we assume that it's a string.
String maybeStringVal = defValue.substring(1, defValue.length() - 1);
if (maybeStringVal.indexOf(defValue.charAt(0)) == -1) {
options.setDefineToStringLiteral(defName, maybeStringVal);
continue;
}
} else {
try {
double value = Double.parseDouble(defValue);
options.setDefineToDoubleLiteral(defName, value);
continue;
} catch (NumberFormatException e) {
// do nothing, it will be caught at the end
}
if (defValue.length() > 0) {
options.setDefineToStringLiteral(defName, defValue);
continue;
}
}
}
throw new RuntimeException("--define flag syntax invalid: " + override);
}
}
/**
* Returns true if and only if a manifest or bundle should be generated for each module, as
* opposed to one unified manifest.
*/
@GwtIncompatible("Unnecessary")
private boolean shouldGenerateOutputPerModule(String output) {
return !config.module.isEmpty() && output != null && output.contains("%outname%");
}
@GwtIncompatible("Unnecessary")
private void outputManifest() throws IOException {
outputManifestOrBundle(config.outputManifests, true);
}
@GwtIncompatible("Unnecessary")
private void outputBundle() throws IOException {
outputManifestOrBundle(config.outputBundles, false);
}
@GwtIncompatible("Unnecessary")
private void outputInstrumentationMapping() throws IOException {
if (!Strings.isNullOrEmpty(config.instrumentationMappingFile)) {
String path = expandCommandLinePath(config.instrumentationMappingFile, /* forModule= */ null);
compiler.getInstrumentationMapping().save(path);
}
}
/**
* Writes the manifest or bundle of all compiler input files that were included as controlled by
* --dependency_mode, if requested.
*/
@GwtIncompatible("Unnecessary")
private void outputManifestOrBundle(List outputFiles, boolean isManifest)
throws IOException {
if (outputFiles.isEmpty()) {
return;
}
for (String output : outputFiles) {
if (output.isEmpty()) {
continue;
}
if (shouldGenerateOutputPerModule(output)) {
// Generate per-module manifests or bundles.
Iterable modules = compiler.getModuleGraph().getAllChunks();
for (JSChunk module : modules) {
try (Writer out = fileNameToOutputWriter2(expandCommandLinePath(output, module))) {
if (isManifest) {
printManifestTo(module, out);
} else {
printBundleTo(module, out);
}
}
}
} else {
// Generate a single file manifest or bundle.
try (Writer out = fileNameToOutputWriter2(expandCommandLinePath(output, null))) {
if (config.module.isEmpty()) {
// For a single-module compilation, generate a single headerless manifest or bundle
// containing only the strong files.
JSChunk module = compiler.getModuleGraph().getChunkByName(JSChunk.STRONG_CHUNK_NAME);
if (isManifest) {
printManifestTo(module, out);
} else {
printBundleTo(module, out);
}
} else {
// For a multi-module compilation, generate a single manifest file with module headers.
printModuleGraphManifestOrBundleTo(compiler.getModuleGraph(), out, isManifest);
}
}
}
}
}
/** Creates a file containing the current module graph in JSON serialization. */
@GwtIncompatible("Unnecessary")
private void outputModuleGraphJson() throws IOException {
if (config.outputModuleDependencies != null && config.outputModuleDependencies.length() != 0) {
try (Writer out = fileNameToOutputWriter2(config.outputModuleDependencies)) {
printModuleGraphJsonTo(out);
}
}
}
/** Prints the current module graph as JSON. */
@VisibleForTesting
@GwtIncompatible("Unnecessary")
void printModuleGraphJsonTo(Appendable out) throws IOException {
out.append(compiler.getModuleGraph().toJson().toString());
}
/** Prints a set of modules to the manifest or bundle file. */
@VisibleForTesting
@GwtIncompatible("Unnecessary")
void printModuleGraphManifestOrBundleTo(JSChunkGraph graph, Appendable out, boolean isManifest)
throws IOException {
Joiner commas = Joiner.on(",");
boolean requiresNewline = false;
for (JSChunk module : graph.getAllChunks()) {
if (!isManifest && module.isWeak()) {
// Skip the weak module on a multi-module bundle, but not a multi-module manifest.
continue;
}
if (requiresNewline) {
out.append("\n");
}
if (isManifest) {
// See CommandLineRunnerTest to see what the format of this
// manifest looks like.
String dependencies = commas.join(module.getSortedDependencyNames());
out.append(
String.format(
"{%s%s}\n", module.getName(), dependencies.isEmpty() ? "" : ":" + dependencies));
printManifestTo(module, out);
} else {
printBundleTo(module, out);
}
requiresNewline = true;
}
}
/**
* Prints a list of input names (using root-relative paths), delimited by newlines, to the
* manifest file.
*/
@VisibleForTesting
@GwtIncompatible("Unnecessary")
void printManifestTo(JSChunk module, Appendable out) throws IOException {
for (CompilerInput input : module.getInputs()) {
String name = input.getName();
// Ignore fill files created by the compiler to facilitate cross-module code motion.
if (Compiler.isFillFileName(name)) {
continue;
}
String rootRelativePath = rootRelativePathsMap.get(name);
String displayName = rootRelativePath != null ? rootRelativePath : name;
out.append(displayName);
out.append("\n");
}
}
/**
* Prints all the input contents, starting with a comment that specifies the input file name
* (using root-relative paths) before each file.
*/
@VisibleForTesting
@GwtIncompatible("Unnecessary")
void printBundleTo(JSChunk module, Appendable out) throws IOException {
Iterable inputs = module.getInputs();
// Prebuild ASTs before they're needed in getLoadFlags, for performance and because
// StackOverflowErrors can be hit if not prebuilt.
if (compiler.getOptions().numParallelThreads > 1) {
new PrebuildAst(compiler, compiler.getOptions().numParallelThreads).prebuild(inputs);
}
if (!compiler.getOptions().preventLibraryInjection) {
// ES6 modules will need a runtime in a bundle. Skip appending this runtime if there are no
// ES6 modules to cut down on size.
for (CompilerInput input : inputs) {
if ("es6".equals(input.getLoadFlags().get("module"))) {
appendRuntimeTo(out);
break;
}
}
}
for (CompilerInput input : inputs) {
String name = input.getName();
String code = input.getSourceFile().getCode();
// Ignore empty fill files created by the compiler to facilitate cross-module code motion.
// Note that non-empty fill files (ones whose code has actually been moved into) are still
// emitted. In particular, this ensures that if there are no (real) inputs the bundle will be
// empty.
if (Compiler.isFillFileName(name) && code.isEmpty()) {
continue;
}
String rootRelativePath = rootRelativePathsMap.get(name);
String displayName = rootRelativePath != null ? rootRelativePath : input.getName();
out.append("//");
out.append(displayName);
out.append("\n");
prepForBundleAndAppendTo(out, input, code);
out.append("\n");
}
}
/**
* Construct and return the input root path map. The key is the exec path of each input file, and
* the value is the corresponding root relative path.
*/
@GwtIncompatible("Unnecessary")
private Map constructRootRelativePathsMap() {
Map rootRelativePathsMap = new LinkedHashMap<>();
for (String mapString : config.manifestMaps) {
int colonIndex = mapString.indexOf(':');
checkState(colonIndex > 0);
String execPath = mapString.substring(0, colonIndex);
String rootRelativePath = mapString.substring(colonIndex + 1);
checkState(rootRelativePath.indexOf(':') == -1);
rootRelativePathsMap.put(execPath, rootRelativePath);
}
return rootRelativePathsMap;
}
/**
* Configurations for the command line configs. Designed for easy building, so that we can
* decouple the flags-parsing library from the actual configuration options.
*
* TODO(tjgq): Investigate whether this class is really needed to mediate between the
* CompilerOptions and runner implementations. An alternative would be for the runners to fill in
* the CompilerOptions directly, but that conflicts with the latter's mutability and the desire to
* reuse the same options across multiple compilations.
*/
@GwtIncompatible("Unnecessary")
protected static class CommandLineConfig {
private boolean printVersion;
CommandLineConfig setPrintVersion(boolean x) {
this.printVersion = x;
return this;
}
private boolean printTree = false;
/** Prints out the parse tree and exits */
CommandLineConfig setPrintTree(boolean printTree) {
this.printTree = printTree;
return this;
}
private boolean printAst = false;
/** Prints a dot file describing the internal abstract syntax tree and exits */
public CommandLineConfig setPrintAst(boolean printAst) {
this.printAst = printAst;
return this;
}
private CompilerOptions.DevMode jscompDevMode = CompilerOptions.DevMode.OFF;
/** Turns on extra validity checks */
public CommandLineConfig setJscompDevMode(CompilerOptions.DevMode jscompDevMode) {
this.jscompDevMode = jscompDevMode;
return this;
}
private String loggingLevel = Level.WARNING.getName();
/**
* The logging level (standard java.util.logging.Level values) for Compiler progress. Does not
* control errors or warnings for the JavaScript code under compilation
*/
public CommandLineConfig setLoggingLevel(String loggingLevel) {
this.loggingLevel = loggingLevel;
return this;
}
private final List externs = new ArrayList<>();
/** The file containing JavaScript externs. You may specify multiple. */
public CommandLineConfig setExterns(List externs) {
this.externs.clear();
this.externs.addAll(externs);
return this;
}
private final List> mixedJsSources = new ArrayList<>();
/** The JavaScript source file names, including .js and .zip files. You may specify multiple. */
public CommandLineConfig setMixedJsSources(List> mixedJsSources) {
this.mixedJsSources.clear();
this.mixedJsSources.addAll(mixedJsSources);
return this;
}
private boolean defaultToStdin = false;
/**
* Whether to read a single source file from standard input if no input files are explicitly
* specified.
*/
public CommandLineConfig setDefaultToStdin() {
this.defaultToStdin = true;
return this;
}
private String jsOutputFile = "";
/** Primary output filename. If not specified, output is written to stdout */
public CommandLineConfig setJsOutputFile(String jsOutputFile) {
this.jsOutputFile = jsOutputFile;
return this;
}
/**
* When non-null specifies a file containing saved compiler state to restore and continue
* compiling.
*/
private String continueSavedCompilationFileName = null;
/**
* When > 0 indicates the stage at which compilation stopped for the compilation state that is
* being restored from a save file.
*/
private int restoredCompilationStage = -1;
/**
* When > 0 indicates the stage at which compilation should stop and the compilation state be
* stored into a save file.
*/
private int saveAfterCompilationStage = -1;
/** Set the compiler to resume a saved compilation state from a file. */
public CommandLineConfig setContinueSavedCompilationFileName(
String fileName, int restoredCompilationStage) {
if (fileName != null) {
checkArgument(
restoredCompilationStage > 0 && restoredCompilationStage < 3,
"invalid compilation stage: %s",
restoredCompilationStage);
continueSavedCompilationFileName = fileName;
this.restoredCompilationStage = restoredCompilationStage;
}
return this;
}
boolean shouldSaveAfterStage1() {
if (saveCompilationStateToFilename != null && saveAfterCompilationStage == 1) {
checkState(
restoredCompilationStage < 0,
"cannot perform stage 1 after restoring from stage %s",
restoredCompilationStage);
checkState(
continueSavedCompilationFileName == null,
"cannot restore a saved compilation and also save after stage 1");
return true;
} else {
return false;
}
}
String getContinueSavedCompilationFileName() {
return continueSavedCompilationFileName;
}
boolean shouldRestoreAndPerformStages2And3() {
// We have a saved compilations state to restore
return shouldContinueCompilation()
// the restored compilation stopped at stage 1
&& restoredCompilationStage == 1
// we want to complete the rest of the compilation stages
&& saveAfterCompilationStage < 0;
}
private boolean shouldContinueCompilation() {
if (continueSavedCompilationFileName != null) {
checkState(
restoredCompilationStage > 0 && restoredCompilationStage < 3,
"invalid restored compilation stage: %s",
restoredCompilationStage);
return true;
} else {
return false;
}
}
boolean shouldRestoreAndPerformStage2AndSave() {
// We have a saved compilations state to restore
return shouldContinueCompilation()
// the restored compilation stopped at stage 1
&& restoredCompilationStage == 1
// we want to stop and save after stage 2
&& saveAfterCompilationStage == 2;
}
boolean shouldRestoreAndPerformStage3() {
if (shouldContinueCompilation() && restoredCompilationStage == 2) {
// We have a saved compilations state from stage 2 to restore and continue
checkState(
saveAfterCompilationStage < 0,
"request to save after stage %s is invalid when restoring from stage 2",
saveAfterCompilationStage);
return true;
} else {
return false;
}
}
boolean shouldDoLateLocalization() {
// The point of doing a 3-stage compilation is to save localization work for last, so
// we avoid doing checks and optimizations separately for every locale.
// If we aren't doing a 3-stage compilation, then late localization is just doing more work
// for a possibly-bigger compiled output (because code gets added after optimizations have
// already executed).
return shouldRestoreAndPerformStage2AndSave() || shouldRestoreAndPerformStage3();
}
@Nullable private String typedAstListInputFilename;
public CommandLineConfig setTypedAstListInputFilename(@Nullable String fileName) {
this.typedAstListInputFilename = fileName;
return this;
}
private String saveCompilationStateToFilename = null;
/** Set the compiler to perform the first phase and save the intermediate result to a file. */
public CommandLineConfig setSaveCompilationStateToFilename(
String fileName, int saveAfterCompilationStage) {
saveCompilationStateToFilename = fileName;
this.saveAfterCompilationStage = saveAfterCompilationStage;
return this;
}
public String getSaveCompilationStateToFilename() {
return saveCompilationStateToFilename;
}
private final List module = new ArrayList<>();
/**
* A JavaScript module specification. The format is {@code
* :[:[,...][:]]]}. Module names must be unique. Each dep is the name
* of a module that this module depends on. Modules must be listed in dependency order, and JS
* source files must be listed in the corresponding order. Where --module flags occur in
* relation to --js flags is unimportant
*/
public CommandLineConfig setModule(List module) {
this.module.clear();
this.module.addAll(module);
return this;
}
private Map sourceMapInputFiles = new HashMap<>();
public CommandLineConfig setSourceMapInputFiles(Map sourceMapInputFiles) {
this.sourceMapInputFiles = sourceMapInputFiles;
return this;
}
private boolean parseInlineSourceMaps = false;
public CommandLineConfig setParseInlineSourceMaps(boolean parseInlineSourceMaps) {
this.parseInlineSourceMaps = parseInlineSourceMaps;
return this;
}
private String variableMapInputFile = "";
/**
* File containing the serialized version of the variable renaming map produced by a previous
* compilation
*/
public CommandLineConfig setVariableMapInputFile(String variableMapInputFile) {
this.variableMapInputFile = variableMapInputFile;
return this;
}
private String propertyMapInputFile = "";
/**
* File containing the serialized version of the property renaming map produced by a previous
* compilation
*/
public CommandLineConfig setPropertyMapInputFile(String propertyMapInputFile) {
this.propertyMapInputFile = propertyMapInputFile;
return this;
}
private String variableMapOutputFile = "";
/** File where the serialized version of the variable renaming map produced should be saved */
public CommandLineConfig setVariableMapOutputFile(String variableMapOutputFile) {
this.variableMapOutputFile = variableMapOutputFile;
return this;
}
private boolean createNameMapFiles = false;
/**
* If true, variable renaming and property renaming map files will be produced as {binary
* name}_vars_map.out and {binary name}_props_map.out. Note that this flag cannot be used in
* conjunction with either variable_map_output_file or property_map_output_file
*/
public CommandLineConfig setCreateNameMapFiles(boolean createNameMapFiles) {
this.createNameMapFiles = createNameMapFiles;
return this;
}
private String propertyMapOutputFile = "";
/** File where the serialized version of the property renaming map produced should be saved */
public CommandLineConfig setPropertyMapOutputFile(String propertyMapOutputFile) {
this.propertyMapOutputFile = propertyMapOutputFile;
return this;
}
private String stringMapOutputPath = "";
/**
* File where the serialized version of the string map produced by the ReplaceStrings pass
* should be saved.
*/
public CommandLineConfig setStringMapOutputFile(String stringMapOutputPath) {
this.stringMapOutputPath = stringMapOutputPath;
return this;
}
private String instrumentationMappingFile = "";
public CommandLineConfig setInstrumentationMappingFile(String instrumentationMappingFile) {
this.instrumentationMappingFile = instrumentationMappingFile;
return this;
}
private CodingConvention codingConvention = CodingConventions.getDefault();
/** Sets rules and conventions to enforce. */
public CommandLineConfig setCodingConvention(CodingConvention codingConvention) {
this.codingConvention = codingConvention;
return this;
}
private int summaryDetailLevel = 1;
/**
* Controls how detailed the compilation summary is. Values: 0 (never print summary), 1 (print
* summary only if there are errors or warnings), 2 (print summary if type checking is on, see
* --check_types), 3 (always print summary). The default level is 1
*/
public CommandLineConfig setSummaryDetailLevel(int summaryDetailLevel) {
this.summaryDetailLevel = summaryDetailLevel;
return this;
}
private String outputWrapper = "";
/**
* Interpolate output into this string at the place denoted by the marker token %output%, or
* %output|jsstring%
*/
public CommandLineConfig setOutputWrapper(String outputWrapper) {
this.outputWrapper = outputWrapper;
return this;
}
private final List moduleWrapper = new ArrayList<>();
/**
* An output wrapper for a JavaScript module (optional). See the flag description for formatting
* requirements.
*/
public CommandLineConfig setModuleWrapper(List moduleWrapper) {
this.moduleWrapper.clear();
this.moduleWrapper.addAll(moduleWrapper);
return this;
}
private String moduleOutputPathPrefix = "";
/**
* Prefix for filenames of compiled chunks. {@code .js} will be appended to this
* prefix. Directories will be created as needed. Use with --module
*/
public CommandLineConfig setModuleOutputPathPrefix(String moduleOutputPathPrefix) {
this.moduleOutputPathPrefix = moduleOutputPathPrefix;
return this;
}
private final List moduleOutputFiles = new ArrayList<>();
/**
* The output file name for a JavaScript chunk (optional). See the flag description for
* formatting requirements.
*/
public CommandLineConfig setModuleOutputFiles(List moduleOutputFiles) {
this.moduleOutputFiles.clear();
this.moduleOutputFiles.addAll(moduleOutputFiles);
return this;
}
private String createSourceMap = "";
/**
* If specified, a source map file mapping the generated source files back to the original
* source file will be output to the specified path. The %outname% placeholder will expand to
* the name of the output file that the source map corresponds to.
*/
public CommandLineConfig setCreateSourceMap(String createSourceMap) {
this.createSourceMap = createSourceMap;
return this;
}
private SourceMap.DetailLevel sourceMapDetailLevel = SourceMap.DetailLevel.ALL;
/** The detail supplied in the source map file, if generated. */
public CommandLineConfig setSourceMapDetailLevel(SourceMap.DetailLevel level) {
this.sourceMapDetailLevel = level;
return this;
}
private SourceMap.Format sourceMapFormat = SourceMap.Format.DEFAULT;
/** The source map format to use, if generated. */
public CommandLineConfig setSourceMapFormat(SourceMap.Format format) {
this.sourceMapFormat = format;
return this;
}
private ImmutableList sourceMapLocationMappings = ImmutableList.of();
/** The source map location mappings to use, if generated. */
public CommandLineConfig setSourceMapLocationMappings(
List locationMappings) {
this.sourceMapLocationMappings = ImmutableList.copyOf(locationMappings);
return this;
}
private boolean applyInputSourceMaps = false;
/**
* Whether to apply input source maps to the output, i.e. map back to original inputs from input
* files that have source maps applied to them.
*/
public CommandLineConfig setApplyInputSourceMaps(boolean applyInputSourceMaps) {
this.applyInputSourceMaps = applyInputSourceMaps;
return this;
}
private final ArrayList> warningGuards = new ArrayList<>();
/** Add warning guards. */
public CommandLineConfig setWarningGuards(List> warningGuards) {
this.warningGuards.clear();
this.warningGuards.addAll(warningGuards);
return this;
}
private final List define = new ArrayList<>();
/**
* Override the value of a variable annotated @define. The format is {@code [=]},
* where {@code } is the name of a @define variable and {@code } is a boolean,
* number, or a single-quoted string that contains no single quotes. If {@code [=]} is
* omitted, the variable is marked true
*/
public CommandLineConfig setDefine(List define) {
this.define.clear();
this.define.addAll(define);
return this;
}
private int browserFeaturesetYear = 0;
/** Indicates target browser's year */
public CommandLineConfig setBrowserFeaturesetYear(Integer browserFeaturesetYear) {
this.browserFeaturesetYear = browserFeaturesetYear;
return this;
}
private boolean emitAsyncFunctionsWithZonejs = false;
/** Relax the restriction on disallowing --language_out=ES_2017 together with Zone.js */
public CommandLineConfig setEmitAsyncFunctionsWithZonejs(boolean emitAsyncFunctionsWithZonejs) {
this.emitAsyncFunctionsWithZonejs = emitAsyncFunctionsWithZonejs;
return this;
}
private TweakProcessing tweakProcessing = TweakProcessing.OFF;
/** Sets the kind of processing to do for goog.tweak functions. */
public CommandLineConfig setTweakProcessing(TweakProcessing tweakProcessing) {
this.tweakProcessing = tweakProcessing;
return this;
}
private String charset = "";
/** Input charset for all files. */
public CommandLineConfig setCharset(String charset) {
this.charset = charset;
return this;
}
private DependencyOptions dependencyOptions = null;
/** Sets the dependency management options. */
public CommandLineConfig setDependencyOptions(@Nullable DependencyOptions dependencyOptions) {
this.dependencyOptions = dependencyOptions;
return this;
}
private List outputManifests = ImmutableList.of();
/** Sets whether to print output manifest files. Filter out empty file names. */
public CommandLineConfig setOutputManifest(List outputManifests) {
this.outputManifests = new ArrayList<>();
for (String manifestName : outputManifests) {
if (!manifestName.isEmpty()) {
this.outputManifests.add(manifestName);
}
}
this.outputManifests = ImmutableList.copyOf(this.outputManifests);
return this;
}
private String outputModuleDependencies = null;
/** Sets whether a JSON file representing the dependencies between modules should be created. */
public CommandLineConfig setOutputModuleDependencies(String outputModuleDependencies) {
this.outputModuleDependencies = outputModuleDependencies;
return this;
}
private List outputBundles = ImmutableList.of();
/** Sets whether to print output bundle files. */
public CommandLineConfig setOutputBundle(List outputBundles) {
this.outputBundles = outputBundles;
return this;
}
private boolean skipNormalOutputs = false;
/** Sets whether the normal outputs of compilation should be skipped. */
public CommandLineConfig setSkipNormalOutputs(boolean skipNormalOutputs) {
this.skipNormalOutputs = skipNormalOutputs;
return this;
}
private List manifestMaps = ImmutableList.of();
/** Sets the execPath:rootRelativePath mappings */
public CommandLineConfig setManifestMaps(List manifestMaps) {
this.manifestMaps = manifestMaps;
return this;
}
private boolean transformAMDToCJSModules = false;
/** Set whether to transform AMD to Commonchunks. */
public CommandLineConfig setTransformAMDToCJSModules(boolean transformAMDToCJSModules) {
this.transformAMDToCJSModules = transformAMDToCJSModules;
return this;
}
private boolean processCommonJSModules = false;
/** Sets whether to process Commonchunks. */
public CommandLineConfig setProcessCommonJSModules(boolean processCommonJSModules) {
this.processCommonJSModules = processCommonJSModules;
return this;
}
private List moduleRoots = ImmutableList.of(ModuleLoader.DEFAULT_FILENAME_PREFIX);
/** Sets the module roots. */
public CommandLineConfig setModuleRoots(List jsChunkRoots) {
this.moduleRoots = jsChunkRoots;
return this;
}
private String warningsAllowFile = "";
/** Sets a allowlist file that suppresses warnings. */
public CommandLineConfig setWarningsAllowlistFile(String fileName) {
this.warningsAllowFile = fileName;
return this;
}
private List hideWarningsFor = ImmutableList.of();
/** Sets the paths for which warnings will be hidden. */
public CommandLineConfig setHideWarningsFor(List hideWarningsFor) {
this.hideWarningsFor = hideWarningsFor;
return this;
}
private boolean angularPass = false;
/** Sets whether to process AngularJS-specific annotations. */
public CommandLineConfig setAngularPass(boolean angularPass) {
this.angularPass = angularPass;
return this;
}
private JsonStreamMode jsonStreamMode = JsonStreamMode.NONE;
public CommandLineConfig setJsonStreamMode(JsonStreamMode mode) {
this.jsonStreamMode = mode;
return this;
}
/** Set of options that can be used with the --formatting flag. */
protected enum ErrorFormatOption {
STANDARD,
JSON
}
private ErrorFormatOption errorFormat = ErrorFormatOption.STANDARD;
public CommandLineConfig setErrorFormat(ErrorFormatOption errorFormat) {
this.errorFormat = errorFormat;
return this;
}
private String jsonWarningsFile = "";
public CommandLineConfig setJsonWarningsFile(String jsonWarningsFile) {
this.jsonWarningsFile = jsonWarningsFile;
return this;
}
}
/** Representation of a source file from an encoded json stream input */
@GwtIncompatible("Unnecessary")
public static class JsonFileSpec {
@Nullable private final String src;
@Nullable private final String path;
@Nullable
@SerializedName(
value = "source_map",
alternate = {"sourceMap"})
private String sourceMap;
@Nullable
@SerializedName(
value = "webpack_id",
alternate = {"webpackId"})
private final String webpackId;
// Graal requires a non-arg constructor for use with GSON
// See https://github.com/oracle/graal/issues/680
private JsonFileSpec() {
this(null, null, null, null);
}
public JsonFileSpec(String src, String path) {
this(src, path, null, null);
}
public JsonFileSpec(String src, String path, String sourceMap) {
this(src, path, sourceMap, null);
}
public JsonFileSpec(String src, String path, String sourceMap, @Nullable String webpackId) {
this.src = src;
this.path = path;
this.sourceMap = sourceMap;
this.webpackId = webpackId;
}
public String getSrc() {
return this.src;
}
public String getPath() {
return this.path;
}
public String getSourceMap() {
return this.sourceMap;
}
public String getWebpackId() {
return this.webpackId;
}
public void setSourceMap(String map) {
this.sourceMap = map;
}
}
/** Flag types for JavaScript source files. */
@GwtIncompatible("Unnecessary")
protected enum JsSourceType {
EXTERN("extern"),
JS("js"),
JS_ZIP("jszip"),
WEAKDEP("weakdep"),
IJS("ijs");
@VisibleForTesting final String flagName;
private JsSourceType(String flagName) {
this.flagName = flagName;
}
}
/** A pair from flag to its value. */
@GwtIncompatible("Unnecessary")
protected static class FlagEntry {
private final T flag;
private final String value;
protected FlagEntry(T flag, String value) {
this.flag = flag;
this.value = value;
}
@Override
public boolean equals(Object o) {
if (o instanceof FlagEntry) {
FlagEntry> that = (FlagEntry>) o;
return that.flag.equals(this.flag) && that.value.equals(this.value);
}
return false;
}
@Override
public int hashCode() {
return flag.hashCode() + value.hashCode();
}
@Override
public String toString() {
return this.flag + "=" + this.value;
}
public T getFlag() {
return flag;
}
public String getValue() {
return value;
}
}
/** Represents a specification for a serving chunk. */
public static class JsChunkSpec {
private final String name;
// Number of input files, including js and zip files.
private final int numInputs;
private final ImmutableList deps;
// Number of input js files. All zip files should be expanded.
private int numJsFiles;
private JsChunkSpec(String name, int numInputs, ImmutableList deps) {
this.name = name;
this.numInputs = numInputs;
this.deps = deps;
this.numJsFiles = numInputs;
}
/**
* @param specString The spec format is: name:num-js-files[:[dep,...][:]]
. Module
* names must not contain the ':' character.
* @param isFirstChunk Whether the spec is for the first module.
* @return A parsed chunk spec.
*/
public static JsChunkSpec create(String specString, boolean isFirstChunk) {
// Format is ":[:[,...][:]]".
String[] parts = specString.split(":");
if (parts.length < 2 || parts.length > 4) {
throw new FlagUsageException(
"Expected 2-4 colon-delimited parts in " + "chunk spec: " + specString);
}
// Parse module name.
String name = parts[0];
// Parse module dependencies.
String[] deps =
parts.length > 2 && parts[2].length() > 0 ? parts[2].split(",") : new String[0];
// Parse module inputs.
int numInputs = -1;
try {
numInputs = Integer.parseInt(parts[1]);
} catch (NumberFormatException ignored) {
numInputs = -1;
}
// We will allow modules of zero input.
if (numInputs < 0) {
// A size of 'auto' is only allowed on the base module if
// and it must also be the first module
if (parts.length == 2 && "auto".equals(parts[1])) {
if (!isFirstChunk) {
throw new FlagUsageException(
"Invalid JS file count '"
+ parts[1]
+ "' for module: "
+ name
+ ". Only the first module may specify "
+ "a size of 'auto' and it must have no dependencies.");
}
} else {
throw new FlagUsageException(
"Invalid JS file count '" + parts[1] + "' for module: " + name);
}
}
return new JsChunkSpec(name, numInputs, ImmutableList.copyOf(deps));
}
public String getName() {
return name;
}
public int getNumInputs() {
return numInputs;
}
public ImmutableList getDeps() {
return deps;
}
public int getNumJsFiles() {
return numJsFiles;
}
}
@GwtIncompatible("Unnecessary")
static final class SystemExitCodeReceiver implements Function {
static final SystemExitCodeReceiver INSTANCE = new SystemExitCodeReceiver();
private SystemExitCodeReceiver() {
// singleton
}
@Override
public Void apply(Integer exitCode) {
int exitCodeValue = checkNotNull(exitCode);
// Don't spuriously report success.
// Posix conventions only guarantee that 8b are significant.
byte exitCodeByte = (byte) exitCodeValue;
if (exitCodeByte == 0 && exitCodeValue != 0) {
exitCodeByte = (byte) -1;
}
System.exit(exitCodeByte);
return null;
}
}
}