All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.Compiler Maven / Gradle / Ivy

/*
 * Copyright 2004 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 com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.ImmutableList.toImmutableList;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.debugging.sourcemap.SourceMapConsumerV3;
import com.google.debugging.sourcemap.proto.Mapping.OriginalMapping;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.javascript.jscomp.CompilerInput.ModuleType;
import com.google.javascript.jscomp.CompilerOptions.DevMode;
import com.google.javascript.jscomp.CompilerOptions.InstrumentOption;
import com.google.javascript.jscomp.JSChunkGraph.ChunkDependenceException;
import com.google.javascript.jscomp.JSChunkGraph.MissingChunkException;
import com.google.javascript.jscomp.NodeTraversal.AbstractPreOrderCallback;
import com.google.javascript.jscomp.SortingErrorManager.ErrorReportGenerator;
import com.google.javascript.jscomp.colors.ColorRegistry;
import com.google.javascript.jscomp.deps.BrowserModuleResolver;
import com.google.javascript.jscomp.deps.BrowserWithTransformedPrefixesModuleResolver;
import com.google.javascript.jscomp.deps.JsFileRegexParser;
import com.google.javascript.jscomp.deps.ModuleLoader;
import com.google.javascript.jscomp.deps.ModuleLoader.ModulePath;
import com.google.javascript.jscomp.deps.ModuleLoader.ModuleResolverFactory;
import com.google.javascript.jscomp.deps.ModuleLoader.PathResolver;
import com.google.javascript.jscomp.deps.NodeModuleResolver;
import com.google.javascript.jscomp.deps.SortedDependencies.MissingProvideException;
import com.google.javascript.jscomp.deps.WebpackModuleResolver;
import com.google.javascript.jscomp.diagnostic.LogFile;
import com.google.javascript.jscomp.instrumentation.CoverageInstrumentationPass;
import com.google.javascript.jscomp.instrumentation.CoverageInstrumentationPass.CoverageReach;
import com.google.javascript.jscomp.modules.ModuleMap;
import com.google.javascript.jscomp.modules.ModuleMetadataMap;
import com.google.javascript.jscomp.parsing.Config;
import com.google.javascript.jscomp.parsing.Config.JsDocParsing;
import com.google.javascript.jscomp.parsing.Config.LanguageMode;
import com.google.javascript.jscomp.parsing.Config.RunMode;
import com.google.javascript.jscomp.parsing.Config.StrictMode;
import com.google.javascript.jscomp.parsing.ParserRunner;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.jscomp.parsing.parser.trees.Comment;
import com.google.javascript.jscomp.parsing.parser.util.format.SimpleFormat;
import com.google.javascript.jscomp.resources.ResourceLoader;
import com.google.javascript.jscomp.serialization.ColorPool;
import com.google.javascript.jscomp.serialization.SerializeTypedAstPass;
import com.google.javascript.jscomp.serialization.TypedAstDeserializer;
import com.google.javascript.jscomp.type.ChainableReverseAbstractInterpreter;
import com.google.javascript.jscomp.type.ClosureReverseAbstractInterpreter;
import com.google.javascript.jscomp.type.ReverseAbstractInterpreter;
import com.google.javascript.jscomp.type.SemanticReverseAbstractInterpreter;
import com.google.javascript.rhino.ErrorReporter;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.InputId;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.StaticScope;
import com.google.javascript.rhino.jstype.JSTypeRegistry;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.nio.file.Files;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jspecify.nullness.Nullable;

/**
 * Compiler (and the other classes in this package) does the following:
 *
 * 
    *
  • parses JS code *
  • checks for undefined variables *
  • performs optimizations such as constant folding and constants inlining *
  • renames variables (to short names) *
  • outputs compact JavaScript code *
* * External variables are declared in 'externs' files. For instance, the file may include * definitions for global javascript/browser objects such as window, document. */ // TODO(tbreisacher): Rename Compiler to JsCompiler and remove this suppression. @SuppressWarnings("JavaLangClash") public class Compiler extends AbstractCompiler implements ErrorHandler, SourceFileMapping { static final DiagnosticType MODULE_DEPENDENCY_ERROR = DiagnosticType.error( "JSC_MODULE_DEPENDENCY_ERROR", "Bad dependency: {0} -> {1}. " + "Modules must be listed in dependency order."); static final DiagnosticType MISSING_ENTRY_ERROR = DiagnosticType.error( "JSC_MISSING_ENTRY_ERROR", "required entry point \"{0}\" never provided"); static final DiagnosticType MISSING_MODULE_ERROR = DiagnosticType.error( "JSC_MISSING_MODULE_ERROR", "unknown module \"{0}\" specified in entry point spec"); private @Nullable CompilerOptions options = null; private @Nullable PassConfig passes = null; // The externs inputs private final ArrayList externs = new ArrayList<>(); // The source module graph, denoting dependencies between chunks. private JSChunkGraph moduleGraph; // The module loader for resolving paths into module URIs. private ModuleLoader moduleLoader; // Map of module names to module types - used for module rewriting private final Map moduleTypesByName; // error manager to which error management is delegated private ErrorManager errorManager; // Warnings guard for filtering warnings. private WarningsGuard warningsGuard; // Compile-time injected libraries private final LinkedHashSet injectedLibraries = new LinkedHashSet<>(); // Node of the final injected library. Future libraries will be injected // after this node. private @Nullable Node lastInjectedLibrary; // Parse tree root nodes private Node externsRoot; private Node jsRoot; private Node externAndJsRoot; // Used for debugging; to see the compiled code between passes private @Nullable String lastJsSource = null; private FeatureSet featureSet; private final Map inputsById = new ConcurrentHashMap<>(); private final Map scriptNodeByFilename = new ConcurrentHashMap<>(); private ImmutableMap inputPathByWebpackId; private StaticScope transpilationNamespace; /** * Subclasses are responsible for loading sources that were not provided as explicit inputs to the * compiler. For example, looking up sources referenced within sourcemaps. */ public static class ExternalSourceLoader { public SourceFile loadSource(String filename) { throw new RuntimeException("Cannot load without a valid loader."); } } // Original sources referenced by the source maps. private final ConcurrentHashMap sourceMapOriginalSources = new ConcurrentHashMap<>(); /** Configured {@link SourceMapInput}s, plus any source maps discovered in source files. */ ConcurrentHashMap inputSourceMaps = new ConcurrentHashMap<>(); // Map from filenames to lists of all the comments in each file. private Map> commentsPerFile = new ConcurrentHashMap<>(); /** The source code map */ private SourceMap sourceMap; /** The externs created from the exports. */ private @Nullable String externExports = null; private UniqueIdSupplier uniqueIdSupplier = new UniqueIdSupplier(); /** Ids for function inlining so that each declared name remains unique. */ private int uniqueNameId = 0; /** Whether to assume there are references to the RegExp Global object properties. */ private boolean hasRegExpGlobalReferences = true; private boolean runJ2clPasses = false; /** Detects Google-specific coding conventions. */ CodingConvention defaultCodingConvention = new ClosureCodingConvention(); private @Nullable JSTypeRegistry typeRegistry; private @Nullable ColorRegistry colorRegistry; private volatile @Nullable Config parserConfig = null; private volatile @Nullable Config externsParserConfig = null; private @Nullable ReverseAbstractInterpreter abstractInterpreter; private @Nullable TypeValidator typeValidator; // The compiler can ask phaseOptimizer for things like which pass is currently // running, or which functions have been changed by optimizations private @Nullable PhaseOptimizer phaseOptimizer = null; public PerformanceTracker tracker; /** Runtime-library files deserailized from a TypedAST JAR resource; indexed by filename */ private ImmutableMap> runtimeLibraryTypedAsts; // Types that have been forward declared private Set forwardDeclaredTypes = new HashSet<>(); private boolean typeCheckingHasRun = false; // This error reporter gets the messages from the current Rhino parser or TypeRegistry. private final ErrorReporter oldErrorReporter = RhinoErrorReporter.forOldRhino(this); private final CompilerExecutor compilerExecutor = createCompilerExecutor(); /** * Logger for the whole com.google.javascript.jscomp domain - setting configuration for this * logger affects all loggers in other classes within the compiler. */ public static final Logger logger = Logger.getLogger("com.google.javascript.jscomp"); private final PrintStream outStream; private String lastPassName; private @Nullable ImmutableSet externProperties = null; private @Nullable AccessorSummary accessorSummary = null; private static final Joiner pathJoiner = Joiner.on(Platform.getFileSeperator()); // Starts at 0, increases as "interesting" things happen. // Nothing happens at time START_TIME, the first pass starts at time 1. // The correctness of scope-change tracking relies on Node/getIntProp // returning 0 if the custom attribute on a node hasn't been set. private int changeStamp = 1; private final Timeline changeTimeline = new Timeline<>(); private final Timeline deleteTimeline = new Timeline<>(); /** * When mapping symbols from a source map, we must repeatedly combine the path of the original * file with the path from the source map to compute the SourceFile of the underlying code. When * iterating through adjacent symbols this computation ends up computing the same thing. * *

This object tracks the last source map we resolved to reduce that work. It caches: for a * given originalPath+sourceMapPath, what sourceFile did we find? */ private static class ResolvedSourceMap { public String originalPath; public String sourceMapPath; public String relativePath; } /** * There is exactly one cached resolvedSourceMap, which you can think of as a cache of size one. */ private final ResolvedSourceMap resolvedSourceMap = new ResolvedSourceMap(); /** Creates a Compiler that reports errors and warnings to its logger. */ public Compiler() { this((PrintStream) null); } /** Creates a Compiler that reports errors and warnings to an output stream. */ public Compiler(@Nullable PrintStream outStream) { addChangeHandler(recentChange); this.outStream = outStream; this.moduleTypesByName = new HashMap<>(); } /** Creates a Compiler that uses a custom error manager. */ public Compiler(ErrorManager errorManager) { this(); setErrorManager(errorManager); } /** * Sets the error manager. * * @param errorManager the error manager, it cannot be {@code null} */ public void setErrorManager(ErrorManager errorManager) { checkNotNull(errorManager, "the error manager cannot be null"); this.errorManager = new ThreadSafeDelegatingErrorManager(errorManager); } /** Creates a message formatter instance corresponding to the value of {@link CompilerOptions}. */ private MessageFormatter createMessageFormatter() { boolean colorize = options.shouldColorizeErrorOutput(); return options.errorFormat.toFormatter(this, colorize); } /** * Initializes the compiler options. It's called as part of a normal compile() job. Public for the * callers that are not doing a normal compile() job. */ public void initOptions(CompilerOptions options) { this.options = options; this.setFeatureSet(options.getLanguageIn().toFeatureSet()); if (options.getForceClassTranspilation()) { options.setOutputFeatureSet( options .getOutputFeatureSet() .without( Feature.CLASSES, Feature.CLASS_EXTENDS, Feature.CLASS_GETTER_SETTER, Feature.PUBLIC_CLASS_FIELDS, Feature.CLASS_STATIC_BLOCK, Feature.NEW_TARGET)); } if (errorManager == null) { if (this.outStream == null) { setErrorManager(new LoggerErrorManager(createMessageFormatter(), logger)); } else { ImmutableSet.Builder builder = ImmutableSet.builder(); builder.add( new PrintStreamErrorReportGenerator( createMessageFormatter(), this.outStream, options.summaryDetailLevel)); builder.addAll(options.getExtraReportGenerators()); setErrorManager(new SortingErrorManager(builder.build())); } } moduleLoader = ModuleLoader.EMPTY; reconcileOptionsWithGuards(); // TODO(johnlenz): generally, the compiler should not be changing the options object // provided by the user. This should be handled a different way. // Turn off type-based optimizations when type checking is off if (!options.isTypecheckingEnabled()) { options.setUseTypesForLocalOptimization(false); options.setUseTypesForOptimization(false); } if (options.assumeForwardDeclaredForMissingTypes) { this.forwardDeclaredTypes = new AbstractSet() { @Override public boolean contains(Object o) { return true; // Report all types as forward declared types. } @Override public boolean add(String e) { return false; } @Override public Iterator iterator() { return Collections.emptyIterator(); } @Override public int size() { return 0; } }; } initWarningsGuard(options.getWarningsGuard()); if (this.isDebugLoggingEnabled()) { // If debug logs are requested, then we'll always log the configuration for convenience. options.setPrintConfig(true); } if (options.isCheckingMissingOverrideTypes()) { // this allows us to generate syntactically better replacement JSDoc that preserves existing // description. options.setParseJsDocDocumentation(JsDocParsing.INCLUDE_ALL_COMMENTS); } } public void printConfig() { if (this.isDebugLoggingEnabled()) { // Log to separate files for convenience. logToFile("externs.log", externs::toString); // To get a pretty-printed JSON module graph, change the string generation expression to // // new GsonBuilder().setPrettyPrinting().create().toJson(moduleGraph.toJson()) // // TODO(bradfordcsmith): Come up with a JSON-printing version that will work when this code is // compiled with J2CL, so we can permanently improve this. logToFile("inputs.json", () -> Iterables.toString(moduleGraph.getAllInputs())); logToFile("options.log", () -> options.toString()); logToFile("warningsGuard.log", () -> warningsGuard.toString()); } else { // Debug logging is not enabled, so use stderr final PrintStream err = System.err; err.println("==== Externs ===="); err.println(externs); err.println("==== Inputs ===="); // To get a pretty-printed JSON module graph, change this line to // // err.println( // new GsonBuilder().setPrettyPrinting().create().toJson(moduleGraph.toJson())); // // TODO(bradfordcsmith): Come up with a JSON-printing version that will work when this code is // compiled with J2CL, so we can permanently improve this. err.println(Iterables.toString(moduleGraph.getAllInputs())); err.println("==== CompilerOptions ===="); err.println(options); err.println("==== WarningsGuard ===="); err.println(warningsGuard); } } private void logToFile(String logFileName, Supplier logStringSupplier) { try (LogFile log = this.createOrReopenLog(this.getClass(), logFileName)) { log.log(logStringSupplier); } } private void initWarningsGuard(WarningsGuard warningsGuard) { ImmutableList.Builder guards = ImmutableList.builder(); guards .add(new J2clSuppressWarningsGuard()) .add(new SuppressDocWarningsGuard(this, DiagnosticGroups.getRegisteredGroups())) .add(warningsGuard); this.warningsGuard = new ComposeWarningsGuard(guards.build()); } /** When the CompilerOptions and its WarningsGuard overlap, reconcile any discrepancies. */ protected void reconcileOptionsWithGuards() { // DiagnosticGroups override the plain checkTypes option. if (options.enables(DiagnosticGroups.CHECK_TYPES)) { options.checkTypes = true; } else if (options.disables(DiagnosticGroups.CHECK_TYPES)) { options.checkTypes = false; } else if (!options.checkTypes) { // If DiagnosticGroups did not override the plain checkTypes // option, and checkTypes is disabled, then turn off the // parser type warnings. options.setWarningLevel( DiagnosticGroup.forType(RhinoErrorReporter.TYPE_PARSE_ERROR), CheckLevel.OFF); } if (!options.checkTypes) { options.setWarningLevel(DiagnosticGroups.BOUNDED_GENERICS, CheckLevel.OFF); } // All passes must run the variable check. This synthesizes // variables later so that the compiler doesn't crash. It also // checks the externs file for validity. If you don't want to warn // about missing variable declarations, we shut that specific // error off. if (!options.checkSymbols && !options.enables(DiagnosticGroups.CHECK_VARIABLES)) { options.setWarningLevel(DiagnosticGroups.CHECK_VARIABLES, CheckLevel.OFF); } // If we're in transpile-only mode, we don't need to do checks for suspicious var usages. // Since we still have to run VariableReferenceCheck before transpilation to check block-scoped // variables, though, we disable the unnecessary warnings it produces relating to vars here. if (options.skipNonTranspilationPasses && !options.enables(DiagnosticGroups.CHECK_VARIABLES)) { options.setWarningLevel(DiagnosticGroups.CHECK_VARIABLES, CheckLevel.OFF); } // If we're in transpile-only mode, we don't need to check for missing requires unless the user // explicitly enables missing-provide checks. if (options.skipNonTranspilationPasses && !options.enables(DiagnosticGroups.MISSING_PROVIDE)) { options.setWarningLevel(DiagnosticGroups.MISSING_PROVIDE, CheckLevel.OFF); } } private @Nullable ImmutableMap> typedAstFilesystem; @Override @Nullable Supplier getTypedAstDeserializer(SourceFile file) { if (this.typedAstFilesystem == null) { return null; } Supplier ast = this.typedAstFilesystem.get(file); checkState( ast != null || file.getName().startsWith(SYNTHETIC_FILE_NAME_PREFIX), "TypedAST filesystem initialized, but missing requested file: %s", file); return ast; } /** * Initializes a compiler that will use pre-compiled TypedAst files instead of reading the source * from disk */ @GwtIncompatible public final void initWithTypedAstFilesystem( List externs, List sources, CompilerOptions options, InputStream typedAstListStream) { ImmutableList files = ImmutableList.builder().addAll(externs).addAll(sources).build(); this.initTypedAstFilesystem(files, typedAstListStream, options); this.init(externs, sources, options); } /** * Initializes a compiler that will use pre-compiled TypedAst files instead of reading the source * from disk */ @GwtIncompatible public void initModulesWithTypedAstFilesystem( List externs, List modules, CompilerOptions options, InputStream typedAstListStream) { ImmutableList.Builder filesBuilder = ImmutableList.builder(); filesBuilder.addAll(externs); for (JSChunk chunk : modules) { for (CompilerInput input : chunk.getInputs()) { filesBuilder.add(input.getSourceFile()); } } ImmutableList files = filesBuilder.build(); this.initTypedAstFilesystem(files, typedAstListStream, options); this.initModules(externs, modules, options); } @GwtIncompatible private void initTypedAstFilesystem( ImmutableList existingSourceFiles, InputStream typedAstListStream, CompilerOptions options) { checkState(this.typedAstFilesystem == null); options.setMergedPrecompiledLibraries(true); this.setLifeCycleStage(LifeCycleStage.COLORS_AND_SIMPLIFIED_JSDOC); // To speed up builds that don't run type-based optimizations, skip type deserialization boolean deserializeTypes = options.requiresTypesForOptimization(); TypedAstDeserializer.DeserializedAst astData = TypedAstDeserializer.deserializeFullAst( this, SYNTHETIC_EXTERNS_FILE, existingSourceFiles, typedAstListStream, deserializeTypes); this.typedAstFilesystem = astData.getFilesystem(); this.externProperties = astData.getExternProperties(); this.colorRegistry = astData.getColorRegistry().orNull(); this.setTypeCheckingHasRun(deserializeTypes); } @Override @GwtIncompatible public void initRuntimeLibraryTypedAsts(Optional colorPoolBuilder) { checkState(this.runtimeLibraryTypedAsts == null); String path = String.join( "", "/runtime_libs.typedast"); TypedAstDeserializer.DeserializedAst astData = TypedAstDeserializer.deserializeRuntimeLibraries( this, SYNTHETIC_EXTERNS_FILE, colorPoolBuilder, Compiler.class.getResourceAsStream(path)); // Re-index the runtime libraries by file name rather than SourceFile object LinkedHashMap> runtimeLibraryTypedAsts = new LinkedHashMap<>(); for (Map.Entry> library : astData.getFilesystem().entrySet()) { runtimeLibraryTypedAsts.computeIfAbsent( library.getKey().getName(), (f) -> library.getValue()); } this.runtimeLibraryTypedAsts = ImmutableMap.copyOf(runtimeLibraryTypedAsts); } /** Initializes the instance state needed for a compile job. */ public final void init( List externs, List sources, CompilerOptions options) { JSChunk module = new JSChunk(JSChunk.STRONG_CHUNK_NAME); for (SourceFile source : sources) { module.add(new CompilerInput(source, /* isExtern= */ false)); } List modules = new ArrayList<>(1); modules.add(module); initModules(externs, modules, options); addFilesToSourceMap(sources); } /** Initializes the instance state needed for a compile job if the sources are in modules. */ public void initModules( List externs, List modules, CompilerOptions options) { initOptions(options); checkFirstModule(modules); this.externs.clear(); for (SourceFile file : externs) { this.externs.add(new CompilerInput(file, /* isExtern= */ true)); } // Generate the module graph, and report any errors in the module specification as errors. try { this.moduleGraph = new JSChunkGraph(modules); } catch (ChunkDependenceException e) { // problems with the module format. Report as an error. The // message gives all details. report( JSError.make( MODULE_DEPENDENCY_ERROR, e.getChunk().getName(), e.getDependentChunk().getName())); return; } // Creating the module graph can move weak source around, and end up with empty modules. fillEmptyModules(getModules()); this.commentsPerFile = new ConcurrentHashMap<>(moduleGraph.getInputCount()); initBasedOnOptions(); initInputsByIdMap(); initAST(); // If debug logging is enabled, write out the module / chunk graph in GraphViz format. // This graph is often too big to reasonably render. // Using gvpr(1) is recommended to extract the parts of the graph that are of interest. // `dot -Tpng graph_file.dot > graph_file.png` will render an image. try (LogFile moduleGraphLog = createOrReopenLog(this.getClass(), "chunk_graph.dot")) { moduleGraphLog.log(DotFormatter.toDot(moduleGraph.toGraphvizGraph())); } } /** Do any initialization that is dependent on the compiler options. */ public void initBasedOnOptions() { inputSourceMaps.putAll(options.inputSourceMaps); // Create the source map if necessary. if (options.shouldGatherSourceMapInfo()) { sourceMap = options.sourceMapFormat.getInstance(); sourceMap.setPrefixMappings(options.sourceMapLocationMappings); if (options.applyInputSourceMaps) { sourceMap.setSourceFileMapping(this); if (options.sourceMapIncludeSourcesContent) { for (SourceMapInput inputSourceMap : inputSourceMaps.values()) { addSourceMapSourceFiles(inputSourceMap); } } } } } private static final DiagnosticType EMPTY_MODULE_LIST_ERROR = DiagnosticType.error("JSC_EMPTY_MODULE_LIST_ERROR", "At least one module must be provided"); private static final DiagnosticType EMPTY_ROOT_MODULE_ERROR = DiagnosticType.error( "JSC_EMPTY_ROOT_MODULE_ERROR", "Root module ''{0}'' must contain at least one source code input"); /** * Verifies that at least one module has been provided and that the first one has at least one * source code input. */ private void checkFirstModule(List modules) { if (modules.isEmpty()) { report(JSError.make(EMPTY_MODULE_LIST_ERROR)); } else if (modules.get(0).getInputs().isEmpty() && modules.size() > 1) { // The root module may only be empty if there is exactly 1 module. report(JSError.make(EMPTY_ROOT_MODULE_ERROR, modules.get(0).getName())); } } /** Creates an OS specific path string from parts */ public static String joinPathParts(String... pathParts) { return pathJoiner.join(pathParts); } /** Fill any empty modules with a place holder file. It makes any cross module motion easier. */ private void fillEmptyModules(Iterable modules) { for (JSChunk module : modules) { if (!module.getName().equals(JSChunk.WEAK_CHUNK_NAME) && module.getInputs().isEmpty()) { CompilerInput input = new CompilerInput(SourceFile.fromCode(createFillFileName(module.getName()), "")); input.setCompiler(this); module.add(input); } } } /** * Rebuilds the internal input map by iterating over all modules. This is necessary if inputs have * been added to or removed from a module after an {@link #init} or {@link #initModules} call. */ public void rebuildInputsFromModules() { initInputsByIdMap(); } static final DiagnosticType DUPLICATE_INPUT = DiagnosticType.error("JSC_DUPLICATE_INPUT", "Duplicate input: {0}"); static final DiagnosticType DUPLICATE_EXTERN_INPUT = DiagnosticType.error("JSC_DUPLICATE_EXTERN_INPUT", "Duplicate extern input: {0}"); /** Creates a map to make looking up an input by name fast. Also checks for duplicate inputs. */ void initInputsByIdMap() { inputsById.clear(); for (CompilerInput input : externs) { CompilerInput previous = putCompilerInput(input); if (previous != null) { report(JSError.make(DUPLICATE_EXTERN_INPUT, input.getName())); } } boolean hasZone = false; for (CompilerInput input : moduleGraph.getAllInputs()) { if (input.getName().endsWith("packages/zone.js/lib/zone.closure.js")) { hasZone = true; } CompilerInput previous = putCompilerInput(input); if (previous != null) { report(JSError.make(DUPLICATE_INPUT, input.getName())); } } if (hasZone && !options.allowsZoneJsWithAsyncFunctionsInOutput() && options.getOutputFeatureSet().contains(Feature.ASYNC_FUNCTIONS)) { throw new UnsupportedOperationException( "ZoneJS is incompatible with language level ES2017 or higher (See go/ngissue/31730)\n" + "Please set `--language_out=ECMASCRIPT_2016` (or older) in your flags."); } } /** Sets up the skeleton of the AST (the externs and root). */ private void initAST() { jsRoot = IR.root(); externsRoot = IR.root(); externAndJsRoot = IR.root(externsRoot, jsRoot); if (this.typedAstFilesystem != null) { this.getSynthesizedExternsInput(); // Force lazy creation. } } /** Compiles a single source file and a single externs file. */ public Result compile(SourceFile extern, SourceFile input, CompilerOptions options) { return compile(ImmutableList.of(extern), ImmutableList.of(input), options); } /** * Compiles a list of inputs. * *

This is a convenience method to wrap up all the work of compilation, including generating * the error and warning report. * *

NOTE: All methods called here must be public, because client code must be able to replicate * and customize this. */ public Result compile( List externs, List inputs, CompilerOptions options) { // The compile method should only be called once. checkState(jsRoot == null); try { init(externs, inputs, options); if (options.printConfig) { printConfig(); } if (!hasErrors()) { parseForCompilation(); } if (!hasErrors()) { if (options.getInstrumentForCoverageOnly()) { // TODO(bradfordcsmith): The option to instrument for coverage only should belong to the // runner, not the compiler. instrumentForCoverage(); } else { stage1Passes(); if (!hasErrors()) { stage2Passes(); if (!hasErrors()) { stage3Passes(); } } } performPostCompilationTasks(); } } finally { generateReport(); } return getResult(); } /** * Generates a report of all warnings and errors found during compilation to stderr. * *

Client code must call this method explicitly if it doesn't use one of the convenience * methods that do so automatically. * *

Always call this method, even if the compiler throws an exception. The report will include * information about the exception. */ public void generateReport() { Tracer t = newTracer("generateReport"); errorManager.generateReport(); stopTracer(t, "generateReport"); } /** * Compiles a list of modules. * *

This is a convenience method to wrap up all the work of compilation, including generating * the error and warning report. * *

NOTE: All methods called here must be public, because client code must be able to replicate * and customize this. */ public Result compileModules( List externs, List modules, CompilerOptions options) { // The compile method should only be called once. checkState(jsRoot == null); try { initModules(externs, modules, options); if (options.printConfig) { printConfig(); } if (!hasErrors()) { parseForCompilation(); } if (!hasErrors()) { // TODO(bradfordcsmith): The option to instrument for coverage only should belong to the // runner, not the compiler. if (options.getInstrumentForCoverageOnly()) { instrumentForCoverage(); } else { stage1Passes(); if (!hasErrors()) { stage2Passes(); if (!hasErrors()) { stage3Passes(); } } } performPostCompilationTasks(); } } finally { generateReport(); } return getResult(); } /** * Perform compiler passes for stage 1 of compilation. * *

Stage 1 consists primarily of error and type checking passes. * *

{@code parseForCompilation()} must be called before this method is called. * *

The caller is responsible for also calling {@code generateReport()} to generate a report of * warnings and errors to stderr. See the invocation in {@link #compile} for a good example. */ public void stage1Passes() { checkState(moduleGraph != null, "No inputs. Did you call init() or initModules()?"); checkState(!hasErrors()); checkState(!options.getInstrumentForCoverageOnly()); runInCompilerThread( () -> { performChecks(); return null; }); } /** * Perform compiler passes for stage 2 of compilation. * *

Stage 2 consists primarily of transpilation and optimization passes. * *

{@code stage1Passes()} must be called before this method is called. * *

The caller is responsible for also calling {@code generateReport()} to generate a report of * warnings and errors to stderr. See the invocation in {@link #compile} for a good example. */ public void stage2Passes() { checkState(moduleGraph != null, "No inputs. Did you call init() or initModules()?"); checkState(!hasErrors()); checkState(!options.getInstrumentForCoverageOnly()); JSChunk weakModule = moduleGraph.getChunkByName(JSChunk.WEAK_CHUNK_NAME); if (weakModule != null) { for (CompilerInput i : moduleGraph.getAllInputs()) { if (i.getSourceFile().isWeak()) { checkState( i.getChunk() == weakModule, "Expected all weak files to be in the weak module."); } } } runInCompilerThread( () -> { if (options.shouldOptimize()) { performTranspilationAndOptimizations(); } return null; }); } /** * Perform compiler passes for stage 3 of compilation. * *

Stage 3 consists primarily of localization passes. * *

{@code stage2Passes()} must be called before this method is called. * *

The caller is responsible for also calling {@code generateReport()} to generate a report of * warnings and errors to stderr. See the invocation in {@link #compile} for a good example. */ public void stage3Passes() { checkState(moduleGraph != null, "No inputs. Did you call init() or initModules()?"); checkState(!hasErrors()); checkState(!options.getInstrumentForCoverageOnly()); runInCompilerThread( () -> { if (options.shouldOptimize()) { performFinalizations(); } return null; }); } /** Disable threads. This is for clients that run on AppEngine and don't have threads. */ public void disableThreads() { compilerExecutor.disableThreads(); } /** * Sets the timeout when Compiler is run in a thread * * @param timeout seconds to wait before timeout */ public void setTimeout(int timeout) { compilerExecutor.setTimeout(timeout); } /** * The primary purpose of this method is to run the provided code with a larger than standard * stack. */ T runInCompilerThread(Callable callable) { return compilerExecutor.runInCompilerThread( callable, options != null && options.getTracerMode().isOn()); } private void performChecks() { if (options.skipNonTranspilationPasses) { // i.e. whitespace-only mode, which will not work with goog.module without: whitespaceOnlyPasses(); if (options.needsTranspilationFrom(options.getLanguageIn().toFeatureSet())) { transpileAndDontCheck(); } } else { check(); } } /** * Performs all the bookkeeping required at the end of a compilation. * *

This method must be called if the compilation makes it as far as doing checks. * *

DON'T call it if the compiler threw an exception. * *

DO call it even when {@code hasErrors()} returns true. */ public void performPostCompilationTasks() { runInCompilerThread( () -> { performPostCompilationTasksInternal(); return null; }); } /** Performs all the bookkeeping required at the end of a compilation. */ private void performPostCompilationTasksInternal() { if (options.devMode == DevMode.START_AND_END) { runValidityCheck(); } if (tracker != null) { if (options.getTracerOutput() == null) { tracker.outputTracerReport(this.outStream); } else { try (PrintStream out = new PrintStream(Files.newOutputStream(options.getTracerOutput()))) { tracker.outputTracerReport(out); } catch (Exception e) { throw new RuntimeException(e); } } } } /** * Instrument code for coverage. * *

{@code parseForCompilation()} must be called before this method is called. * *

The caller is responsible for also calling {@code generateReport()} to generate a report of * warnings and errors to stderr. See the invocation in {@link #compile} for a good example. * *

This method is mutually exclusive with stage1Passes() and stage2Passes(). Either call those * two methods or this one, but not both. */ public void instrumentForCoverage() { checkState(moduleGraph != null, "No inputs. Did you call init() or initModules()?"); checkState(!hasErrors()); runInCompilerThread( () -> { checkState(options.getInstrumentForCoverageOnly()); checkState(!hasErrors()); if (options.getInstrumentForCoverageOption() != InstrumentOption.NONE) { instrumentForCoverageInternal(options.getInstrumentForCoverageOption()); } return null; }); } private void instrumentForCoverageInternal(InstrumentOption instrumentForCoverageOption) { Tracer tracer = newTracer("instrumentationPass"); process( new CoverageInstrumentationPass( this, CoverageReach.ALL, instrumentForCoverageOption, options.getProductionInstrumentationArrayName())); stopTracer(tracer, "instrumentationPass"); } /** * Parses input files in preparation for compilation. * *

Either {@code init()} or {@code initModules()} must be called first to set up the input * files to be read. * *

TODO(bradfordcsmith): Rename this to parse() */ public void parseForCompilation() { runInCompilerThread( () -> { parseForCompilationInternal(); return null; }); } /** * Parses input files in preparation for compilation. * *

Either {@code init()} or {@code initModules()} must be called first to set up the input * files to be read. * *

TODO(bradfordcsmith): Rename this to parse() */ private void parseForCompilationInternal() { CompilerOptionsPreprocessor.preprocess(options); maybeSetTracker(); parseInputs(); } /** * Parses input files without doing progress tracking that is part of a full compile. * *

Either {@code init()} or {@code initModules()} must be called first to set up the input * files to be read. * *

TODO(bradfordcsmith): Rename this to parseIndependentOfCompilation() or similar. */ public void parse() { parseInputs(); } public Node parse(SourceFile file) { initCompilerOptionsIfTesting(); logger.finest("Parsing: " + file.getName()); return new JsAst(file).getAstRoot(this); } PassConfig getPassConfig() { if (passes == null) { passes = createPassConfigInternal(); } return passes; } /** Create the passes object. Clients should use setPassConfig instead of overriding this. */ protected PassConfig createPassConfigInternal() { return new DefaultPassConfig(options); } /** * @param passes The PassConfig to use with this Compiler. * @throws NullPointerException if passes is null * @throws IllegalStateException if this.passes has already been assigned */ public void setPassConfig(PassConfig passes) { // Important to check for null because if setPassConfig(null) is // called before this.passes is set, getPassConfig() will create a // new PassConfig object and use that, which is probably not what // the client wanted since they probably meant to use their // own PassConfig object. checkNotNull(passes); checkState(this.passes == null, "setPassConfig was already called"); this.passes = passes; } public void whitespaceOnlyPasses() { runCustomPasses(CustomPassExecutionTime.BEFORE_CHECKS); Tracer t = newTracer("runWhitespaceOnlyPasses"); try { for (PassFactory pf : getPassConfig().getWhitespaceOnlyPasses().build()) { pf.create(this).process(externsRoot, jsRoot); } } finally { stopTracer(t, "runWhitespaceOnlyPasses"); } } public void transpileAndDontCheck() { Tracer t = newTracer("runTranspileOnlyPasses"); try { for (PassFactory pf : getPassConfig().getTranspileOnlyPasses().build()) { if (hasErrors()) { return; } pf.create(this).process(externsRoot, jsRoot); } } finally { stopTracer(t, "runTranspileOnlyPasses"); } } private PhaseOptimizer createPhaseOptimizer() { PhaseOptimizer phaseOptimizer = new PhaseOptimizer(this, tracker); if (options.devMode == DevMode.EVERY_PASS) { phaseOptimizer.setValidityCheck(validityCheck); } if (options.getCheckDeterminism()) { phaseOptimizer.setPrintAstHashcodes(true); } return phaseOptimizer; } void check() { runCustomPasses(CustomPassExecutionTime.BEFORE_CHECKS); phaseOptimizer = createPhaseOptimizer(); phaseOptimizer.consume(getPassConfig().getChecks().build()); phaseOptimizer.process(externsRoot, jsRoot); if (hasErrors()) { return; } runCustomPasses(CustomPassExecutionTime.BEFORE_OPTIMIZATIONS); phaseOptimizer = null; } @Override void setExternExports(String externExports) { this.externExports = externExports; } @Override void process(CompilerPass p) { p.process(externsRoot, jsRoot); } private final PassFactory validityCheck = PassFactory.builder() .setName("validityCheck") .setRunInFixedPointLoop(true) .setInternalFactory(ValidityCheck::new) .setCondition( new Function() { @Override public Boolean apply(CompilerOptions o) { throw new IllegalStateException("Unexpected"); } }) // not used // o -> { // }) // not used. .build(); private void runValidityCheck() { validityCheck.create(this).process(externsRoot, jsRoot); } /** Runs custom passes that are designated to run at a particular time. */ private void runCustomPasses(CustomPassExecutionTime executionTime) { if (options.customPasses != null) { Tracer t = newTracer("runCustomPasses"); try { for (CompilerPass p : options.customPasses.get(executionTime)) { process(p); } } finally { stopTracer(t, "runCustomPasses"); } } } @Override final void beforePass(String passName) { super.beforePass(passName); } @Override final void afterPass(String passName) { super.afterPass(passName); this.maybePrintSourceAfterEachPass(passName); } private void maybePrintSourceAfterEachPass(String passName) { if (!options.printSourceAfterEachPass) { return; } String currentJsSource = getCurrentJsSource(); if (currentJsSource.equals(this.lastJsSource)) { return; } if (this.isDebugLoggingEnabled()) { try (LogFile log = this.createOrReopenIndexedLog(this.getClass(), "source_after_pass", passName)) { log.log(currentJsSource); } } else { System.err.println(); System.err.println("// " + passName + " yields:"); System.err.println("// ************************************"); System.err.println(currentJsSource); System.err.println("// ************************************"); } this.lastJsSource = currentJsSource; } final String getCurrentJsSource() { this.resetAndIntitializeSourceMap(); List fileNameRegexList = options.filesToPrintAfterEachPassRegexList; List chunkNameRegexList = options.chunksToPrintAfterEachPassRegexList; final Set qnameSet = new LinkedHashSet<>(options.qnameUsesToPrintAfterEachPassList); StringBuilder builder = new StringBuilder(); if (fileNameRegexList.isEmpty() && chunkNameRegexList.isEmpty() && qnameSet.isEmpty()) { return toSource(); } if (!fileNameRegexList.isEmpty()) { checkNotNull(externsRoot); checkNotNull(jsRoot); for (Node r : ImmutableList.of(externsRoot, jsRoot)) { for (Node fileNode = r.getFirstChild(); fileNode != null; fileNode = fileNode.getNext()) { String fileName = fileNode.getSourceFileName(); for (String regex : fileNameRegexList) { if (fileName.matches(regex)) { String source = "// " + fileName + "\n" + toSource(fileNode); builder.append(source); break; } } } } if (builder.length() == 0) { builder.append("// No files matched any of: ").append(fileNameRegexList); } } if (!chunkNameRegexList.isEmpty()) { final Iterable chunks = checkNotNull(getModules()); final List unmatchedChunkNames = new ArrayList<>(); for (JSChunk jsChunk : chunks) { final String chunkName = jsChunk.getName(); for (String regex : chunkNameRegexList) { if (chunkName.matches(regex)) { String source = "// module '" + chunkName + "'\n" + toSource(jsChunk); builder.append(source); break; } } unmatchedChunkNames.add(chunkName); } if (builder.length() == 0) { builder.append("// No chunks were matched:\n"); for (String chunkName : unmatchedChunkNames) { builder.append("// ").append(chunkName).append("\n"); } } } if (!qnameSet.isEmpty()) { // Keep track of how a qualified name we're interested in gets renamed. // Note that a qname can actually be renamed multiple times during compilation, // so we need a multimap here. final Multimap originalToNewQNameMap = LinkedHashMultimap.create(); // Get a stream of top level statement nodes that contain at least one reference to one of the // qnames we are interested in. Take note of relevant renamings while we're at it. final Stream statementStream = getTopLevelStatements(jsRoot) .filter( (Node statement) -> // filter to just those statements containing interesting qnames NodeUtil.has( statement, (Node n) -> { checkNotNull(n); if (!n.isQualifiedName()) { return false; } String qname = n.getQualifiedName(); if (qnameSet.contains(qname)) { return true; } String originalQname = n.getOriginalQualifiedName(); if (originalQname != null && qnameSet.contains(originalQname)) { originalToNewQNameMap.put(originalQname, qname); return true; } else { return false; } }, node -> true)); builder.append("//\n"); builder.append("// closure-compiler: Printing all of the top-level statements\n"); builder.append("// that contain references to these qualified names.\n"); builder.append("//\n"); for (String qname : qnameSet) { builder.append(SimpleFormat.format("// '%s'\n", qname)); for (String newName : originalToNewQNameMap.get(qname)) { builder.append(SimpleFormat.format("// '%s' (originally '%s')\n", newName, qname)); } } builder.append("//\n"); statementStream.forEach( (Node statement) -> builder .append(SimpleFormat.format("// %s\n", statement.getLocation())) .append(toSource(statement)) .append("\n")); } return builder.toString(); } private Stream getTopLevelStatements(Node root) { Stream.Builder builder = Stream.builder(); NodeTraversal.traverse( this, root, new AbstractPreOrderCallback() { @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { if (NodeUtil.isStatement(n)) { builder.add(n); return false; } else { return true; } } }); return builder.build(); } @Override public final @Nullable Node getScriptNode(String filename) { return scriptNodeByFilename.get(checkNotNull(filename)); } /** Returns a new tracer for the given pass name. */ Tracer newTracer(String passName) { String comment = passName + (recentChange.hasCodeChanged() ? " on recently changed AST" : ""); if (options.getTracerMode().isOn() && tracker != null) { tracker.recordPassStart(passName, true); } return new Tracer("Compiler", comment); } void stopTracer(Tracer t, String passName) { long result = t.stop(); if (options.getTracerMode().isOn() && tracker != null) { tracker.recordPassStop(passName, result); } } /** Returns the result of the compilation. */ public Result getResult() { Set transpiledFiles = new HashSet<>(); if (jsRoot != null) { for (Node scriptNode = jsRoot.getFirstChild(); scriptNode != null; scriptNode = scriptNode.getNext()) { if (scriptNode.getBooleanProp(Node.TRANSPILED)) { transpiledFiles.add(getSourceFileByName(scriptNode.getSourceFileName())); } } } return new Result( getErrors(), getWarnings(), this.variableMap, this.propertyMap, null, this.stringMap, this.instrumentationMapping, this.sourceMap, this.externExports, this.cssNames, this.idGeneratorMap, transpiledFiles); } /** Returns the list of errors (never null). */ public ImmutableList getErrors() { return (errorManager == null) ? ImmutableList.of() : errorManager.getErrors(); } /** Returns the list of warnings (never null). */ public ImmutableList getWarnings() { return (errorManager == null) ? ImmutableList.of() : errorManager.getWarnings(); } @Override public Node getRoot() { return externAndJsRoot; } @Override FeatureSet getFeatureSet() { return featureSet; } @Override void setFeatureSet(FeatureSet fs) { featureSet = fs; } @Override UniqueIdSupplier getUniqueIdSupplier() { return this.uniqueIdSupplier; } /** Creates a new id for making unique names. */ @Deprecated private int nextUniqueNameId() { return uniqueNameId++; } /** Resets the unique name id counter */ @Deprecated @VisibleForTesting void resetUniqueNameId() { uniqueNameId = 0; } /** * Legacy supplier for getting unique names. * * @deprecated This is deprecated because this supplier fails to generate unique Ids across input * files when used for tagged template literal transpilation. Kindly use the new {@code * UniqueIdSupplier} instead. */ @Deprecated @Override com.google.common.base.Supplier getUniqueNameIdSupplier() { return () -> String.valueOf(Compiler.this.nextUniqueNameId()); } @Override boolean areNodesEqualForInlining(Node n1, Node n2) { if (options.shouldAmbiguateProperties() || options.shouldDisambiguateProperties()) { // The type based optimizations require that type information is preserved // during other optimizations. return n1.isEquivalentToTyped(n2); } else { return n1.isEquivalentTo(n2); } } // ------------------------------------------------------------------------ // Inputs // ------------------------------------------------------------------------ // TODO(nicksantos): Decide which parts of these belong in an AbstractCompiler // interface, and which ones should always be injected. @Override public @Nullable CompilerInput getInput(InputId id) { // TODO(bradfordcsmith): Allowing null id is less ideal. Add checkNotNull(id) here and fix // call sites that break. if (id == null) { return null; } return inputsById.get(id); } private CompilerInput putCompilerInput(CompilerInput input) { input.setCompiler(this); return inputsById.put(input.getInputId(), input); } /** * Gets the graph of JS source modules. * *

Returns null if {@code #init} or {@code #initModules} hasn't been called yet. Otherwise, the * result is always a module graph, even in the degenerate case where there's only one module. */ @Override @Nullable JSChunkGraph getModuleGraph() { return moduleGraph; } /** * Gets the JS source modules in dependency order. * *

Returns null if {@code #init} or {@code #initModules} hasn't been called yet. Otherwise, the * result is always non-empty, even in the degenerate case where there's only one module. */ public @Nullable Iterable getModules() { return moduleGraph != null ? moduleGraph.getAllChunks() : null; } @Override public void clearJSTypeRegistry() { typeRegistry = null; typeValidator = null; abstractInterpreter = null; } @Override public boolean isTypeRegistryCleared() { return typeCheckingHasRun && typeRegistry == null; } @Override public JSTypeRegistry getTypeRegistry() { if (typeRegistry == null) { checkState( !this.hasTypeCheckingRun(), // This could throw when calling "getTypeRegistry()" during the optimizations phase when // JSTypes have been converted to optimization colors "Attempted to re-initialize JSTypeRegistry after it had been cleared"); typeRegistry = new JSTypeRegistry(oldErrorReporter, forwardDeclaredTypes); } return typeRegistry; } @Override public ColorRegistry getColorRegistry() { return checkNotNull(colorRegistry, "Color registry has not been initialized yet"); } @Override public void setColorRegistry(ColorRegistry colorRegistry) { checkState(this.runtimeLibraryTypedAsts != null); this.colorRegistry = colorRegistry; } @Override public void forwardDeclareType(String typeName) { forwardDeclaredTypes.add(typeName); } @Override void setTypeCheckingHasRun(boolean hasRun) { this.typeCheckingHasRun = hasRun; } @Override public boolean hasTypeCheckingRun() { return this.typeCheckingHasRun; } @Override public boolean hasOptimizationColors() { return this.colorRegistry != null; } private @Nullable TypedScopeCreator typedScopeCreator; @Override public ScopeCreator getTypedScopeCreator() { if (this.typedScopeCreator == null) { checkState( !this.hasTypeCheckingRun(), // This could throw when calling "getTypedScopeCreator()" during the optimizations phase // when JSTypes have been converted to optimization colors "Attempted to re-initialize TypedScopeCreator after it had been cleared"); this.typedScopeCreator = new TypedScopeCreator(this); } return this.typedScopeCreator; } @Override void clearTypedScopeCreator() { this.typedScopeCreator = null; } DefaultPassConfig ensureDefaultPassConfig() { PassConfig passes = getPassConfig().getBasePassConfig(); checkState( passes instanceof DefaultPassConfig, "PassConfigs must eventually delegate to the DefaultPassConfig"); return (DefaultPassConfig) passes; } public SymbolTable buildKnownSymbolTable() { SymbolTable symbolTable = new SymbolTable(this, getTypeRegistry()); if (this.typedScopeCreator != null) { symbolTable.addScopes(this.typedScopeCreator.getAllMemoizedScopes()); symbolTable.addSymbolsFrom(this.typedScopeCreator); } else { symbolTable.findScopes(externsRoot, jsRoot); } GlobalNamespace globalNamespace = new GlobalNamespace(this, this.externsRoot, this.jsRoot); symbolTable.addSymbolsFrom(globalNamespace); ReferenceCollector refCollector = new ReferenceCollector( this, ReferenceCollector.DO_NOTHING_BEHAVIOR, new SyntacticScopeCreator(this)); refCollector.process(getRoot()); symbolTable.addSymbolsFrom(refCollector); PreprocessorSymbolTable preprocessorSymbolTable = ensureDefaultPassConfig().getPreprocessorSymbolTable(); if (preprocessorSymbolTable != null) { symbolTable.addSymbolsFrom(preprocessorSymbolTable); } symbolTable.flattenGoogModuleExports(); symbolTable.fillNamespaceReferences(); symbolTable.fillPropertyScopes(); symbolTable.fillThisReferences(externsRoot, jsRoot); symbolTable.fillPropertySymbols(externsRoot, jsRoot); symbolTable.fillSuperReferences(externsRoot, jsRoot); symbolTable.fillJSDocInfo(externsRoot, jsRoot); symbolTable.fillSymbolVisibility(externsRoot, jsRoot); symbolTable.fillGoogProvideModuleRequires(externsRoot, jsRoot); symbolTable.removeGeneratedSymbols(); return symbolTable; } private @Nullable TypedScope topScope = null; @Override public TypedScope getTopScope() { return this.topScope; } @Override void setTopScope(TypedScope x) { checkState(x == null || x.getParent() == null, x); this.topScope = x; } @Override public ReverseAbstractInterpreter getReverseAbstractInterpreter() { if (abstractInterpreter == null) { ChainableReverseAbstractInterpreter interpreter = new SemanticReverseAbstractInterpreter(getTypeRegistry()); if (options.closurePass) { interpreter = new ClosureReverseAbstractInterpreter(getTypeRegistry()).append(interpreter).getFirst(); } abstractInterpreter = interpreter; } return abstractInterpreter; } @Override TypeValidator getTypeValidator() { if (typeValidator == null) { checkState( !this.hasTypeCheckingRun(), // This could throw when calling "getTypeValidator()" during the optimizations phase when // JSTypes have been converted to optimization colors "Attempted to re-initialize TypeValidator after it had been cleared"); typeValidator = new TypeValidator(this); } return typeValidator; } @Override public Iterable getTypeMismatches() { if (this.typeCheckingHasRun) { return getTypeValidator().getMismatches(); } throw new RuntimeException("Can't ask for type mismatches before type checking."); } @Override StaticScope getTranspilationNamespace() { if (this.transpilationNamespace == null) { // Note: this also needs to happen /after/ the runtime libraries are injected, or we will not // be able to find the runtime library code. checkState( !getLifeCycleStage().isNormalized(), "cannot init transpilation namespace after optimizations phase, or information may be" + " lost"); GlobalNamespace gn = new GlobalNamespace(this, this.getExternsRoot(), this.getJsRoot()); // Exclude user-defined code names, to save memory and help prevent misuse. gn.setShouldTraverseScriptPredicate( (Node script) -> script.isFromExterns() || script.isFirstChildOf(this.getJsRoot())); gn.getNameForest(); // ensure namespace is generated // wrap GlobalNamespace to prevent looking up extra information in GlobalNamespace this.transpilationNamespace = gn; } return this.transpilationNamespace; } public void maybeSetTracker() { if (!options.getTracerMode().isOn()) { return; } tracker = new PerformanceTracker(externsRoot, jsRoot, options.getTracerMode()); addChangeHandler(tracker.getCodeChangeHandler()); } void initializeModuleLoader() { ModuleResolverFactory moduleResolverFactory = null; switch (options.getModuleResolutionMode()) { case BROWSER: moduleResolverFactory = BrowserModuleResolver.FACTORY; break; case NODE: // processJsonInputs requires a module loader to already be defined // so we redefine it afterwards with the package.json inputs moduleResolverFactory = new NodeModuleResolver.Factory(processJsonInputs(moduleGraph.getAllInputs())); break; case WEBPACK: moduleResolverFactory = new WebpackModuleResolver.Factory(inputPathByWebpackId); break; case BROWSER_WITH_TRANSFORMED_PREFIXES: moduleResolverFactory = new BrowserWithTransformedPrefixesModuleResolver.Factory( options.getBrowserResolverPrefixReplacements()); break; } this.moduleLoader = ModuleLoader.builder() .setModuleRoots(options.moduleRoots) .setInputs(moduleGraph.getAllInputs()) .setFactory(moduleResolverFactory) .setPathResolver(PathResolver.RELATIVE) .setPathEscaper(options.getPathEscaper()) .build(); } // ------------------------------------------------------------------------ // Parsing // ------------------------------------------------------------------------ /** * Parses the externs and main inputs. * * @return A synthetic root node whose two children are the externs root and the main root */ @Nullable Node parseInputs() { boolean devMode = options.devMode != DevMode.OFF; // If old roots exist (we are parsing a second time), detach each of the // individual file parse trees. externsRoot.detachChildren(); jsRoot.detachChildren(); scriptNodeByFilename.clear(); Tracer tracer = newTracer(PassNames.PARSE_INPUTS); beforePass(PassNames.PARSE_INPUTS); try { // Parse externs sources. if (options.numParallelThreads > 1) { new PrebuildAst(this, options.numParallelThreads).prebuild(externs); } for (CompilerInput input : externs) { Node n = checkNotNull(input.getAstRoot(this)); if (hasErrors()) { return null; } externsRoot.addChildToBack(n); scriptNodeByFilename.put(input.getSourceFile().getName(), n); } if (options.transformAMDToCJSModules) { processAMDModules(moduleGraph.getAllInputs()); } if (options.getLanguageIn().toFeatureSet().has(Feature.MODULES) || options.getProcessCommonJSModules()) { initializeModuleLoader(); } else { // Use an empty module loader if we're not actually dealing with modules. this.moduleLoader = ModuleLoader.EMPTY; } if (options.getDependencyOptions().needsManagement()) { findModulesFromEntryPoints( options.getLanguageIn().toFeatureSet().has(Feature.MODULES), options.getProcessCommonJSModules()); } else if (options.needsTranspilationFrom(FeatureSet.ES2015_MODULES) || options.getProcessCommonJSModules()) { if (options.getLanguageIn().toFeatureSet().has(Feature.MODULES)) { parsePotentialModules(moduleGraph.getAllInputs()); } // Build a map of module identifiers for any input which provides no namespace. // These files could be imported modules which have no exports, but do have side effects. Map inputModuleIdentifiers = new HashMap<>(); for (CompilerInput input : moduleGraph.getAllInputs()) { if (input.getKnownProvides().isEmpty()) { ModulePath modPath = moduleLoader.resolve(input.getSourceFile().getName()); inputModuleIdentifiers.put(modPath.toModuleName(), input); } } // Find out if any input attempted to import a module that had no exports. // In this case we must force module rewriting to occur on the imported file Map inputsToRewrite = new HashMap<>(); for (CompilerInput input : moduleGraph.getAllInputs()) { for (String require : input.getKnownRequiredSymbols()) { if (inputModuleIdentifiers.containsKey(require) && !inputsToRewrite.containsKey(require)) { inputsToRewrite.put(require, inputModuleIdentifiers.get(require)); } } } for (CompilerInput input : inputsToRewrite.values()) { input.setJsModuleType(ModuleType.IMPORTED_SCRIPT); } } if (this.moduleLoader != null) { this.moduleLoader.setErrorHandler(this); } orderInputs(); // If in IDE mode, we ignore the error and keep going. if (hasErrors()) { return null; } // Build the AST. if (options.numParallelThreads > 1) { new PrebuildAst(this, options.numParallelThreads).prebuild(moduleGraph.getAllInputs()); } for (CompilerInput input : moduleGraph.getAllInputs()) { Node n = checkNotNull(input.getAstRoot(this)); if (devMode) { runValidityCheck(); if (hasErrors()) { return null; } } if (options.shouldGatherSourceMapInfo() || options.getExternExportsPath() != null || !options.replaceStringsFunctionDescriptions.isEmpty()) { // Annotate the nodes in the tree with information from the // input file. This information is used to construct the SourceMap. SourceInformationAnnotator sia = DevMode.OFF.equals(options.devMode) ? SourceInformationAnnotator.create() : SourceInformationAnnotator.createWithAnnotationChecks(input.getName()); NodeTraversal.traverse(this, n, sia); } if (NodeUtil.isFromTypeSummary(n)) { input.setIsExtern(); externsRoot.addChildToBack(n); } else { jsRoot.addChildToBack(n); } scriptNodeByFilename.put(input.getSourceFile().getName(), n); } if (hasErrors()) { return null; } return externAndJsRoot; } finally { afterPass(PassNames.PARSE_INPUTS); stopTracer(tracer, PassNames.PARSE_INPUTS); // Save on memory. Any future calls to "createInputConsideringTypedAstFilesystem" will throw. // TODO(lharker): do we actually need the synthetic externs file in stage 2? if (this.typedAstFilesystem != null) { this.typedAstFilesystem = ImmutableMap.of( SYNTHETIC_EXTERNS_FILE, this.typedAstFilesystem.get(SYNTHETIC_EXTERNS_FILE)); } } } void orderInputsWithLargeStack() { runInCompilerThread( () -> { Tracer tracer = newTracer("orderInputsWithLargeStack"); try { orderInputs(); } finally { stopTracer(tracer, "orderInputsWithLargeStack"); } return null; }); } void orderInputs() { maybeDoThreadedParsing(); // Before dependency pruning, save a copy of the original inputs to use for externs hoisting. ImmutableList originalInputs = ImmutableList.copyOf(moduleGraph.getAllInputs()); // Externs must be marked before dependency management since it needs to know what is an extern. markExterns(originalInputs); // Check if the sources need to be re-ordered. boolean staleInputs = false; if (options.getDependencyOptions().needsManagement()) { for (CompilerInput input : moduleGraph.getAllInputs()) { // Forward-declare all the provided types, so that they // are not flagged even if they are dropped from the process. for (String provide : input.getProvides()) { forwardDeclareType(provide); } } try { moduleGraph.manageDependencies(this, options.getDependencyOptions()); staleInputs = true; } catch (MissingProvideException e) { report(JSError.make(MISSING_ENTRY_ERROR, e.getMessage())); } catch (MissingChunkException e) { report(JSError.make(MISSING_MODULE_ERROR, e.getMessage())); } } hoistExterns(originalInputs); // Manage dependencies may move weak sources around, and end up with empty modules. fillEmptyModules(getModules()); hoistNoCompileFiles(); if (staleInputs) { repartitionInputs(); } } /** * Find modules by recursively traversing dependencies starting with the entry points. * *

Causes a regex parse of every file, and a full parse of every file reachable from the entry * points (which would be required by later compilation passes regardless). * *

If the dependency mode is set to PRUNE_LEGACY, inputs which the regex parse does not * identify as ES modules and which do not contain any provide statements are considered to be * additional entry points. */ private void findModulesFromEntryPoints( boolean supportEs6Modules, boolean supportCommonJSModules) { maybeDoThreadedParsing(); List entryPoints = new ArrayList<>(); Map inputsByProvide = new HashMap<>(); Map inputsByIdentifier = new HashMap<>(); for (CompilerInput input : moduleGraph.getAllInputs()) { Iterable provides = Iterables.filter(input.getProvides(), p -> !p.startsWith("module$")); if (!options.getDependencyOptions().shouldDropMoochers() && Iterables.isEmpty(provides)) { entryPoints.add(input); } inputsByIdentifier.put( ModuleIdentifier.forFile(input.getPath().toString()).toString(), input); for (String provide : provides) { inputsByProvide.put(provide, input); } } for (ModuleIdentifier moduleIdentifier : options.getDependencyOptions().getEntryPoints()) { CompilerInput input = inputsByProvide.get(moduleIdentifier.toString()); if (input == null) { input = inputsByIdentifier.get(moduleIdentifier.toString()); } if (input != null) { entryPoints.add(input); } } Set workingInputSet = Sets.newHashSet(moduleGraph.getAllInputs()); for (CompilerInput entryPoint : entryPoints) { findModulesFromInput( entryPoint, /* wasImportedByModule = */ false, workingInputSet, inputsByIdentifier, inputsByProvide, supportEs6Modules, supportCommonJSModules); } } /** Traverse an input's dependencies to find additional modules. */ private void findModulesFromInput( CompilerInput input, boolean wasImportedByModule, Set inputs, Map inputsByIdentifier, Map inputsByProvide, boolean supportEs6Modules, boolean supportCommonJSModules) { if (!inputs.remove(input)) { // It's possible for a module to be included as both a script // and a module in the same compilation. In these cases, it should // be forced to be a module. if (wasImportedByModule && input.getJsModuleType() == ModuleType.NONE) { input.setJsModuleType(ModuleType.IMPORTED_SCRIPT); } return; } FindModuleDependencies findDeps = new FindModuleDependencies( this, supportEs6Modules, supportCommonJSModules, inputPathByWebpackId); findDeps.process(checkNotNull(input.getAstRoot(this))); // If this input was imported by another module, it is itself a module // so we force it to be detected as such. if (wasImportedByModule && input.getJsModuleType() == ModuleType.NONE) { input.setJsModuleType(ModuleType.IMPORTED_SCRIPT); } this.moduleTypesByName.put(input.getPath().toModuleName(), input.getJsModuleType()); Iterable allDeps = Iterables.concat( input.getRequiredSymbols(), input.getDynamicRequires(), input.getTypeRequires()); for (String requiredNamespace : allDeps) { CompilerInput requiredInput = null; boolean requiredByModuleImport = false; if (inputsByProvide.containsKey(requiredNamespace)) { requiredInput = inputsByProvide.get(requiredNamespace); } else if (inputsByIdentifier.containsKey(requiredNamespace)) { requiredByModuleImport = true; requiredInput = inputsByIdentifier.get(requiredNamespace); } if (requiredInput != null) { findModulesFromInput( requiredInput, requiredByModuleImport, inputs, inputsByIdentifier, inputsByProvide, supportEs6Modules, supportCommonJSModules); } } } /** Hoists inputs with the @externs annotation into the externs list. */ void hoistExterns(ImmutableList originalInputs) { boolean staleInputs = false; for (CompilerInput input : originalInputs) { if (hoistIfExtern(input)) { staleInputs = true; } } if (staleInputs) { repartitionInputs(); } } /** * Hoists a compiler input to externs if it contains the @externs annotation. Return whether or * not the given input was hoisted. */ private boolean hoistIfExtern(CompilerInput input) { if (input.getHasExternsAnnotation()) { // If the input file is explicitly marked as an externs file, then move it out of the main // JS root and put it with the other externs. Node root = input.getAstRoot(this); externsRoot.addChildToBack(root); scriptNodeByFilename.put(input.getSourceFile().getName(), root); JSChunk module = input.getChunk(); if (module != null) { module.remove(input); } externs.add(input); return true; } return false; } /** * Marks inputs with the @externs annotation as an Extern source file type. This is so that * externs marking can be done before dependency management, and externs hoisting done after * dependency management. */ private void markExterns(ImmutableList originalInputs) { for (CompilerInput input : originalInputs) { if (input.getHasExternsAnnotation()) { input.setIsExtern(); } } } /** Hoists inputs with the @nocompile annotation out of the inputs. */ void hoistNoCompileFiles() { boolean staleInputs = false; maybeDoThreadedParsing(); // Iterate a copy because hoisting modifies what we're iterating over. for (CompilerInput input : ImmutableList.copyOf(moduleGraph.getAllInputs())) { if (input.getHasNoCompileAnnotation()) { input.getChunk().remove(input); staleInputs = true; } } if (staleInputs) { repartitionInputs(); } } private void maybeDoThreadedParsing() { if (options.numParallelThreads > 1) { new PrebuildDependencyInfo(options.numParallelThreads).prebuild(moduleGraph.getAllInputs()); } } private void repartitionInputs() { fillEmptyModules(getModules()); rebuildInputsFromModules(); } /** * Transforms JSON files to a module export that closure compiler can process and keeps track of * any "main" entries in package.json files. */ Map processJsonInputs(Iterable inputsToProcess) { RewriteJsonToModule rewriteJson = new RewriteJsonToModule(this); for (CompilerInput input : inputsToProcess) { if (!input.getSourceFile().getName().endsWith(".json")) { continue; } input.setCompiler(this); try { // JSON objects need wrapped in parens to parse properly input.getSourceFile().setCodeDeprecated("(" + input.getSourceFile().getCode() + ")"); } catch (IOException e) { continue; } Node root = checkNotNull(input.getAstRoot(this)); input.setJsModuleType(ModuleType.JSON); rewriteJson.process(null, root); } return rewriteJson.getPackageJsonMainEntries(); } private List parsePotentialModules(Iterable inputsToProcess) { List filteredInputs = new ArrayList<>(); for (CompilerInput input : inputsToProcess) { // Only process files that are detected as ES6 modules if (!options.getDependencyOptions().shouldPrune() || !JsFileRegexParser.isSupported() || "es6".equals(input.getLoadFlags().get("module"))) { filteredInputs.add(input); } } if (options.numParallelThreads > 1) { new PrebuildAst(this, options.numParallelThreads).prebuild(filteredInputs); } for (CompilerInput input : filteredInputs) { input.setCompiler(this); // Call getRequires to force regex-based dependency parsing to happen. input.getRequires(); input.setJsModuleType(ModuleType.ES6); } return filteredInputs; } /** Transforms AMD to CJS modules */ void processAMDModules(Iterable inputs) { for (CompilerInput input : inputs) { input.setCompiler(this); Node root = checkNotNull(input.getAstRoot(this)); new TransformAMDToCJSModule(this).process(null, root); } } /** Allow subclasses to override the default CompileOptions object. */ protected CompilerOptions newCompilerOptions() { return new CompilerOptions(); } void initCompilerOptionsIfTesting() { if (options == null) { // initialization for tests that don't initialize the compiler // by the normal mechanisms. initOptions(newCompilerOptions()); // allow tests to use unsupported features. options.setLanguage(CompilerOptions.LanguageMode.UNSUPPORTED); } } @Override public Node parseSyntheticCode(String fileName, String js) { initCompilerOptionsIfTesting(); SourceFile source = SourceFile.fromCode(SYNTHETIC_FILE_NAME_PREFIX + fileName + "] ", js); addFilesToSourceMap(ImmutableList.of(source)); return parseCodeHelper(source); } @Override @VisibleForTesting Node parseTestCode(String js) { initCompilerOptionsIfTesting(); initBasedOnOptions(); // NOTE: The file name used here is important, because at least Es6RewriteClassTest expects // it to match the default name used by the CompilerTestCase class for the source file // created when its `test("source string", "expected string")` method is called. // Also, some test cases rely on generated variable names that are based partially on the file // name or error messages that include the file name. return parseCodeHelper(SourceFile.fromCode("testcode", js)); } @Override @VisibleForTesting Node parseTestCode(ImmutableList code) { initCompilerOptionsIfTesting(); initBasedOnOptions(); return parseCodeHelper( Streams.mapWithIndex( code.stream(), (value, index) -> SourceFile.fromCode("testcode" + index, value)) .collect(Collectors.toList())); } private Node parseCodeHelper(SourceFile src) { CompilerInput input = new CompilerInput(src); putCompilerInput(input); Node root = input.getAstRoot(this); scriptNodeByFilename.put(input.getSourceFile().getName(), root); return checkNotNull(root); } private Node parseCodeHelper(List srcs) { Node root = IR.root(); for (SourceFile src : srcs) { root.addChildToBack(parseCodeHelper(src)); } return root; } @Override ErrorReporter getDefaultErrorReporter() { return oldErrorReporter; } // ------------------------------------------------------------------------ // Convert back to source code // ------------------------------------------------------------------------ /** Converts the main parse tree back to JS code. */ @Override public String toSource() { return runInCompilerThread( () -> { Tracer tracer = newTracer("toSource"); try { CodeBuilder cb = new CodeBuilder(); if (jsRoot != null) { int i = 0; if (options.shouldPrintExterns()) { for (Node scriptNode = externsRoot.getFirstChild(); scriptNode != null; scriptNode = scriptNode.getNext()) { toSource(cb, i++, scriptNode); } } for (Node scriptNode = jsRoot.getFirstChild(); scriptNode != null; scriptNode = scriptNode.getNext()) { toSource(cb, i++, scriptNode); } } return cb.toString(); } finally { stopTracer(tracer, "toSource"); } }); } /** Converts the parse tree for a module back to JS code. */ public String toSource(final JSChunk module) { return runInCompilerThread( () -> { ImmutableList inputs = module.getInputs(); int numInputs = inputs.size(); if (numInputs == 0) { return ""; } CodeBuilder cb = new CodeBuilder(); for (int i = 0; i < numInputs; i++) { Node scriptNode = inputs.get(i).getAstRoot(Compiler.this); if (scriptNode == null) { throw new IllegalArgumentException("Bad module: " + module.getName()); } toSource(cb, i, scriptNode); } return cb.toString(); }); } /** * Writes out JS code from a root node. If printing input delimiters, this method will attach a * comment to the start of the text indicating which input the output derived from. If there were * any preserve annotations within the root's source, they will also be printed in a block comment * at the beginning of the output. */ public void toSource(final CodeBuilder cb, final int inputSeqNum, final Node root) { runInCompilerThread( () -> { if (options.printInputDelimiter) { if ((cb.getLength() > 0) && !cb.endsWith("\n")) { cb.append("\n"); // Make sure that the label starts on a new line } checkState(root.isScript()); String delimiter = options.inputDelimiter; String inputName = root.getInputId().getIdName(); String sourceName = root.getSourceFileName(); checkState(sourceName != null); checkState(!sourceName.isEmpty()); delimiter = delimiter .replace("%name%", Matcher.quoteReplacement(inputName)) .replace("%num%", String.valueOf(inputSeqNum)) .replace("%n%", "\n"); cb.append(delimiter).append("\n"); } if (root.getJSDocInfo() != null) { String license = root.getJSDocInfo().getLicense(); if (license != null && cb.addLicense(license)) { cb.append("/*\n").append(license).append("*/\n"); } } // If there is a valid source map, then indicate to it that the current // root node's mappings are offset by the given string builder buffer. if (options.shouldGatherSourceMapInfo()) { sourceMap.setStartingPosition(cb.getLineIndex(), cb.getColumnIndex()); } // if LanguageMode is strict, only print 'use strict' // for the first input file String code = toSource(root, sourceMap, inputSeqNum == 0); if (!code.isEmpty()) { cb.append(code); // In order to avoid parse ambiguity when files are concatenated // together, all files should end in a semi-colon. Do a quick // heuristic check if there's an obvious semi-colon already there. int length = code.length(); char lastChar = code.charAt(length - 1); char secondLastChar = length >= 2 ? code.charAt(length - 2) : '\0'; boolean hasSemiColon = lastChar == ';' || (lastChar == '\n' && secondLastChar == ';'); if (!hasSemiColon) { cb.append(";"); } } return null; }); } /** Generates JavaScript source code for an AST, doesn't generate source map info. */ @Override public String toSource(Node n) { initCompilerOptionsIfTesting(); return toSource(n, null, true); } /** Generates JavaScript source code for an AST. */ private String toSource(Node n, @Nullable SourceMap sourceMap, boolean firstOutput) { CodePrinter.Builder builder = new CodePrinter.Builder(n); builder.setCompilerOptions(options); builder.setSourceMap(sourceMap); builder.setTagAsTypeSummary(options.shouldGenerateTypedExterns()); builder.setTagAsStrict(firstOutput && options.shouldEmitUseStrict()); return builder.build(); } /** Converts the parse tree for each input back to JS code. */ public String[] toSourceArray() { return runInCompilerThread( () -> { Tracer tracer = newTracer("toSourceArray"); try { int numInputs = moduleGraph.getInputCount(); String[] sources = new String[numInputs]; CodeBuilder cb = new CodeBuilder(); int i = 0; for (CompilerInput input : moduleGraph.getAllInputs()) { Node scriptNode = input.getAstRoot(Compiler.this); cb.reset(); toSource(cb, i, scriptNode); sources[i] = cb.toString(); i++; } return sources; } finally { stopTracer(tracer, "toSourceArray"); } }); } /** Converts the parse tree for each input in a module back to JS code. */ public String[] toSourceArray(final JSChunk module) { return runInCompilerThread( () -> { ImmutableList inputs = module.getInputs(); int numInputs = inputs.size(); if (numInputs == 0) { return new String[0]; } String[] sources = new String[numInputs]; CodeBuilder cb = new CodeBuilder(); for (int i = 0; i < numInputs; i++) { Node scriptNode = inputs.get(i).getAstRoot(Compiler.this); if (scriptNode == null) { throw new IllegalArgumentException("Bad module input: " + inputs.get(i).getName()); } cb.reset(); toSource(cb, i, scriptNode); sources[i] = cb.toString(); } return sources; }); } /** * Stores a buffer of text to which more can be appended. This is just like a StringBuilder except * that we also track the number of lines. */ public static class CodeBuilder { private final StringBuilder sb = new StringBuilder(); private int lineCount = 0; private int colCount = 0; private final Set uniqueLicenses = new HashSet<>(); /** Removes all text, but leaves the line count unchanged. */ void reset() { sb.setLength(0); } /** Appends the given string to the text buffer. */ @CanIgnoreReturnValue CodeBuilder append(String str) { sb.append(str); // Adjust the line and column information for the new text. int index = -1; int lastIndex = index; while ((index = str.indexOf('\n', index + 1)) >= 0) { ++lineCount; lastIndex = index; } if (lastIndex == -1) { // No new lines, append the new characters added. colCount += str.length(); } else { colCount = str.length() - (lastIndex + 1); } return this; } /** Returns all text in the text buffer. */ @Override public String toString() { return sb.toString(); } /** Returns the length of the text buffer. */ public int getLength() { return sb.length(); } /** Returns the (zero-based) index of the last line in the text buffer. */ int getLineIndex() { return lineCount; } /** Returns the (zero-based) index of the last column in the text buffer. */ int getColumnIndex() { return colCount; } /** Determines whether the text ends with the given suffix. */ boolean endsWith(String suffix) { return (sb.length() > suffix.length()) && suffix.equals(sb.substring(sb.length() - suffix.length())); } /** Adds a license and returns whether it is unique (has yet to be encountered). */ boolean addLicense(String license) { return uniqueLicenses.add(license); } } // ------------------------------------------------------------------------ // Optimizations // ------------------------------------------------------------------------ void performTranspilationAndOptimizations() { checkState(options.shouldOptimize()); // getOptimizations() also includes transpilation passes ImmutableList optimizations = getPassConfig().getOptimizations().build(); if (optimizations.isEmpty()) { return; } phaseOptimizer = createPhaseOptimizer(); phaseOptimizer.consume(optimizations); phaseOptimizer.process(externsRoot, jsRoot); phaseOptimizer = null; } void performFinalizations() { ImmutableList finalizations = getPassConfig().getFinalizations().build(); if (finalizations.isEmpty()) { return; } phaseOptimizer = createPhaseOptimizer(); phaseOptimizer.consume(finalizations); phaseOptimizer.process(externsRoot, jsRoot); phaseOptimizer = null; } @Override void setCssRenamingMap(CssRenamingMap map) { options.cssRenamingMap = map; } @Override CssRenamingMap getCssRenamingMap() { return options.cssRenamingMap; } /** Control Flow Analysis. */ ControlFlowGraph computeCFG() { logger.fine("Computing Control Flow Graph"); Tracer tracer = newTracer("computeCFG"); ControlFlowGraph cfg = ControlFlowAnalysis.builder() .setCompiler(this) .setCfgRoot(jsRoot) .setTraverseFunctions(true) .computeCfg(); stopTracer(tracer, "computeCFG"); return cfg; } private static final String SYNTHETIC_FILE_NAME_PREFIX = " [synthetic:"; private static final InputId SYNTHETIC_CODE_INPUT_ID = new InputId(SYNTHETIC_FILE_NAME_PREFIX + "input] "); /** * Non-static because this file represents different content for every Compiler (and TSAN * complains if one instance is shared by all threads). */ @VisibleForTesting final SourceFile SYNTHETIC_EXTERNS_FILE = SourceFile.fromCode(SYNTHETIC_FILE_NAME_PREFIX + "externs] ", ""); private @Nullable CompilerInput syntheticExternsInput; // matches SYNTHETIC_EXTERNS_FILE protected final RecentChange recentChange = new RecentChange(); private final List codeChangeHandlers = new ArrayList<>(); private final Map, IndexProvider> indexProvidersByType = new LinkedHashMap<>(); @Override void addChangeHandler(CodeChangeHandler handler) { codeChangeHandlers.add(handler); } @Override void removeChangeHandler(CodeChangeHandler handler) { codeChangeHandlers.remove(handler); } @Override void addIndexProvider(IndexProvider indexProvider) { Class type = indexProvider.getType(); if (indexProvidersByType.put(type, indexProvider) != null) { throw new IllegalStateException( "A provider is already registered for index of type " + type.getSimpleName()); } } @SuppressWarnings("unchecked") @Override T getIndex(Class key) { IndexProvider indexProvider = (IndexProvider) indexProvidersByType.get(key); if (indexProvider == null) { return null; } return indexProvider.get(); } protected Node getExternsRoot() { return externsRoot; } @Override protected Node getJsRoot() { return jsRoot; } /** * Some tests don't want to call the compiler "wholesale," they may not want to call check and/or * optimize. With this method, tests can execute custom optimization loops. */ @VisibleForTesting void setPhaseOptimizer(PhaseOptimizer po) { this.phaseOptimizer = po; } @Override public int getChangeStamp() { return changeStamp; } @Override List getChangedScopeNodesForPass(String passName) { List changedScopeNodes = changeTimeline.getSince(passName); changeTimeline.mark(passName); return changedScopeNodes; } @Override List getDeletedScopeNodesForPass(String passName) { List deletedScopeNodes = deleteTimeline.getSince(passName); deleteTimeline.mark(passName); return deletedScopeNodes; } @Override public void incrementChangeStamp() { changeStamp++; } private Node getChangeScopeForNode(Node n) { /** * Compiler change reporting usually occurs after the AST change has already occurred. In the * case of node removals those nodes are already removed from the tree and so have no parent * chain to walk. In these situations changes are reported instead against what (used to be) * their parent. If that parent is itself a script node then it's important to be able to * recognize it as the enclosing scope without first stepping to its parent as well. */ if (n.isScript()) { return n; } Node enclosingScopeNode = NodeUtil.getEnclosingChangeScopeRoot(n.getParent()); if (enclosingScopeNode == null) { throw new IllegalStateException( "An enclosing scope is required for change reports but node " + n + " doesn't have one."); } return enclosingScopeNode; } private void recordChange(Node n) { if (n.isDeleted()) { // Some complicated passes (like SmartNameRemoval) might both change and delete a scope in // the same pass, and they might even perform the change after the deletion because of // internal queueing. Just ignore the spurious attempt to mark changed after already marking // deleted. There's no danger of deleted nodes persisting in the AST since this is enforced // separately in ChangeVerifier. return; } n.setChangeTime(changeStamp); // Every code change happens at a different time changeStamp++; changeTimeline.add(n); } @Override boolean hasScopeChanged(Node n) { if (phaseOptimizer == null) { return true; } return phaseOptimizer.hasScopeChanged(n); } @Override public void reportChangeToChangeScope(Node changeScopeRoot) { checkState(changeScopeRoot.isScript() || changeScopeRoot.isFunction()); recordChange(changeScopeRoot); notifyChangeHandlers(); } @Override public void reportFunctionDeleted(Node n) { checkState(n.isFunction()); n.setDeleted(true); changeTimeline.remove(n); deleteTimeline.add(n); } @Override public void reportChangeToEnclosingScope(Node n) { recordChange(getChangeScopeForNode(n)); notifyChangeHandlers(); } private void notifyChangeHandlers() { for (CodeChangeHandler handler : codeChangeHandlers) { handler.reportChange(); } } @Override public CodingConvention getCodingConvention() { CodingConvention convention = options.getCodingConvention(); convention = convention != null ? convention : defaultCodingConvention; return convention; } private LanguageMode getParserConfigLanguageMode(CompilerOptions.LanguageMode languageMode) { switch (languageMode) { case ECMASCRIPT3: return LanguageMode.ECMASCRIPT3; case ECMASCRIPT5: case ECMASCRIPT5_STRICT: return LanguageMode.ECMASCRIPT5; case ECMASCRIPT_2015: return LanguageMode.ECMASCRIPT_2015; case ECMASCRIPT_2016: return LanguageMode.ECMASCRIPT_2016; case ECMASCRIPT_2017: return LanguageMode.ECMASCRIPT_2017; case ECMASCRIPT_2018: return LanguageMode.ECMASCRIPT_2018; case ECMASCRIPT_2019: return LanguageMode.ECMASCRIPT_2019; case ECMASCRIPT_2020: return LanguageMode.ECMASCRIPT_2020; case ECMASCRIPT_2021: return LanguageMode.ECMASCRIPT_2021; case ECMASCRIPT_NEXT: return LanguageMode.ES_NEXT; case UNSTABLE: return LanguageMode.UNSTABLE; case UNSUPPORTED: return LanguageMode.UNSUPPORTED; default: throw new IllegalStateException("Unexpected language mode: " + options.getLanguageIn()); } } @Override Config getParserConfig(ConfigContext context) { if (parserConfig == null || externsParserConfig == null) { synchronized (this) { if (parserConfig == null) { LanguageMode configLanguageMode = getParserConfigLanguageMode(options.getLanguageIn()); StrictMode strictMode = options.expectStrictModeInput() ? StrictMode.STRICT : StrictMode.SLOPPY; parserConfig = createConfig(configLanguageMode, strictMode); // Externs must always be parsed with at least ES5 language mode. externsParserConfig = configLanguageMode.equals(LanguageMode.ECMASCRIPT3) ? createConfig(LanguageMode.ECMASCRIPT5, strictMode) : parserConfig; } } } if (context == ConfigContext.EXTERNS) { return externsParserConfig; } return parserConfig; } protected Config createConfig(LanguageMode mode, StrictMode strictMode) { return ParserRunner.createConfig( mode, options.isParseJsDocDocumentation(), options.canContinueAfterErrors() ? RunMode.KEEP_GOING : RunMode.STOP_AFTER_ERROR, options.extraAnnotationNames, options.parseInlineSourceMaps, strictMode); } // ------------------------------------------------------------------------ // Error reporting // ------------------------------------------------------------------------ /** * The warning classes that are available from the command-line, and are suppressible by the * {@code @suppress} annotation. */ public DiagnosticGroups getDiagnosticGroups() { return new DiagnosticGroups(); } @Override public void report(JSError error) { CheckLevel level = error.getDefaultLevel(); if (warningsGuard != null) { CheckLevel newLevel = warningsGuard.level(error); if (newLevel != null) { level = newLevel; } } if (level.isOn()) { initCompilerOptionsIfTesting(); if (getOptions().errorHandler != null) { getOptions().errorHandler.report(level, error); } errorManager.report(level, error); } } @Override public void report(CheckLevel ignoredLevel, JSError error) { report(error); } @Override public CheckLevel getErrorLevel(JSError error) { checkNotNull(options); return warningsGuard.level(error); } /** Report an internal error. */ @Override void throwInternalError(String message, Throwable cause) { throw new RuntimeException( "INTERNAL COMPILER ERROR.\nPlease report this problem.\n\n" + message, cause); } /** Gets the number of errors. */ public int getErrorCount() { return errorManager.getErrorCount(); } /** Gets the number of warnings. */ public int getWarningCount() { return errorManager.getWarningCount(); } @Override boolean hasHaltingErrors() { return !getOptions().canContinueAfterErrors() && errorManager.hasHaltingErrors(); } /** * Consults the {@link ErrorManager} to see if we've encountered errors that should halt * compilation. * *

If {@link CompilerOptions#canContinueAfterErrors} is {@code true}, this function always * returns {@code false} without consulting the error manager. The error manager will continue to * be told about new errors and warnings, but the compiler will complete compilation of all * inputs. * *

*/ public boolean hasErrors() { return hasHaltingErrors(); } @Override @Nullable SourceFile getSourceFileByName(String sourceName) { // Here we assume that the source name is the input name, this // is true of JavaScript parsed from source. if (sourceName != null) { CompilerInput input = inputsById.get(new InputId(sourceName)); if (input != null) { return input.getSourceFile(); } // Alternatively, the sourceName might have been reverse-mapped by // an input source-map, so let's look in our sourcemap original sources. return sourceMapOriginalSources.get(sourceName); } return null; } public @Nullable CharSequence getSourceFileContentByName(String sourceName) { SourceFile file = getSourceFileByName(sourceName); checkNotNull(file); try { return file.getCode(); } catch (IOException e) { return null; } } @Override public void addInputSourceMap(String sourceFileName, SourceMapInput inputSourceMap) { inputSourceMaps.put(sourceFileName, inputSourceMap); if (options.sourceMapIncludeSourcesContent && sourceMap != null) { addSourceMapSourceFiles(inputSourceMap); } } /** * Adds file name to content mappings for all sources found in a source map. This is used to * populate sourcesContent array in the output source map even for sources embedded in the input * source map. */ private synchronized void addSourceMapSourceFiles(SourceMapInput inputSourceMap) { // synchronized annotation guards concurrent access to sourceMap during parsing. SourceMapConsumerV3 consumer = inputSourceMap.getSourceMap(errorManager); if (consumer == null) { return; } Collection sourcesContent = consumer.getOriginalSourcesContent(); if (sourcesContent == null) { return; } Iterator content = sourcesContent.iterator(); Iterator sources = consumer.getOriginalSources().iterator(); while (sources.hasNext() && content.hasNext()) { String code = content.next(); SourceFile source = SourceMapResolver.getRelativePath(inputSourceMap.getOriginalPath(), sources.next()); if (source != null) { sourceMap.addSourceFile(source.getName(), code); } } if (sources.hasNext() || content.hasNext()) { throw new RuntimeException( "Source map's \"sources\" and \"sourcesContent\" lengths do not match."); } } @Override public @Nullable OriginalMapping getSourceMapping( String sourceName, int lineNumber, int columnNumber) { if (sourceName == null) { return null; } SourceMapInput sourceMap = inputSourceMaps.get(sourceName); if (sourceMap == null) { return null; } // JSCompiler uses 1-indexing for lineNumber and 0-indexing for columnNumber. // Sourcemaps use 1-indexing for both. SourceMapConsumerV3 consumer = sourceMap.getSourceMap(errorManager); if (consumer == null) { return null; } OriginalMapping result = consumer.getMappingForLine(lineNumber, columnNumber + 1); if (result == null) { return null; } // First check to see if the original file was loaded from an input source map. String sourceMapOriginalPath = sourceMap.getOriginalPath(); String resultOriginalPath = result.getOriginalFile(); final String relativePath; // Resolving the paths to a source file is expensive, so check the cache first. if (sourceMapOriginalPath.equals(resolvedSourceMap.originalPath) && resultOriginalPath.equals(resolvedSourceMap.sourceMapPath)) { relativePath = resolvedSourceMap.relativePath; } else { relativePath = resolveSibling(sourceMapOriginalPath, resultOriginalPath); SourceFile source = getSourceFileByName(relativePath); if (source == null && !isNullOrEmpty(resultOriginalPath)) { source = SourceMapResolver.getRelativePath( sourceMap.getOriginalPath(), result.getOriginalFile()); if (source != null) { sourceMapOriginalSources.putIfAbsent(relativePath, source); } } // Cache this resolved source for the next caller. resolvedSourceMap.originalPath = sourceMapOriginalPath; resolvedSourceMap.sourceMapPath = resultOriginalPath; resolvedSourceMap.relativePath = relativePath; } return result.toBuilder() .setOriginalFile(relativePath) .setColumnPosition(result.getColumnPosition() - 1) .build(); } @Override public @Nullable String getSourceLine(String sourceName, int lineNumber) { if (lineNumber < 1) { return null; } SourceFile input = getSourceFileByName(sourceName); if (input != null) { return input.getLine(lineNumber); } return null; } @Override public @Nullable Region getSourceLines(String sourceName, int lineNumber, int length) { if (lineNumber < 1) { return null; } SourceFile input = getSourceFileByName(sourceName); if (input != null) { return input.getLines(lineNumber, length); } return null; } @Override public @Nullable Region getSourceRegion(String sourceName, int lineNumber) { if (lineNumber < 1) { return null; } SourceFile input = getSourceFileByName(sourceName); if (input != null) { return input.getRegion(lineNumber); } return null; } // ------------------------------------------------------------------------ // Package-private and Protected helpers // ------------------------------------------------------------------------ @Override protected Node getNodeForCodeInsertion(@Nullable JSChunk module) { if (this.inputsById.containsKey(SYNTHETIC_CODE_INPUT_ID)) { return this.inputsById.get(SYNTHETIC_CODE_INPUT_ID).getAstRoot(this); } if (module == null) { if (moduleGraph == null || Iterables.isEmpty(moduleGraph.getAllInputs())) { throw new IllegalStateException("No inputs"); } CompilerInput firstInput = Iterables.getFirst(moduleGraph.getAllInputs(), null); // TODO(lharker): add `checkNotModule(`. return firstInput.getAstRoot(this); } ImmutableList moduleInputs = module.getInputs(); if (!moduleInputs.isEmpty()) { return checkNotModule( moduleInputs.get(0).getAstRoot(this), "Cannot insert code into a module"); } throw new IllegalStateException("Root module has no inputs"); } public SourceMap getSourceMap() { return sourceMap; } /** Ids for cross-module method stubbing, so that each method has a unique id. */ private IdGenerator crossModuleIdGenerator = new IdGenerator(); /** * Keys are arguments passed to getCssName() found during compilation; values are the number of * times the key appeared as an argument to getCssName(). */ private @Nullable LinkedHashMap cssNames = null; /** The variable renaming map */ private @Nullable VariableMap variableMap = null; /** The property renaming map */ private @Nullable VariableMap propertyMap = null; /** String replacement map */ private @Nullable VariableMap stringMap = null; /** Mapping for Instrumentation parameter encoding */ private @Nullable VariableMap instrumentationMapping = null; /** Id generator map */ private @Nullable String idGeneratorMap = null; /** Names exported by goog.exportSymbol. */ private final LinkedHashSet exportedNames = new LinkedHashSet<>(); @Override public void setVariableMap(VariableMap variableMap) { this.variableMap = variableMap; } VariableMap getVariableMap() { return variableMap; } @Override public void setPropertyMap(VariableMap propertyMap) { this.propertyMap = propertyMap; } VariableMap getPropertyMap() { return this.propertyMap; } @Override public void setStringMap(VariableMap stringMap) { this.stringMap = stringMap; } @Override public void setCssNames(LinkedHashMap cssNames) { this.cssNames = cssNames; } @Override public void setIdGeneratorMap(String serializedIdMappings) { this.idGeneratorMap = serializedIdMappings; } @Override public IdGenerator getCrossModuleIdGenerator() { return crossModuleIdGenerator; } @Override public void setAnonymousFunctionNameMap(VariableMap functionMap) { // NOTE: remove this method } VariableMap getStringMap() { return this.stringMap; } @Override public void setInstrumentationMapping(VariableMap instrumentationMapping) { this.instrumentationMapping = instrumentationMapping; } public VariableMap getInstrumentationMapping() { return this.instrumentationMapping; } @Override public void addExportedNames(Set exportedNames) { this.exportedNames.addAll(exportedNames); } @Override public Set getExportedNames() { return exportedNames; } @Override public CompilerOptions getOptions() { return options; } /** Sets the logging level for the com.google.javascript.jscomp package. */ public static void setLoggingLevel(Level level) { logger.setLevel(level); } /** Gets the DOT graph of the AST generated at the end of compilation. */ public String getAstDotGraph() throws IOException { if (jsRoot != null) { ControlFlowGraph cfg = ControlFlowAnalysis.builder() .setCompiler(this) .setCfgRoot(jsRoot) .setTraverseFunctions(true) .computeCfg(); return DotFormatter.toDot(jsRoot, cfg); } else { return ""; } } @Override public ErrorManager getErrorManager() { if (options == null) { initOptions(new CompilerOptions()); } return errorManager; } @Override Iterable getInputsInOrder() { return moduleGraph.getAllInputs(); } @Override int getNumberOfInputs() { // In some testing cases inputs will be null, but obviously there must be at least one input. // The intended use of this method is to allow passes to estimate how much memory they will // need for data structures, so it's not necessary that the returned value be exactly right // in the corner cases where inputs ends up being null. return (moduleGraph != null) ? moduleGraph.getInputCount() : 1; } /** Returns an unmodifiable view of the compiler inputs indexed by id. */ public Map getInputsById() { return Collections.unmodifiableMap(inputsById); } /** Gets the externs in the order in which they are being processed. */ List getExternsInOrder() { return Collections.unmodifiableList(externs); } @VisibleForTesting @Nullable List getInputsForTesting() { return moduleGraph != null ? ImmutableList.copyOf(moduleGraph.getAllInputs()) : null; } @VisibleForTesting List getExternsForTesting() { return externs; } @Override boolean hasRegExpGlobalReferences() { return hasRegExpGlobalReferences; } @Override void setHasRegExpGlobalReferences(boolean references) { hasRegExpGlobalReferences = references; } /** Set whether J2CL passes should run */ @Override void setRunJ2clPasses(boolean runJ2clPasses) { this.runJ2clPasses = runJ2clPasses; } /** Whether J2CL passes should run */ @Override boolean runJ2clPasses() { return runJ2clPasses; } @Override CompilerInput getSynthesizedExternsInput() { if (syntheticExternsInput != null) { return syntheticExternsInput; } CompilerInput input = new CompilerInput(SYNTHETIC_EXTERNS_FILE, /* isExtern= */ true); Node root = checkNotNull(input.getAstRoot(this)); putCompilerInput(input); this.syntheticExternsInput = input; externsRoot.addChildToFront(root); externs.add(0, input); scriptNodeByFilename.put(input.getSourceFile().getName(), root); return input; } @Override InputId getSyntheticCodeInputId() { return SYNTHETIC_CODE_INPUT_ID; } @Override void initializeSyntheticCodeInput() { checkState( !this.inputsById.containsKey(SYNTHETIC_CODE_INPUT_ID), "Already initialized synthetic input"); JsAst ast = new JsAst(SourceFile.fromCode(SYNTHETIC_CODE_INPUT_ID.getIdName(), "")); if (inputsById.containsKey(ast.getInputId())) { throw new IllegalStateException("Conflicting synthetic id name"); } CompilerInput input = new CompilerInput(ast, false); jsRoot.addChildToFront(checkNotNull(ast.getAstRoot(this))); JSChunk firstModule = Iterables.getFirst(getModules(), null); if (firstModule.getName().equals(JSChunk.STRONG_CHUNK_NAME)) { firstModule.add(input); } input.setModule(firstModule); putCompilerInput(input); commentsPerFile.put(SYNTHETIC_CODE_INPUT_ID.getIdName(), ImmutableList.of()); reportChangeToChangeScope(ast.getAstRoot(this)); } @Override void removeSyntheticCodeInput() { this.removeSyntheticCodeInput(/* mergeContentIntoFirstInput= */ false); } @Override void mergeSyntheticCodeInput() { this.removeSyntheticCodeInput(/* mergeContentIntoFirstInput= */ true); } /** * Deletes the synthesized code input and optionally moves all its subtree contents into the first * non-synthetic input */ private void removeSyntheticCodeInput(boolean mergeContentIntoFirstInput) { checkState( this.inputsById.containsKey(SYNTHETIC_CODE_INPUT_ID), "Never initialized the synthetic input"); CompilerInput input = this.inputsById.get(SYNTHETIC_CODE_INPUT_ID); Node astRoot = input.getAstRoot(this); checkState(astRoot.isFirstChildOf(jsRoot)); checkState(SYNTHETIC_CODE_INPUT_ID.equals(input.getInputId())); if (mergeContentIntoFirstInput && astRoot.hasChildren()) { // If we've inserted anything into the synthetic AST, move it into the top of the next script. Node next = astRoot.getNext(); checkNotNull(next, "Must provide at least one source"); checkNotModule( next, "Cannot remove synthetic code input until modules are rewritten: %s", next); next.addChildrenToFront(astRoot.removeChildren()); reportChangeToChangeScope(next); } astRoot.detach(); // bookkeeping to mark scopes and nodes as deleted reportChangeToChangeScope(astRoot); astRoot.setDeleted(true); NodeUtil.markFunctionsDeleted(astRoot, this); input.getChunk().remove(input); inputsById.remove(input.getInputId()); } @Override String getLastPassName() { return lastPassName; } @Override void setExternProperties(ImmutableSet externProperties) { this.externProperties = externProperties; } @Override public ImmutableSet getExternProperties() { return externProperties; } @Override AccessorSummary getAccessorSummary() { return accessorSummary; } @Override public void setAccessorSummary(AccessorSummary summary) { this.accessorSummary = checkNotNull(summary); } @Override protected Node ensureLibraryInjected(String resourceName, boolean force) { boolean shouldInject = force || (!options.skipNonTranspilationPasses && !options.preventLibraryInjection); if (injectedLibraries.contains(resourceName) || !shouldInject) { return lastInjectedLibrary; } checkState(!getLifeCycleStage().isNormalized(), "runtime library injected after normalization"); // Load/parse the code. String path = String.join("", AbstractCompiler.RUNTIME_LIB_DIR, resourceName, ".js"); final Node ast; if (this.getLifeCycleStage().hasColorAndSimplifiedJSDoc()) { checkNotNull( this.runtimeLibraryTypedAsts, "Must call initRuntimeLibraryTypedAsts before calling ensureLibraryInjected during" + " optimizations"); Supplier typedAstSupplier = checkNotNull( this.runtimeLibraryTypedAsts.get(path), String.join( "", "Missing precompiled .typedast for '%s'. If this file is newly added, ", "you may need to regenerate runtime_libs.typedast.textproto", ""), path); ast = typedAstSupplier.get(); } else { checkState( !this.hasTypeCheckingRun(), "runtime library injected after type checking but before optimization colors"); String originalCode = ResourceLoader.loadTextResource(Compiler.class, "js/" + resourceName + ".js"); SourceFile source = SourceFile.fromCode(path, originalCode); addFilesToSourceMap(ImmutableList.of(source)); ast = parseCodeHelper(source); } // Look for string literals of the form 'require foo bar' // As we process each one, remove it from its parent. for (Node node = ast.getFirstChild(); node != null && node.isExprResult() && node.getFirstChild().isStringLit(); node = ast.getFirstChild()) { String directive = node.getFirstChild().getString(); List words = Splitter.on(' ').limit(2).splitToList(directive); switch (words.get(0)) { case "use": // 'use strict' is ignored (and deleted). break; case "require": // 'require lib'; pulls in the named library before this one. ensureLibraryInjected(words.get(1), force); break; default: throw new RuntimeException("Bad directive: " + directive); } node.detach(); } // Insert the code immediately after the last-inserted runtime library. Node lastChild = ast.getLastChild(); for (Node child = ast.getFirstChild(); child != null; child = child.getNext()) { NodeUtil.markNewScopesChanged(child, this); } Node firstChild = ast.removeChildren(); if (firstChild == null) { // Handle require-only libraries. return lastInjectedLibrary; } Node parent = getNodeForCodeInsertion(null); if (lastInjectedLibrary == null) { parent.addChildrenToFront(firstChild); } else { parent.addChildrenAfter(firstChild, lastInjectedLibrary); } lastInjectedLibrary = lastChild; injectedLibraries.add(resourceName); reportChangeToEnclosingScope(parent); return lastChild; } @Override void addComments(String filename, List comments) { if (!getOptions().preservesDetailedSourceInfo()) { throw new UnsupportedOperationException("addComments may only be called in IDE mode."); } commentsPerFile.put(filename, comments); } @Override public List getComments(String filename) { if (!getOptions().preservesDetailedSourceInfo()) { throw new UnsupportedOperationException("getComments may only be called in IDE mode."); } return commentsPerFile.get(filename); } @Override public ModuleLoader getModuleLoader() { return moduleLoader; } private synchronized void addFilesToSourceMap(Iterable files) { // synchronized annotation guards concurrent access to sourceMap during parsing. if (getOptions().sourceMapIncludeSourcesContent && getSourceMap() != null) { for (SourceFile file : files) { try { getSourceMap().addSourceFile(file.getName(), file.getCode()); } catch (IOException e) { throw new RuntimeException("Cannot read code of a source map's source file.", e); } } } } public void initWebpackMap(ImmutableMap inputPathByWebpackId) { this.inputPathByWebpackId = inputPathByWebpackId; } protected CompilerExecutor createCompilerExecutor() { return new CompilerExecutor(); } protected CompilerExecutor getCompilerExecutor() { return compilerExecutor; } /** * Serializable state of the compiler specific to multistage binary builds * *

Only contains state that does not make sense in 'multilevel' binary builds (where * library-level TypedASTs are the input). Such state belongs in the jscomp.TypedAst proto. */ protected static class CompilerState implements Serializable { private final FeatureSet featureSet; private final boolean typeCheckingHasRun; private final boolean hasRegExpGlobalReferences; private final LifeCycleStage lifeCycleStage; private final JSChunkGraph moduleGraph; private final int uniqueNameId; private final UniqueIdSupplier uniqueIdSupplier; private final LinkedHashSet exportedNames; private final LinkedHashMap cssNames; private final String idGeneratorMap; private final IdGenerator crossModuleIdGenerator; private final boolean runJ2clPasses; private final ImmutableMap inputSourceMaps; private final ImmutableList externs; private final ImmutableListMultimap moduleToInputList; private final LinkedHashSet injectedLibraries; private final int lastInjectedLibraryIndexInFirstScript; private final AccessorSummary accessorSummary; private final VariableMap stringMap; CompilerState(Compiler compiler) { this.featureSet = checkNotNull(compiler.featureSet); this.typeCheckingHasRun = compiler.typeCheckingHasRun; this.hasRegExpGlobalReferences = compiler.hasRegExpGlobalReferences; this.lifeCycleStage = compiler.getLifeCycleStage(); this.moduleGraph = compiler.moduleGraph; this.uniqueNameId = compiler.uniqueNameId; this.uniqueIdSupplier = compiler.uniqueIdSupplier; this.exportedNames = compiler.exportedNames; this.cssNames = compiler.cssNames; this.idGeneratorMap = compiler.idGeneratorMap; this.crossModuleIdGenerator = compiler.crossModuleIdGenerator; this.runJ2clPasses = compiler.runJ2clPasses; this.inputSourceMaps = ImmutableMap.copyOf(new TreeMap<>(compiler.inputSourceMaps)); this.externs = compiler.externs.stream().map(CompilerInput::getInputId).collect(toImmutableList()); this.moduleToInputList = mapJSModulesToInputIds(compiler.moduleGraph.getAllChunks()); this.injectedLibraries = compiler.injectedLibraries; this.lastInjectedLibraryIndexInFirstScript = compiler.lastInjectedLibrary != null ? compiler.jsRoot.getFirstChild().getIndexOfChild(compiler.lastInjectedLibrary) : -1; this.accessorSummary = compiler.accessorSummary; this.stringMap = compiler.getStringMap(); } } /** Restore the portions of the compiler state that don't require access to the serialized AST. */ protected void restoreFromState(CompilerState compilerState) { featureSet = compilerState.featureSet; scriptNodeByFilename.clear(); typeCheckingHasRun = compilerState.typeCheckingHasRun; injectedLibraries.clear(); injectedLibraries.addAll(compilerState.injectedLibraries); hasRegExpGlobalReferences = compilerState.hasRegExpGlobalReferences; // after restoreState, we're always guaranteed to have colors & simplified JSDoc on the AST. // whether the AST is also normalized depends on when saveState was called (after stage 1 or 2) LifeCycleStage stage = compilerState.lifeCycleStage == LifeCycleStage.RAW ? LifeCycleStage.COLORS_AND_SIMPLIFIED_JSDOC : compilerState.lifeCycleStage; setLifeCycleStage(stage); moduleGraph = compilerState.moduleGraph; uniqueNameId = compilerState.uniqueNameId; uniqueIdSupplier = compilerState.uniqueIdSupplier; exportedNames.clear(); exportedNames.addAll(compilerState.exportedNames); cssNames = compilerState.cssNames; variableMap = null; propertyMap = null; stringMap = compilerState.stringMap; idGeneratorMap = compilerState.idGeneratorMap; crossModuleIdGenerator = compilerState.crossModuleIdGenerator; runJ2clPasses = compilerState.runJ2clPasses; // We don't save changeStamp, because its value turned out to be non-deterministic // (Reasons for this are unknown.), and there's no benefit to saving it anyway. // We don't save the change stamps that are stored on the AST nodes, // so all the AST Nodes we read in effectively have a change stamp of 0, // and we can just start the compiler's counter over at 1. changeStamp = 1; accessorSummary = compilerState.accessorSummary; inputSourceMaps = new ConcurrentHashMap<>(compilerState.inputSourceMaps); if (options.shouldGatherSourceMapInfo() && options.sourceMapIncludeSourcesContent && options.applyInputSourceMaps) { // The inline source maps found by the parser in stage one will be in the restored // inputSourceMaps field. // addSourceMapSourceFiles() is invoked by the parser during stage one. // When restoring state, we need to do it again to get the source content from // the inline source maps into our current source map object. for (SourceMapInput inputSourceMap : inputSourceMaps.values()) { addSourceMapSourceFiles(inputSourceMap); } } } private static final ImmutableListMultimap mapJSModulesToInputIds( Iterable jsModules) { ImmutableListMultimap.Builder jsmoduleToInputId = ImmutableListMultimap.builder(); for (JSChunk jsModule : jsModules) { jsmoduleToInputId.putAll( jsModule, jsModule.getInputs().stream().map(CompilerInput::getInputId).collect(toImmutableList())); } return jsmoduleToInputId.build(); } @GwtIncompatible("ObjectOutputStream") public void saveState(OutputStream outputStream) throws IOException { // Do not close the outputstream, caller is responsible for closing it. runInCompilerThread( () -> { Tracer tracer = newTracer("serializeCompilerState"); new ObjectOutputStream(outputStream).writeObject(getCompilerState()); stopTracer(tracer, "serializeCompilerState"); tracer = newTracer("serializeTypedAst"); SerializeTypedAstPass.createFromOutputStream(this, outputStream) .process(externsRoot, jsRoot); stopTracer(tracer, "serializeTypedAst"); return null; }); } protected CompilerState getCompilerState() { return new CompilerState(this); } @GwtIncompatible("ClassNotFoundException") public void restoreState(InputStream inputStream) throws IOException, ClassNotFoundException { initWarningsGuard(options.getWarningsGuard()); maybeSetTracker(); runInCompilerThread( () -> { Tracer tracer = newTracer(PassNames.DESERIALIZE_COMPILER_STATE); logger.fine("Deserializing the CompilerState"); try { deserializeCompilerState(inputStream); return null; } finally { logger.fine("Finished deserializing CompilerState"); stopTracer(tracer, PassNames.DESERIALIZE_COMPILER_STATE); } }); if (tracker != null) { tracker.updateAfterDeserialize(jsRoot); } } @GwtIncompatible("ObjectInputStream") // this method must be called from within a "compiler thread" with a larger stack private void deserializeCompilerState(InputStream inputStream) throws IOException, ClassNotFoundException { // Do not close the input stream, caller is responsible for closing it. CompilerState compilerState = (CompilerState) new ObjectInputStream(inputStream).readObject(); checkNotNull( this.moduleGraph, "Did you forget to call .init or .initModules before restoreState?"); ImmutableMap.Builder externFilesBuilder = ImmutableMap.builder(); ImmutableMap.Builder codeFilesBuilder = ImmutableMap.builder(); ImmutableList.Builder allInputFiles = ImmutableList.builder(); for (CompilerInput input : this.moduleGraph.getAllInputs()) { allInputFiles.add(input.getSourceFile()); codeFilesBuilder.put(input.getInputId().getIdName(), input.getSourceFile()); } for (CompilerInput extern : this.externs) { allInputFiles.add(extern.getSourceFile()); externFilesBuilder.put(extern.getInputId().getIdName(), extern.getSourceFile()); } TypedAstDeserializer.DeserializedAst deserializedAst = TypedAstDeserializer.deserializeFullAst( this, SYNTHETIC_EXTERNS_FILE, allInputFiles.build(), inputStream, compilerState.typeCheckingHasRun); restoreFromState(compilerState); // Restore TypedAST and related fields externProperties = deserializedAst.getExternProperties(); externAndJsRoot = IR.root(IR.root(), IR.root()); externsRoot = externAndJsRoot.getFirstChild(); jsRoot = externAndJsRoot.getLastChild(); inputsById.clear(); externs.clear(); colorRegistry = deserializedAst.getColorRegistry().orNull(); // Tells CompilerInput::getRoot to deserialize an AST rather than re-parsing the file this.typedAstFilesystem = deserializedAst.getFilesystem(); // overwrite any existing CompilerInput instances. Reuse the SourceFiles created by // TypedAstDeserializer; otherwise lookups in this.typedAstFilesystem will fail. ImmutableMap externFiles = externFilesBuilder.buildOrThrow(); ImmutableMap codeFiles = codeFilesBuilder.buildOrThrow(); for (InputId extern : compilerState.externs) { if (extern.getIdName().equals(SYNTHETIC_EXTERNS_FILE.getName())) { this.getSynthesizedExternsInput(); continue; } SourceFile externFile = externFiles.get(extern.getIdName()); if (externFile == null) { // Extern files may passed in with regular source files, and later 'hoisted' to externs // because they are annotated `@externs`. externFile = checkNotNull(codeFiles.get(extern.getIdName()), "Missing %s", extern); } CompilerInput input = new CompilerInput(externFile, /* isExtern= */ true); Node script = input.getAstRoot(this); // accesses this.typedAstFilesystem externsRoot.addChildToBack(script); inputsById.put(script.getInputId(), input); scriptNodeByFilename.put(externFile.getName(), script); this.externs.add(input); } for (JSChunk deserializedModule : getModules()) { for (InputId inputId : compilerState.moduleToInputList.get(deserializedModule)) { SourceFile src = codeFiles.get(inputId.getIdName()); if (src == null) { // The auto-generated empty fill files used to facilitate CCCM for // empty chunks may not have gotten serialized, but all the others // should have. checkState(isFillFileName(inputId.getIdName()), "Missing %s", inputId); continue; } CompilerInput input = new CompilerInput(src); Node script = input.getAstRoot(this); // accesses this.typedAstFilesystem jsRoot.addChildToBack(script); scriptNodeByFilename.put(src.getName(), script); putCompilerInput(input); // overwrite the old input deserializedModule.add(input); } } this.typedAstFilesystem = null; // allow garbage collection lastInjectedLibrary = compilerState.lastInjectedLibraryIndexInFirstScript != -1 ? jsRoot .getFirstChild() .getChildAtIndex(compilerState.lastInjectedLibraryIndexInFirstScript) : null; } /** Returns the module type for the provided namespace. */ @Override @Nullable ModuleType getModuleTypeByName(String moduleName) { return moduleTypesByName.get(moduleName); } private ModuleMetadataMap moduleMetadataMap; @Override public ModuleMetadataMap getModuleMetadataMap() { return moduleMetadataMap; } @Override public void setModuleMetadataMap(ModuleMetadataMap moduleMetadataMap) { this.moduleMetadataMap = moduleMetadataMap; } private ModuleMap moduleMap; @Override public ModuleMap getModuleMap() { return moduleMap; } @Override public void setModuleMap(ModuleMap moduleMap) { this.moduleMap = moduleMap; } /** * Simplistic implementation of the java.nio.file.Path resolveSibling method that always uses "/" * regardless of platform. This is required for consistency of path handling when using input * source maps, which are expected to always use "/". * *

If `toPath` is already absolute, just return it. * *

Otherwise, strip the filename off of `fromPath` and logically replaces it with `toPath`. * Then normalize the path by resolving and removing any "." or ".." elements. * * @param fromPath - must be a file (not directory) * @param toPath - must be a file (not directory) * @return `toPath` adjusted so that it has the same parent directory as `fromPath` */ private static String resolveSibling(String fromPath, String toPath) { // If the destination is an absolute path, nothing to do. if (toPath.startsWith("/")) { return toPath; } List fromPathParts = new ArrayList<>(Arrays.asList(fromPath.split("/"))); List toPathParts = new ArrayList<>(Arrays.asList(toPath.split("/"))); if (!fromPathParts.isEmpty()) { fromPathParts.remove(fromPathParts.size() - 1); } while (!fromPathParts.isEmpty() && !toPathParts.isEmpty()) { if (toPathParts.get(0).equals(".")) { toPathParts.remove(0); } else if (toPathParts.get(0).equals("..")) { toPathParts.remove(0); fromPathParts.remove(fromPathParts.size() - 1); } else { break; } } fromPathParts.addAll(toPathParts); return String.join("/", fromPathParts); } public void resetAndIntitializeSourceMap() { if (sourceMap == null) { return; } sourceMap.reset(); if (options.sourceMapIncludeSourcesContent) { if (options.applyInputSourceMaps) { // Add any input source map content files to the source map as potential sources for (SourceMapInput inputSourceMap : inputSourceMaps.values()) { addSourceMapSourceFiles(inputSourceMap); } } // Add all the compilation sources to the source map as potential sources Iterable allModules = getModules(); if (allModules != null) { List sourceFiles = new ArrayList<>(); for (JSChunk module : allModules) { for (CompilerInput input : module.getInputs()) { sourceFiles.add(input.getSourceFile()); } } addFilesToSourceMap(sourceFiles); } } } private static Node checkNotModule(Node script, String msg, Object... args) { checkArgument(script.isScript(), script); if (!script.hasOneChild()) { return script; } checkState(!script.getFirstChild().isModuleBody(), msg, args); return script; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy