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 com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.debugging.sourcemap.proto.Mapping.OriginalMapping;
import com.google.javascript.jscomp.CompilerOptions.DevMode;
import com.google.javascript.jscomp.ReferenceCollectingCallback.ReferenceCollection;
import com.google.javascript.jscomp.TypeValidator.TypeMismatch;
import com.google.javascript.jscomp.deps.SortedDependencies.MissingProvideException;
import com.google.javascript.jscomp.parsing.Config;
import com.google.javascript.jscomp.parsing.ParserRunner;
import com.google.javascript.jscomp.parsing.parser.trees.Comment;
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.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.TypeIRegistry;
import com.google.javascript.rhino.jstype.JSTypeRegistry;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.FileSystems;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;

/**
 * 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. * */ public class Compiler extends AbstractCompiler { static final String SINGLETON_MODULE_NAME = "$singleton$"; 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_ENTRY_ERROR", "unknown module \"{0}\" specified in entry point spec"); // Used in PerformanceTracker static final String PARSING_PASS_NAME = "parseInputs"; static final String CROSS_MODULE_CODE_MOTION_NAME = "crossModuleCodeMotion"; static final String CROSS_MODULE_METHOD_MOTION_NAME = "crossModuleMethodMotion"; private static final String CONFIG_RESOURCE = "com.google.javascript.jscomp.parsing.ParserConfig"; CompilerOptions options = null; private PassConfig passes = null; // The externs inputs private List externs; // The JS source modules private List modules; // The graph of the JS source modules. Must be null if there are less than // 2 modules, because we use this as a signal for which passes to run. private JSModuleGraph moduleGraph; // The JS source inputs private List inputs; // error manager to which error management is delegated private ErrorManager errorManager; // Warnings guard for filtering warnings. private WarningsGuard warningsGuard; // Compile-time injected libraries. The node points to the last node of // the library, so code can be inserted after. private final Map injectedLibraries = new LinkedHashMap<>(); // Node of the final injected library. Future libraries will be injected // after this node. private Node lastInjectedLibrary; // Parse tree root nodes Node externsRoot; Node jsRoot; Node externAndJsRoot; // Used for debugging; to see the compiled code between passes private String lastJsSource = null; /** @see #getLanguageMode() */ private CompilerOptions.LanguageMode languageMode = CompilerOptions.LanguageMode.ECMASCRIPT3; private Map inputsById; // Function to load source files from disk or memory. private Function originalSourcesLoader = new Function() { @Override public SourceFile apply(String filename) { return SourceFile.fromFile(filename); } }; // Original sources referenced by the source maps. private ConcurrentHashMap sourceMapOriginalSources = new ConcurrentHashMap<>(); // Map from filenames to lists of all the comments in each file. private Map> commentsPerFile = new HashMap<>(); /** The source code map */ private SourceMap sourceMap; /** The externs created from the exports. */ private String externExports = null; /** * 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; /** The function information map */ private FunctionInformationMap functionInformationMap; /** Debugging information */ private final StringBuilder debugLog = new StringBuilder(); /** Detects Google-specific coding conventions. */ CodingConvention defaultCodingConvention = new ClosureCodingConvention(); private JSTypeRegistry typeRegistry; private Config parserConfig = null; private Config externsParserConfig = null; private ReverseAbstractInterpreter abstractInterpreter; private TypeValidator typeValidator; // The compiler can ask phaseOptimizer for things like which pass is currently // running, or which functions have been changed by optimizations private PhaseOptimizer phaseOptimizer = null; public PerformanceTracker tracker; // Used by optimize-returns, optimize-parameters and remove-unused-variables private SimpleDefinitionFinder defFinder = null; // For use by the new type inference private GlobalTypeInfo symbolTable; // This error reporter gets the messages from the current Rhino parser or TypeRegistry. private final ErrorReporter oldErrorReporter = RhinoErrorReporter.forOldRhino(this); /** Error strings used for reporting JSErrors */ public static final DiagnosticType OPTIMIZE_LOOP_ERROR = DiagnosticType.error( "JSC_OPTIMIZE_LOOP_ERROR", "Exceeded max number of optimization iterations: {0}"); public static final DiagnosticType MOTION_ITERATIONS_ERROR = DiagnosticType.error("JSC_OPTIMIZE_LOOP_ERROR", "Exceeded max number of code motion iterations: {0}"); private final CompilerExecutor compilerExecutor = new CompilerExecutor(); /** * Logger for the whole com.google.javascript.jscomp domain - * setting configuration for this logger affects all loggers * in other classes within the compiler. */ private static final Logger logger = Logger.getLogger("com.google.javascript.jscomp"); private final PrintStream outStream; private GlobalVarReferenceMap globalRefMap = null; private volatile double progress = 0.0; private String lastPassName; private Set externProperties = null; private static final Joiner pathJoiner = Joiner.on(File.separator); /** * 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(PrintStream stream) { addChangeHandler(recentChange); outStream = stream; } /** * 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) { Preconditions.checkNotNull( errorManager, "the error manager cannot be null"); this.errorManager = 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); } @VisibleForTesting void setOriginalSourcesLoader( Function originalSourcesLoader) { this.originalSourcesLoader = originalSourcesLoader; } /** * 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.languageMode = options.getLanguageIn(); if (errorManager == null) { if (outStream == null) { setErrorManager( new LoggerErrorManager(createMessageFormatter(), logger)); } else { PrintStreamErrorManager printer = new PrintStreamErrorManager(createMessageFormatter(), outStream); printer.setSummaryDetailLevel(options.summaryDetailLevel); setErrorManager(printer); } } reconcileOptionsWithGuards(); if (options.legacyCodeCompile) { options.disambiguateProperties = false; options.ambiguateProperties = false; options.useNonStrictWarningsGuard(); } initWarningsGuard(options.getWarningsGuard()); } void initWarningsGuard(WarningsGuard warningsGuard) { this.warningsGuard = new ComposeWarningsGuard( new SuppressDocWarningsGuard(getDiagnosticGroups().getRegisteredGroups()), warningsGuard); } /** * When the CompilerOptions and its WarningsGuard overlap, reconcile * any discrepencies. */ 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 enabled, then turn off the // parser type warnings. options.setWarningLevel( DiagnosticGroup.forType( RhinoErrorReporter.TYPE_PARSE_ERROR), CheckLevel.OFF); } // With NTI, we still need OTI to run because the later passes that use // types only understand OTI types at the moment. // But we do not want to see the warnings from OTI. if (options.getNewTypeInference()) { options.checkTypes = true; if (!options.reportOTIErrorsUnderNTI) { options.setWarningLevel( DiagnosticGroups.OLD_CHECK_TYPES, CheckLevel.OFF); options.setWarningLevel( DiagnosticGroups.OLD_REPORT_UNKNOWN_TYPES, CheckLevel.OFF); options.setWarningLevel( FunctionTypeBuilder.ALL_DIAGNOSTICS, CheckLevel.OFF); } options.setWarningLevel( DiagnosticGroup.forType(RhinoErrorReporter.TYPE_PARSE_ERROR), CheckLevel.WARNING); } if (options.checkGlobalThisLevel.isOn() && !options.disables(DiagnosticGroups.GLOBAL_THIS)) { options.setWarningLevel( DiagnosticGroups.GLOBAL_THIS, options.checkGlobalThisLevel); } if (options.getLanguageIn().isStrict()) { options.setWarningLevel( DiagnosticGroups.ES5_STRICT, CheckLevel.ERROR); } // 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); } } /** * Initializes the instance state needed for a compile job. */ public void init( List externs, List inputs, CompilerOptions options) { JSModule module = new JSModule(SINGLETON_MODULE_NAME); for (SourceFile input : inputs) { module.add(input); } List modules = new ArrayList<>(1); modules.add(module); initModules(externs, modules, options); } /** * 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); fillEmptyModules(modules); this.externs = makeCompilerInput(externs, true); // Generate the module graph, and report any errors in the module // specification as errors. this.modules = modules; if (modules.size() > 1) { try { this.moduleGraph = new JSModuleGraph(modules); } catch (JSModuleGraph.ModuleDependenceException e) { // problems with the module format. Report as an error. The // message gives all details. report(JSError.make(MODULE_DEPENDENCY_ERROR, e.getModule().getName(), e.getDependentModule().getName())); return; } } else { this.moduleGraph = null; } this.inputs = getAllInputsFromModules(modules); initBasedOnOptions(); initInputsByIdMap(); initAST(); } /** * Do any initialization that is dependent on the compiler options. */ private void initBasedOnOptions() { // Create the source map if necessary. if (options.sourceMapOutputPath != null) { sourceMap = options.sourceMapFormat.getInstance(); sourceMap.setPrefixMappings(options.sourceMapLocationMappings); } } private List makeCompilerInput( List files, boolean isExtern) { List inputs = new ArrayList<>(files.size()); for (T file : files) { inputs.add(new CompilerInput(file, isExtern)); } return inputs; } 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())); } } /** * Empty modules get an empty "fill" file, so that we can move code into * an empty module. */ static String createFillFileName(String moduleName) { return moduleName + "$fillFile"; } /** * 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 static void fillEmptyModules(List modules) { for (JSModule module : modules) { if (module.getInputs().isEmpty()) { module.add(SourceFile.fromCode( createFillFileName(module.getName()), "")); } } } /** * Rebuilds the internal list of inputs by iterating over all modules. * This is necessary if inputs have been added to or removed from a module * after the {@link #init(List, List, CompilerOptions)} call. */ public void rebuildInputsFromModules() { inputs = getAllInputsFromModules(modules); initInputsByIdMap(); } /** * Builds a single list of all module inputs. Verifies that it contains no * duplicates. */ private static List getAllInputsFromModules( List modules) { List inputs = new ArrayList<>(); Map inputMap = new HashMap<>(); for (JSModule module : modules) { for (CompilerInput input : module.getInputs()) { String inputName = input.getName(); // NOTE(nicksantos): If an input is in more than one module, // it will show up twice in the inputs list, and then we // will get an error down the line. inputs.add(input); inputMap.put(inputName, module); } } return inputs; } 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}"); /** * Returns the relative path, resolved relative to the base path, where the * base path is interpreted as a filename rather than a directory. E.g.: * getRelativeTo("../foo/bar.js", "baz/bam/qux.js") --> "baz/foo/bar.js" */ private static String getRelativeTo(String relative, String base) { return FileSystems.getDefault().getPath(base) .resolveSibling(relative) .normalize() .toString() .replace(File.separator, "/"); } /** * Creates a map to make looking up an input by name fast. Also checks for * duplicate inputs. */ void initInputsByIdMap() { inputsById = new HashMap<>(); for (CompilerInput input : externs) { InputId id = input.getInputId(); CompilerInput previous = putCompilerInput(id, input); if (previous != null) { report(JSError.make(DUPLICATE_EXTERN_INPUT, input.getName())); } } for (CompilerInput input : inputs) { InputId id = input.getInputId(); CompilerInput previous = putCompilerInput(id, input); if (previous != null) { report(JSError.make(DUPLICATE_INPUT, input.getName())); } } } /** * Sets up the skeleton of the AST (the externs and root). */ private void initAST() { jsRoot = IR.block(); jsRoot.setIsSyntheticBlock(true); externsRoot = IR.block(); externsRoot.setIsSyntheticBlock(true); externAndJsRoot = IR.block(externsRoot, jsRoot); externAndJsRoot.setIsSyntheticBlock(true); } public Result compile( SourceFile extern, SourceFile input, CompilerOptions options) { return compile(ImmutableList.of(extern), ImmutableList.of(input), options); } /** * Compiles a list of inputs. */ public Result compile( List externs, List inputs, CompilerOptions options) { // The compile method should only be called once. Preconditions.checkState(jsRoot == null); try { init(externs, inputs, options); if (hasErrors()) { return getResult(); } return compile(); } finally { Tracer t = newTracer("generateReport"); errorManager.generateReport(); stopTracer(t, "generateReport"); } } /** * Compiles a list of modules. */ public Result compileModules(List externs, List modules, CompilerOptions options) { // The compile method should only be called once. Preconditions.checkState(jsRoot == null); try { initModules(externs, modules, options); if (hasErrors()) { return getResult(); } return compile(); } finally { Tracer t = newTracer("generateReport"); errorManager.generateReport(); stopTracer(t, "generateReport"); } } private Result compile() { return runInCompilerThread(new Callable() { @Override public Result call() throws Exception { compileInternal(); return getResult(); } }); } /** * 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.tracer.isOn()); } private void compileInternal() { setProgress(0.0, null); CompilerOptionsPreprocessor.preprocess(options); parse(); // 15 percent of the work is assumed to be for parsing (based on some // minimal analysis on big JS projects, of course this depends on options) setProgress(0.15, "parse"); if (hasErrors()) { return; } if (!precheck()) { return; } if (options.skipNonTranspilationPasses) { // i.e. whitespace-only mode, which will not work with goog.module without: whitespaceOnlyPasses(); } if (!options.skipNonTranspilationPasses || options.lowerFromEs6()) { check(); if (hasErrors()) { return; } // IDE-mode is defined to stop here, before the heavy rewriting begins. if (!options.checksOnly && !options.ideMode) { optimize(); } } if (options.recordFunctionInformation) { recordFunctionInformation(); } if (options.devMode == DevMode.START_AND_END) { runSanityCheck(); } setProgress(1.0, "recordFunctionInformation"); if (tracker != null) { tracker.outputTracerReport(outStream == null ? System.out : outStream); } } public void parse() { parseInputs(); } PassConfig getPassConfig() { if (passes == null) { passes = createPassConfigInternal(); } return passes; } /** * Create the passes object. Clients should use setPassConfig instead of * overriding this. */ 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. Preconditions.checkNotNull(passes); Preconditions.checkState(this.passes == null, "setPassConfig was already called"); this.passes = passes; } /** * Carry out any special checks or procedures that need to be done before * proceeding with rest of the compilation process. * * @return true, to continue with compilation */ boolean precheck() { return true; } public void whitespaceOnlyPasses() { Tracer t = newTracer("runWhitespaceOnlyPasses"); try { for (PassFactory pf : getPassConfig().getWhitespaceOnlyPasses()) { pf.create(this).process(externsRoot, jsRoot); } } finally { stopTracer(t, "runWhitespaceOnlyPasses"); } } public void check() { runCustomPasses(CustomPassExecutionTime.BEFORE_CHECKS); // We are currently only interested in check-passes for progress reporting // as it is used for IDEs, that's why the maximum progress is set to 1.0. phaseOptimizer = new PhaseOptimizer(this, tracker, new PhaseOptimizer.ProgressRange(getProgress(), 1.0)); if (options.devMode == DevMode.EVERY_PASS) { phaseOptimizer.setSanityCheck(sanityCheck); } if (options.getCheckDeterminism()) { phaseOptimizer.setPrintAstHashcodes(true); } phaseOptimizer.consume(getPassConfig().getChecks()); phaseOptimizer.process(externsRoot, jsRoot); if (hasErrors()) { return; } if (options.getTweakProcessing().shouldStrip() || !options.stripTypes.isEmpty() || !options.stripNameSuffixes.isEmpty() || !options.stripTypePrefixes.isEmpty() || !options.stripNamePrefixes.isEmpty()) { stripCode(options.stripTypes, options.stripNameSuffixes, options.stripTypePrefixes, options.stripNamePrefixes); } runCustomPasses(CustomPassExecutionTime.BEFORE_OPTIMIZATIONS); phaseOptimizer = null; } private void externExports() { logger.fine("Creating extern file for exports"); startPass("externExports"); ExternExportsPass pass = new ExternExportsPass(this); process(pass); externExports = pass.getGeneratedExterns(); endPass(); } @Override void process(CompilerPass p) { p.process(externsRoot, jsRoot); } private final PassFactory sanityCheck = new PassFactory("sanityCheck", false) { @Override protected CompilerPass create(AbstractCompiler compiler) { return new SanityCheck(compiler); } }; private void maybeSanityCheck() { if (options.devMode == DevMode.EVERY_PASS) { runSanityCheck(); } } private void runSanityCheck() { sanityCheck.create(this).process(externsRoot, jsRoot); } /** * Strips code for smaller compiled code. This is useful for removing debug * statements to prevent leaking them publicly. */ void stripCode(Set stripTypes, Set stripNameSuffixes, Set stripTypePrefixes, Set stripNamePrefixes) { logger.fine("Strip code"); startPass("stripCode"); StripCode r = new StripCode(this, stripTypes, stripNameSuffixes, stripTypePrefixes, stripNamePrefixes); if (options.getTweakProcessing().shouldStrip()) { r.enableTweakStripping(); } process(r); endPass(); } /** * 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"); } } } private Tracer currentTracer = null; private String currentPassName = null; /** * Marks the beginning of a pass. */ void startPass(String passName) { Preconditions.checkState(currentTracer == null); currentPassName = passName; currentTracer = newTracer(passName); } /** * Marks the end of a pass. */ void endPass() { Preconditions.checkState(currentTracer != null, "Tracer should not be null at the end of a pass."); stopTracer(currentTracer, currentPassName); currentPassName = null; currentTracer = null; maybeSanityCheck(); } @Override final void beforePass(String passName) { // does nothing for now } @Override final void afterPass(String passName) { if (options.printSourceAfterEachPass) { String currentJsSource = toSource(); if (!currentJsSource.equals(this.lastJsSource)) { System.out.println(); System.out.println("// " + passName + " yields:"); System.out.println("// ************************************"); System.out.println(currentJsSource); lastJsSource = currentJsSource; } } } /** * Returns a new tracer for the given pass name. */ Tracer newTracer(String passName) { String comment = passName + (recentChange.hasCodeChanged() ? " on recently changed AST" : ""); if (options.tracer.isOn()) { tracker.recordPassStart(passName, true); } return new Tracer("Compiler", comment); } void stopTracer(Tracer t, String passName) { long result = t.stop(); if (options.tracer.isOn()) { tracker.recordPassStop(passName, result); } } /** * Returns the result of the compilation. */ public Result getResult() { PassConfig.State state = getPassConfig().getIntermediateState(); return new Result(getErrors(), getWarnings(), debugLog.toString(), state.variableMap, state.propertyMap, state.anonymousFunctionNameMap, state.stringMap, functionInformationMap, sourceMap, externExports, state.cssNames, state.idGeneratorMap); } /** * Returns the array of errors (never null). */ public JSError[] getErrors() { if (errorManager == null) { return new JSError[] {}; } return errorManager.getErrors(); } /** * Returns the array of warnings (never null). */ public JSError[] getWarnings() { if (errorManager == null) { return new JSError[] {}; } return errorManager.getWarnings(); } @Override public Node getRoot() { return externAndJsRoot; } @Override CompilerOptions.LanguageMode getLanguageMode() { return languageMode; } @Override void setLanguageMode(CompilerOptions.LanguageMode mode) { languageMode = mode; } /** * Creates a new id for making unique names. */ private int nextUniqueNameId() { return uniqueNameId++; } /** * Resets the unique name id counter */ @VisibleForTesting void resetUniqueNameId() { uniqueNameId = 0; } @Override Supplier getUniqueNameIdSupplier() { final Compiler self = this; return new Supplier() { @Override public String get() { return String.valueOf(self.nextUniqueNameId()); } }; } @Override boolean areNodesEqualForInlining(Node n1, Node n2) { if (options.ambiguateProperties || options.disambiguateProperties) { // 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 CompilerInput getInput(InputId id) { return inputsById.get(id); } /** * Removes an input file from AST. * @param id The id of the input to be removed. */ protected void removeExternInput(InputId id) { CompilerInput input = getInput(id); if (input == null) { return; } Preconditions.checkState(input.isExtern(), "Not an extern input: %s", input.getName()); inputsById.remove(id); externs.remove(input); Node root = input.getAstRoot(this); if (root != null) { root.detachFromParent(); } } // Where to put a new synthetic externs file. private static enum SyntheticExternsPosition { START, END } CompilerInput newExternInput(String name, SyntheticExternsPosition pos) { SourceAst ast = new SyntheticAst(name); if (inputsById.containsKey(ast.getInputId())) { throw new IllegalArgumentException("Conflicting externs name: " + name); } CompilerInput input = new CompilerInput(ast, true); putCompilerInput(input.getInputId(), input); if (pos == SyntheticExternsPosition.START) { externsRoot.addChildToFront(ast.getAstRoot(this)); externs.add(0, input); } else { externsRoot.addChildToBack(ast.getAstRoot(this)); externs.add(input); } return input; } CompilerInput putCompilerInput(InputId id, CompilerInput input) { if (inputsById == null) { inputsById = new HashMap<>(); } input.setCompiler(this); return inputsById.put(id, input); } /** * Replace a source input dynamically. Intended for incremental * re-compilation. * * If the new source input doesn't parse, then keep the old input * in the AST and return false. * * @return Whether the new AST was attached successfully. */ boolean replaceIncrementalSourceAst(JsAst ast) { CompilerInput oldInput = getInput(ast.getInputId()); Preconditions.checkNotNull(oldInput, "No input to replace: %s", ast.getInputId().getIdName()); Node newRoot = ast.getAstRoot(this); if (newRoot == null) { return false; } Node oldRoot = oldInput.getAstRoot(this); if (oldRoot != null) { oldRoot.getParent().replaceChild(oldRoot, newRoot); } else { getRoot().getLastChild().addChildToBack(newRoot); } CompilerInput newInput = new CompilerInput(ast); putCompilerInput(ast.getInputId(), newInput); JSModule module = oldInput.getModule(); if (module != null) { module.addAfter(newInput, oldInput); module.remove(oldInput); } // Verify the input id is set properly. Preconditions.checkState( newInput.getInputId().equals(oldInput.getInputId())); InputId inputIdOnAst = newInput.getAstRoot(this).getInputId(); Preconditions.checkState(newInput.getInputId().equals(inputIdOnAst)); inputs.remove(oldInput); return true; } /** * Add a new source input dynamically. Intended for incremental compilation. *

* If the new source input doesn't parse, it will not be added, and a false * will be returned. * * @param ast the JS Source to add. * @return true if the source was added successfully, false otherwise. * @throws IllegalStateException if an input for this ast already exists. */ boolean addNewSourceAst(JsAst ast) { CompilerInput oldInput = getInput(ast.getInputId()); if (oldInput != null) { throw new IllegalStateException( "Input already exists: " + ast.getInputId().getIdName()); } Node newRoot = ast.getAstRoot(this); if (newRoot == null) { return false; } getRoot().getLastChild().addChildToBack(newRoot); CompilerInput newInput = new CompilerInput(ast); // TODO(tylerg): handle this for multiple modules at some point. if (moduleGraph == null && !modules.isEmpty()) { // singleton module modules.get(0).add(newInput); } putCompilerInput(ast.getInputId(), newInput); return true; } @Override JSModuleGraph getModuleGraph() { return moduleGraph; } /** * Gets a module graph. This will always return a module graph, even * in the degenerate case when there's only one module. */ JSModuleGraph getDegenerateModuleGraph() { return moduleGraph == null ? new JSModuleGraph(modules) : moduleGraph; } @Override public TypeIRegistry getTypeIRegistry() { return getTypeRegistry(); } @Override public JSTypeRegistry getTypeRegistry() { if (typeRegistry == null) { typeRegistry = new JSTypeRegistry(oldErrorReporter); } return typeRegistry; } @Override void forwardDeclareType(String typeName) { // Always add it to the old type registry, since OTI runs after NTI to // provide types for the remaining passes. // TODO(dimvar): change this when we stop running OTI after NTI. getTypeRegistry().forwardDeclareType(typeName); if (this.options.getNewTypeInference()) { getSymbolTable().addUnknownTypeName(typeName); } } @Override // Only used by jsdev public MemoizedScopeCreator getTypedScopeCreator() { return getPassConfig().getTypedScopeCreator(); } @SuppressWarnings("unchecked") DefaultPassConfig ensureDefaultPassConfig() { PassConfig passes = getPassConfig().getBasePassConfig(); Preconditions.checkState(passes instanceof DefaultPassConfig, "PassConfigs must eventually delegate to the DefaultPassConfig"); return (DefaultPassConfig) passes; } public SymbolTable buildKnownSymbolTable() { SymbolTable symbolTable = new SymbolTable(getTypeRegistry()); MemoizedScopeCreator typedScopeCreator = getTypedScopeCreator(); if (typedScopeCreator != null) { symbolTable.addScopes(typedScopeCreator.getAllMemoizedScopes()); symbolTable.addSymbolsFrom(typedScopeCreator); } else { symbolTable.findScopes(this, externsRoot, jsRoot); } GlobalNamespace globalNamespace = ensureDefaultPassConfig().getGlobalNamespace(); if (globalNamespace != null) { symbolTable.addSymbolsFrom(globalNamespace); } ReferenceCollectingCallback refCollector = new ReferenceCollectingCallback( this, ReferenceCollectingCallback.DO_NOTHING_BEHAVIOR); NodeTraversal.traverse(this, getRoot(), refCollector); symbolTable.addSymbolsFrom(refCollector); PreprocessorSymbolTable preprocessorSymbolTable = ensureDefaultPassConfig().getPreprocessorSymbolTable(); if (preprocessorSymbolTable != null) { symbolTable.addSymbolsFrom(preprocessorSymbolTable); } symbolTable.fillNamespaceReferences(); symbolTable.fillPropertyScopes(); symbolTable.fillThisReferences(this, externsRoot, jsRoot); symbolTable.fillPropertySymbols(this, externsRoot, jsRoot); symbolTable.fillJSDocInfo(this, externsRoot, jsRoot); symbolTable.fillSymbolVisibility(this, externsRoot, jsRoot); return symbolTable; } @Override public TypedScope getTopScope() { return getPassConfig().getTopScope(); } @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 // Only used by passes in the old type checker. TypeValidator getTypeValidator() { if (typeValidator == null) { typeValidator = new TypeValidator(this); } return typeValidator; } @Override Iterable getTypeMismatches() { return getTypeValidator().getMismatches(); } @Override Iterable getImplicitInterfaceUses() { return getTypeValidator().getImplicitStructuralInterfaceUses(); } @Override GlobalTypeInfo getSymbolTable() { if (this.symbolTable == null) { this.symbolTable = new GlobalTypeInfo(this); } return this.symbolTable; } @Override void setSymbolTable(CompilerPass symbolTable) { Preconditions.checkArgument( symbolTable == null || symbolTable instanceof GlobalTypeInfo); this.symbolTable = (GlobalTypeInfo) symbolTable; } @Override SimpleDefinitionFinder getSimpleDefinitionFinder() { return this.defFinder; } @Override void setSimpleDefinitionFinder(SimpleDefinitionFinder defFinder) { this.defFinder = defFinder; } //------------------------------------------------------------------------ // Parsing //------------------------------------------------------------------------ /** * Parses the externs and main inputs. * * @return A synthetic root node whose two children are the externs root * and the main root */ 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(); if (options.tracer.isOn()) { tracker = new PerformanceTracker(jsRoot, options.tracer); addChangeHandler(tracker.getCodeChangeHandler()); } Tracer tracer = newTracer(PARSING_PASS_NAME); beforePass(PARSING_PASS_NAME); try { // Parse externs sources. for (CompilerInput input : externs) { Node n = input.getAstRoot(this); if (hasErrors()) { return null; } externsRoot.addChildToBack(n); } if (options.lowerFromEs6()) { processEs6Modules(); } // Modules inferred in ProcessCommonJS pass. if (options.transformAMDToCJSModules || options.processCommonJSModules) { processAMDAndCommonJSModules(); } if (options.lowerFromEs6() || options.transformAMDToCJSModules || options.processCommonJSModules) { // 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 : inputs) { if (input.getKnownProvides().isEmpty()) { ModuleIdentifier modInfo = ModuleIdentifier.forFile(input.getSourceFile().getOriginalPath()); inputModuleIdentifiers.put(modInfo.getClosureNamespace(), 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 : inputs) { for (String require : input.getKnownRequires()) { if (inputModuleIdentifiers.containsKey(require) && !inputsToRewrite.containsKey(require)) { inputsToRewrite.put(require, inputModuleIdentifiers.get(require)); } } } if (!inputsToRewrite.isEmpty()) { processEs6Modules(new ArrayList<>(inputsToRewrite.values()), true); } } orderInputs(); // If in IDE mode, we ignore the error and keep going. if (hasErrors()) { return null; } // Build the AST. for (CompilerInput input : inputs) { Node n = input.getAstRoot(this); if (n == null) { continue; } if (devMode) { runSanityCheck(); if (hasErrors()) { return null; } } // TODO(johnlenz): we shouldn't need to check both isExternExportsEnabled and // externExportsPath. if (options.sourceMapOutputPath != null || options.nameReferenceReportPath != null || options.isExternExportsEnabled() || options.externExportsPath != 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 = new SourceInformationAnnotator( input.getName(), options.devMode != DevMode.OFF); NodeTraversal.traverseEs6(this, n, sia); } jsRoot.addChildToBack(n); } if (hasErrors()) { return null; } return externAndJsRoot; } finally { afterPass(PARSING_PASS_NAME); stopTracer(tracer, PARSING_PASS_NAME); } } void orderInputsWithLargeStack() { runInCompilerThread(new Callable() { @Override public Void call() throws Exception { Tracer tracer = newTracer("orderInputsWithLargeStack"); try { orderInputs(); } finally { stopTracer(tracer, "orderInputsWithLargeStack"); } return null; } }); } void orderInputs() { hoistExterns(); // Check if the sources need to be re-ordered. boolean staleInputs = false; if (options.dependencyOptions.needsManagement()) { for (CompilerInput input : inputs) { // 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()) { getTypeRegistry().forwardDeclareType(provide); } } try { inputs = (moduleGraph == null ? new JSModuleGraph(modules) : moduleGraph) .manageDependencies(options.dependencyOptions, inputs); staleInputs = true; } catch (MissingProvideException e) { report(JSError.make( MISSING_ENTRY_ERROR, e.getMessage())); } catch (JSModuleGraph.MissingModuleException e) { report(JSError.make( MISSING_MODULE_ERROR, e.getMessage())); } } hoistNoCompileFiles(); if (staleInputs) { repartitionInputs(); } } /** * Hoists inputs with the @externs annotation into the externs list. */ void hoistExterns() { boolean staleInputs = false; for (CompilerInput input : inputs) { if (options.dependencyOptions.needsManagement()) { // If we're doing scanning dependency info anyway, use that // information to skip sources that obviously aren't externs. if (!input.getProvides().isEmpty() || !input.getRequires().isEmpty()) { continue; } } Node n = input.getAstRoot(this); // Inputs can have a null AST on a parse error. if (n == null) { continue; } JSDocInfo info = n.getJSDocInfo(); if (info != null && info.isExterns()) { // If the input file is explicitly marked as an externs file, then // assume the programmer made a mistake and throw it into // the externs pile anyways. externsRoot.addChildToBack(n); input.setIsExtern(true); input.getModule().remove(input); externs.add(input); staleInputs = true; } } if (staleInputs) { repartitionInputs(); } } /** * Hoists inputs with the @nocompile annotation out of the inputs. */ void hoistNoCompileFiles() { boolean staleInputs = false; for (CompilerInput input : inputs) { Node n = input.getAstRoot(this); // Inputs can have a null AST on a parse error. if (n == null) { continue; } JSDocInfo info = n.getJSDocInfo(); if (info != null && info.isNoCompile()) { input.getModule().remove(input); staleInputs = true; } } if (staleInputs) { repartitionInputs(); } } private void repartitionInputs() { fillEmptyModules(modules); rebuildInputsFromModules(); } void processEs6Modules() { processEs6Modules(inputs, false); } void processEs6Modules(List inputsToProcess, boolean forceRewrite) { ES6ModuleLoader loader = new ES6ModuleLoader(this, options.moduleRoots, inputs); for (CompilerInput input : inputsToProcess) { input.setCompiler(this); Node root = input.getAstRoot(this); if (root == null) { continue; } new ProcessEs6Modules(this, loader, true).processFile(root, forceRewrite); } } /** * Transforms AMD and CJS modules to something closure compiler can * process and creates JSModules and the corresponding dependency tree * on the way. */ void processAMDAndCommonJSModules() { ES6ModuleLoader loader = new ES6ModuleLoader(this, options.moduleRoots, inputs); for (CompilerInput input : inputs) { input.setCompiler(this); Node root = input.getAstRoot(this); if (root == null) { continue; } if (options.transformAMDToCJSModules) { new TransformAMDToCJSModule(this).process(null, root); } if (options.processCommonJSModules) { ProcessCommonJSModules cjs = new ProcessCommonJSModules(this, loader, true); cjs.process(null, root); } } } public Node parse(SourceFile file) { initCompilerOptionsIfTesting(); addToDebugLog("Parsing: " + file.getName()); return new JsAst(file).getAstRoot(this); } private int syntheticCodeId = 0; @Override Node parseSyntheticCode(String js) { CompilerInput input = new CompilerInput( SourceFile.fromCode(" [synthetic:" + (++syntheticCodeId) + "] ", js)); putCompilerInput(input.getInputId(), input); return input.getAstRoot(this); } /** * 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()); } } @Override Node parseSyntheticCode(String fileName, String js) { initCompilerOptionsIfTesting(); return parse(SourceFile.fromCode(fileName, js)); } @Override Node parseTestCode(String js) { initCompilerOptionsIfTesting(); CompilerInput input = new CompilerInput( SourceFile.fromCode("[testcode]", js)); if (inputsById == null) { inputsById = new HashMap<>(); } putCompilerInput(input.getInputId(), input); return input.getAstRoot(this); } @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(new Callable() { @Override public String call() throws Exception { Tracer tracer = newTracer("toSource"); try { CodeBuilder cb = new CodeBuilder(); if (jsRoot != null) { int i = 0; 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 each input back to JS code. */ public String[] toSourceArray() { return runInCompilerThread(new Callable() { @Override public String[] call() throws Exception { Tracer tracer = newTracer("toSourceArray"); try { int numInputs = inputs.size(); String[] sources = new String[numInputs]; CodeBuilder cb = new CodeBuilder(); for (int i = 0; i < numInputs; i++) { Node scriptNode = inputs.get(i).getAstRoot(Compiler.this); cb.reset(); toSource(cb, i, scriptNode); sources[i] = cb.toString(); } return sources; } finally { stopTracer(tracer, "toSourceArray"); } } }); } /** * Converts the parse tree for a module back to JS code. */ public String toSource(final JSModule module) { return runInCompilerThread(new Callable() { @Override public String call() throws Exception { List 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(); } }); } /** * Converts the parse tree for each input in a module back to JS code. */ public String[] toSourceArray(final JSModule module) { return runInCompilerThread(new Callable() { @Override public String[] call() throws Exception { List 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; } }); } /** * 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(new Callable() { @Override public Void call() throws Exception { if (options.printInputDelimiter) { if ((cb.getLength() > 0) && !cb.endsWith("\n")) { cb.append("\n"); // Make sure that the label starts on a new line } Preconditions.checkState(root.isScript()); String delimiter = options.inputDelimiter; String inputName = root.getInputId().getIdName(); String sourceName = root.getSourceFileName(); Preconditions.checkState(sourceName != null); Preconditions.checkState(!sourceName.isEmpty()); delimiter = delimiter .replaceAll("%name%", Matcher.quoteReplacement(inputName)) .replaceAll("%num%", String.valueOf(inputSeqNum)); 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.sourceMapOutputPath != null) { 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, SourceMap sourceMap, boolean firstOutput) { CodePrinter.Builder builder = new CodePrinter.Builder(n); builder.setTypeRegistry(this.typeRegistry); builder.setCompilerOptions(options); builder.setSourceMap(sourceMap); builder.setTagAsStrict(firstOutput && options.getLanguageOut().isStrict()); return builder.build(); } /** * 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. */ 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 //------------------------------------------------------------------------ public void optimize() { List optimizations = getPassConfig().getOptimizations(); if (optimizations.isEmpty()) { return; } // Ideally, this pass should be the first pass run, however: // 1) VariableReferenceCheck reports unexpected warnings if Normalize // is done first. // 2) ReplaceMessages, stripCode, and potentially custom passes rely on // unmodified local names. normalize(); // Create extern exports after the normalize because externExports depends on unique names. if (options.isExternExportsEnabled() || options.externExportsPath != null) { externExports(); } phaseOptimizer = new PhaseOptimizer(this, tracker, null); if (options.devMode == DevMode.EVERY_PASS) { phaseOptimizer.setSanityCheck(sanityCheck); } if (options.getCheckDeterminism()) { phaseOptimizer.setPrintAstHashcodes(true); } phaseOptimizer.consume(optimizations); 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"); ControlFlowAnalysis cfa = new ControlFlowAnalysis(this, true, false); process(cfa); stopTracer(tracer, "computeCFG"); return cfa.getCfg(); } public void normalize() { logger.fine("Normalizing"); startPass("normalize"); process(new Normalize(this, false)); endPass(); } @Override void prepareAst(Node root) { CompilerPass pass = new PrepareAst(this); pass.process(null, root); } void recordFunctionInformation() { logger.fine("Recording function information"); startPass("recordFunctionInformation"); RecordFunctionInformation recordFunctionInfoPass = new RecordFunctionInformation( this, getPassConfig().getIntermediateState().functionNames); process(recordFunctionInfoPass); functionInformationMap = recordFunctionInfoPass.getMap(); endPass(); } protected final RecentChange recentChange = new RecentChange(); private final List codeChangeHandlers = new ArrayList<>(); /** Name of the synthetic input that holds synthesized externs. */ static final String SYNTHETIC_EXTERNS = "{SyntheticVarsDeclar}"; /** * Name of the synthetic input that holds synthesized externs which * must be at the end of the externs AST. */ static final String SYNTHETIC_EXTERNS_AT_END = "{SyntheticVarsAtEnd}"; private CompilerInput synthesizedExternsInput = null; private CompilerInput synthesizedExternsInputAtEnd = null; private ImmutableMap defaultDefineValues = ImmutableMap.of(); @Override void addChangeHandler(CodeChangeHandler handler) { codeChangeHandlers.add(handler); } @Override void removeChangeHandler(CodeChangeHandler handler) { codeChangeHandlers.remove(handler); } @Override void setScope(Node n) { if (phaseOptimizer != null) { phaseOptimizer.setScope(n); } } @Override Node getJsRoot() { return jsRoot; } @Override boolean hasScopeChanged(Node n) { if (phaseOptimizer == null) { return true; } return phaseOptimizer.hasScopeChanged(n); } @Override void reportChangeToEnclosingScope(Node n) { if (phaseOptimizer != null) { phaseOptimizer.reportChangeToEnclosingScope(n); phaseOptimizer.startCrossScopeReporting(); reportCodeChange(); phaseOptimizer.endCrossScopeReporting(); } else { reportCodeChange(); } } /** * 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 void reportCodeChange() { for (CodeChangeHandler handler : codeChangeHandlers) { handler.reportChange(); } } @Override public CodingConvention getCodingConvention() { CodingConvention convention = options.getCodingConvention(); convention = convention != null ? convention : defaultCodingConvention; return convention; } @Override public boolean isIdeMode() { return options.ideMode; } @Override Config getParserConfig(ConfigContext context) { if (parserConfig == null) { switch (options.getLanguageIn()) { case ECMASCRIPT3: parserConfig = createConfig(Config.LanguageMode.ECMASCRIPT3); externsParserConfig = createConfig(Config.LanguageMode.ECMASCRIPT5); break; case ECMASCRIPT5: parserConfig = createConfig(Config.LanguageMode.ECMASCRIPT5); externsParserConfig = parserConfig; break; case ECMASCRIPT5_STRICT: parserConfig = createConfig(Config.LanguageMode.ECMASCRIPT5_STRICT); externsParserConfig = parserConfig; break; case ECMASCRIPT6: parserConfig = createConfig(Config.LanguageMode.ECMASCRIPT6); externsParserConfig = parserConfig; break; case ECMASCRIPT6_STRICT: parserConfig = createConfig(Config.LanguageMode.ECMASCRIPT6_STRICT); externsParserConfig = parserConfig; break; case ECMASCRIPT6_TYPED: parserConfig = createConfig(Config.LanguageMode.ECMASCRIPT6_TYPED); externsParserConfig = parserConfig; break; default: throw new IllegalStateException("unexpected language mode: " + options.getLanguageIn()); } } switch (context) { case EXTERNS: return externsParserConfig; default: return parserConfig; } } protected Config createConfig(Config.LanguageMode mode) { return ParserRunner.createConfig( isIdeMode(), options.isParseJsDocDocumentation(), options.isPreserveJsDocWhitespace(), mode, options.extraAnnotationNames); } //------------------------------------------------------------------------ // Error reporting //------------------------------------------------------------------------ /** * The warning classes that are available from the command-line, and * are suppressible by the {@code @suppress} annotation. */ protected 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 CheckLevel getErrorLevel(JSError error) { Preconditions.checkNotNull(options); return warningsGuard.level(error); } /** * Report an internal error. */ @Override void throwInternalError(String message, Exception cause) { String finalMessage = "INTERNAL COMPILER ERROR.\n" + "Please report this problem.\n\n" + message; RuntimeException e = new RuntimeException(finalMessage, cause); if (cause != null) { e.setStackTrace(cause.getStackTrace()); } throw e; } /** * 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 !isIdeMode() && getErrorCount() > 0; } /** * Consults the {@link ErrorManager} to see if we've encountered errors * that should halt compilation.

* * If {@link CompilerOptions#ideMode} 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(); } /** Called from the compiler passes, adds debug info */ @Override void addToDebugLog(String str) { if (options.useDebugLog) { debugLog.append(str); debugLog.append('\n'); logger.fine(str); } } @Override SourceFile getSourceFileByName(String sourceName) { // Here we assume that the source name is the input name, this // is try 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; } @Override public OriginalMapping getSourceMapping(String sourceName, int lineNumber, int columnNumber) { SourceMapInput sourceMap = options.inputSourceMaps.get(sourceName); if (sourceMap == null) { return null; } // JSCompiler uses 1-indexing for lineNumber and 0-indexing for // columnNumber. // SourceMap uses 1-indexing for both. OriginalMapping result = sourceMap.getSourceMap() .getMappingForLine(lineNumber, columnNumber + 1); if (result == null) { return null; } // The sourcemap will return a path relative to the sourcemap's file. // Translate it to one relative to our base directory. String path = getRelativeTo(result.getOriginalFile(), sourceMap.getOriginalPath()); sourceMapOriginalSources.putIfAbsent( path, originalSourcesLoader.apply(path)); return result.toBuilder() .setOriginalFile(path) .setColumnPosition(result.getColumnPosition() - 1) .build(); } @Override public 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 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 helpers //------------------------------------------------------------------------ @Override Node getNodeForCodeInsertion(JSModule module) { if (module == null) { if (inputs.isEmpty()) { throw new IllegalStateException("No inputs"); } return inputs.get(0).getAstRoot(this); } List moduleInputs = module.getInputs(); if (!moduleInputs.isEmpty()) { return moduleInputs.get(0).getAstRoot(this); } throw new IllegalStateException("Root module has no inputs"); } public SourceMap getSourceMap() { return sourceMap; } VariableMap getVariableMap() { return getPassConfig().getIntermediateState().variableMap; } VariableMap getPropertyMap() { return getPassConfig().getIntermediateState().propertyMap; } @Override CompilerOptions getOptions() { return options; } FunctionInformationMap getFunctionalInformationMap() { return functionInformationMap; } /** * 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) { ControlFlowAnalysis cfa = new ControlFlowAnalysis(this, true, false); cfa.process(null, jsRoot); return DotFormatter.toDot(jsRoot, cfa.getCfg()); } else { return ""; } } @Override public ErrorManager getErrorManager() { if (options == null) { initOptions(newCompilerOptions()); } return errorManager; } @Override List getInputsInOrder() { return Collections.unmodifiableList(inputs); } /** * 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 List getInputsForTesting() { return inputs; } @VisibleForTesting List getExternsForTesting() { return externs; } @Override boolean hasRegExpGlobalReferences() { return hasRegExpGlobalReferences; } @Override void setHasRegExpGlobalReferences(boolean references) { hasRegExpGlobalReferences = references; } @Override void updateGlobalVarReferences(Map refMapPatch, Node collectionRoot) { Preconditions.checkState(collectionRoot.isScript() || collectionRoot.isBlock()); if (globalRefMap == null) { globalRefMap = new GlobalVarReferenceMap(getInputsInOrder(), getExternsInOrder()); } globalRefMap.updateGlobalVarReferences(refMapPatch, collectionRoot); } @Override GlobalVarReferenceMap getGlobalVarReferences() { return globalRefMap; } @Override CompilerInput getSynthesizedExternsInput() { if (synthesizedExternsInput == null) { synthesizedExternsInput = newExternInput(SYNTHETIC_EXTERNS, SyntheticExternsPosition.START); } return synthesizedExternsInput; } @Override CompilerInput getSynthesizedExternsInputAtEnd() { if (synthesizedExternsInputAtEnd == null) { synthesizedExternsInputAtEnd = newExternInput( SYNTHETIC_EXTERNS_AT_END, SyntheticExternsPosition.END); } return synthesizedExternsInputAtEnd; } @Override public double getProgress() { return progress; } @Override String getLastPassName() { return lastPassName; } @Override void setProgress(double newProgress, String passName) { this.lastPassName = passName; if (newProgress > 1.0) { progress = 1.0; } else { progress = newProgress; } } @Override void setExternProperties(Set externProperties) { this.externProperties = externProperties; } @Override Set getExternProperties() { return externProperties; } /** * Replaces one file in a hot-swap mode. The given JsAst should be made * from a new version of a file that already was present in the last compile * call. If the file is new, this will silently ignored. * * @param ast the ast of the file that is being replaced */ public void replaceScript(JsAst ast) { CompilerInput input = this.getInput(ast.getInputId()); if (!replaceIncrementalSourceAst(ast)) { return; } Node originalRoot = input.getAstRoot(this); processNewScript(ast, originalRoot); } /** * Adds a new Script AST to the compile state. If a script for the same file * already exists the script will not be added, instead a call to * #replaceScript should be used. * * @param ast the ast of the new file */ public void addNewScript(JsAst ast) { if (!addNewSourceAst(ast)) { return; } Node emptyScript = new Node(Token.SCRIPT); InputId inputId = ast.getInputId(); emptyScript.setInputId(inputId); emptyScript.setStaticSourceFile( SourceFile.fromCode(inputId.getIdName(), "")); processNewScript(ast, emptyScript); } private void processNewScript(JsAst ast, Node originalRoot) { Node js = ast.getAstRoot(this); Preconditions.checkNotNull(js); runHotSwap(originalRoot, js, this.getCleanupPassConfig()); // NOTE: If hot swap passes that use GlobalNamespace are added, we will need // to revisit this approach to clearing GlobalNamespaces runHotSwapPass(null, null, ensureDefaultPassConfig().garbageCollectChecks); this.getTypeRegistry().clearNamedTypes(); this.removeSyntheticVarsInput(); runHotSwap(originalRoot, js, this.ensureDefaultPassConfig()); } /** * Execute the passes from a PassConfig instance over a single replaced file. */ private void runHotSwap( Node originalRoot, Node js, PassConfig passConfig) { for (PassFactory passFactory : passConfig.getChecks()) { runHotSwapPass(originalRoot, js, passFactory); } } private void runHotSwapPass( Node originalRoot, Node js, PassFactory passFactory) { HotSwapCompilerPass pass = passFactory.getHotSwapPass(this); if (pass != null) { logger.info("Performing HotSwap for pass " + passFactory.getName()); pass.hotSwapScript(js, originalRoot); } } private PassConfig getCleanupPassConfig() { return new CleanupPasses(getOptions()); } private void removeSyntheticVarsInput() { String sourceName = Compiler.SYNTHETIC_EXTERNS; removeExternInput(new InputId(sourceName)); } @Override Node ensureLibraryInjected(String resourceName, boolean force) { boolean doNotInject = !force && (options.skipNonTranspilationPasses || options.preventLibraryInjection); if (injectedLibraries.containsKey(resourceName) || doNotInject) { return lastInjectedLibrary; } // Load/parse the code. String originalCode = ResourceLoader.loadTextResource( Compiler.class, "js/" + resourceName + ".js"); Node ast = parseSyntheticCode(originalCode); // Look for string literals of the form 'require foo bar' or 'externs baz' or 'normalize'. // As we process each one, remove it from its parent. for (Node node = ast.getFirstChild(); node != null && node.isExprResult() && node.getFirstChild().isString(); node = ast.getFirstChild()) { String directive = node.getFirstChild().getString(); List words = Splitter.on(' ').splitToList(directive); switch (words.get(0)) { case "use": // 'use strict' is ignored (and deleted). break; case "require": // 'require lib1 lib2'; pulls in the named libraries before this one. for (String dependency : words.subList(1, words.size())) { ensureLibraryInjected(dependency, force); } break; case "declare": // 'declare name1 name2'; adds the names to the externs (with no type information). // Note that we could simply add the entire externs library, but that leads to // potentially-surprising behavior when the externs that are present depend on // whether or not a polyfill is used. for (String extern : words.subList(1, words.size())) { getSynthesizedExternsInputAtEnd() .getAstRoot(this) .addChildToBack(IR.var(IR.name(extern))); } break; default: throw new RuntimeException("Bad directive: " + directive); } ast.removeChild(node); } // If we've already started optimizations, then we need to normalize this. if (getLifeCycleStage().isNormalized()) { Normalize.normalizeSyntheticCode(this, ast, "jscomp_" + resourceName + "_"); } // Insert the code immediately after the last-inserted runtime library. Node firstChild = ast.removeChildren(); Node lastChild = firstChild.getLastSibling(); Node parent = getNodeForCodeInsertion(null); if (lastInjectedLibrary == null) { parent.addChildrenToFront(firstChild); } else { parent.addChildrenAfter(firstChild, lastInjectedLibrary); } lastInjectedLibrary = lastChild; injectedLibraries.put(resourceName, lastChild); reportCodeChange(); return lastChild; } /** Returns the compiler version baked into the jar. */ @GwtIncompatible("java.util.ResourceBundle") public static String getReleaseVersion() { ResourceBundle config = ResourceBundle.getBundle(CONFIG_RESOURCE); return config.getString("compiler.version"); } /** Returns the compiler date baked into the jar. */ @GwtIncompatible("java.util.ResourceBundle") public static String getReleaseDate() { ResourceBundle config = ResourceBundle.getBundle(CONFIG_RESOURCE); return config.getString("compiler.date"); } @Override void addComments(String filename, List comments) { if (!isIdeMode()) { throw new UnsupportedOperationException( "addComments may only be called in IDE mode."); } commentsPerFile.put(filename, comments); } @Override public List getComments(String filename) { if (!isIdeMode()) { throw new UnsupportedOperationException( "getComments may only be called in IDE mode."); } return commentsPerFile.get(filename); } @Override void setDefaultDefineValues(ImmutableMap values) { this.defaultDefineValues = values; } @Override ImmutableMap getDefaultDefineValues() { return this.defaultDefineValues; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy