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

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

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
Show newest version
/*
 * 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.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.javascript.jscomp.CompilerOptions.DevMode;
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
import com.google.javascript.jscomp.CompilerOptions.TracerMode;
import com.google.javascript.jscomp.deps.SortedDependencies.CircularDependencyException;
import com.google.javascript.jscomp.deps.SortedDependencies.MissingProvideException;
import com.google.javascript.jscomp.mozilla.rhino.ErrorReporter;
import com.google.javascript.jscomp.parsing.Config;
import com.google.javascript.jscomp.parsing.ParserRunner;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.jstype.JSTypeRegistry;

import java.io.IOException;
import java.io.PrintStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * 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 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"); 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; // Parse tree root nodes Node externsRoot; Node jsRoot; Node externAndJsRoot; private Map inputsByName; /** 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 the optional "normalization" pass has been run. Passes that * depend on the assumptions made there should check this value. */ private boolean normalized = false; /** Whether to use threads. */ private boolean useThreads = true; /** * 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 GoogleCodingConvention(); private JSTypeRegistry typeRegistry; private Config parserConfig = null; private ReverseAbstractInterpreter abstractInterpreter; private TypeValidator typeValidator; public PerformanceTracker tracker; // The oldErrorReporter exists so we can get errors from the JSTypeRegistry. private final com.google.javascript.rhino.ErrorReporter oldErrorReporter = RhinoErrorReporter.forOldRhino(this); // This error reporter gets the messages from the current Rhino parser. private final ErrorReporter defaultErrorReporter = RhinoErrorReporter.forNewRhino(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 static final long COMPILER_STACK_SIZE = 1048576L; /** * 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; /** * Creates a Compiler that reports errors and warnings to its logger. */ public Compiler() { this((PrintStream) null); } /** * Creates n 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); } /** * Initialize the compiler options. Only necessary if you're not doing * a normal compile() job. */ public void initOptions(CompilerOptions options) { this.options = options; if (errorManager == null) { if (outStream == null) { setErrorManager( new LoggerErrorManager(createMessageFormatter(), logger)); } else { PrintStreamErrorManager printer = new PrintStreamErrorManager(createMessageFormatter(), outStream); printer.setSummaryDetailLevel(options.summaryDetailLevel); setErrorManager(printer); } } // Initialize the warnings guard. List guards = Lists.newArrayList(); guards.add( new SuppressDocWarningsGuard( getDiagnosticGroups().getRegisteredGroups())); WarningsGuard warningsGuard = options.getWarningsGuard(); if (warningsGuard != null) { guards.add(options.getWarningsGuard()); } // 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 && (warningsGuard == null || !warningsGuard.disables( DiagnosticGroups.CHECK_VARIABLES))) { guards.add(new DiagnosticGroupWarningsGuard( DiagnosticGroups.CHECK_VARIABLES, CheckLevel.OFF)); } this.warningsGuard = new ComposeWarningsGuard(guards); } /** * Initializes the instance state needed for a compile job. */ public void init(JSSourceFile[] externs, JSSourceFile[] inputs, CompilerOptions options) { init(Lists.newArrayList(externs), Lists.newArrayList(inputs), options); } /** * Initializes the instance state needed for a compile job. */ public void init(List externs, List inputs, CompilerOptions options) { JSModule module = new JSModule("[singleton]"); for (JSSourceFile input : inputs) { module.add(input); } initModules(externs, Lists.newArrayList(module), options); } /** * Initializes the instance state needed for a compile job if the sources * are in modules. */ public void init(JSSourceFile[] externs, JSModule[] modules, CompilerOptions options) { initModules(Lists.newArrayList(externs), Lists.newArrayList(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(); initInputsByNameMap(); } /** * 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(); } } private List makeCompilerInput( List files, boolean isExtern) { List inputs = Lists.newArrayList(); for (JSSourceFile 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())); } } /** * 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(JSSourceFile.fromCode("[" + 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(JSSourceFile[], JSModule[], CompilerOptions)} call. */ public void rebuildInputsFromModules() { inputs = getAllInputsFromModules(modules); initInputsByNameMap(); } /** * Builds a single list of all module inputs. Verifies that it contains no * duplicates. */ private static List getAllInputsFromModules( List modules) { List inputs = Lists.newArrayList(); Map inputMap = Maps.newHashMap(); 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}"); /** * Creates a map to make looking up an input by name fast. Also checks for * duplicate inputs. */ void initInputsByNameMap() { inputsByName = new HashMap(); for (CompilerInput input : externs) { String name = input.getName(); if (!inputsByName.containsKey(name)) { inputsByName.put(name, input); } else { report(JSError.make(DUPLICATE_EXTERN_INPUT, name)); } } for (CompilerInput input : inputs) { String name = input.getName(); if (!inputsByName.containsKey(name)) { inputsByName.put(name, input); } else { report(JSError.make(DUPLICATE_INPUT, name)); } } } public Result compile( JSSourceFile extern, JSSourceFile input, CompilerOptions options) { return compile(extern, new JSSourceFile[] { input }, options); } public Result compile( JSSourceFile extern, JSSourceFile[] input, CompilerOptions options) { return compile(new JSSourceFile[] { extern }, input, options); } public Result compile( JSSourceFile extern, JSModule[] modules, CompilerOptions options) { return compile(new JSSourceFile[] { extern }, modules, options); } /** * Compiles a list of inputs. */ public Result compile(JSSourceFile[] externs, JSSourceFile[] inputs, CompilerOptions options) { return compile(Lists.newArrayList(externs), Lists.newArrayList(inputs), 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 compile(JSSourceFile[] externs, JSModule[] modules, CompilerOptions options) { return compileModules(Lists.newArrayList(externs), Lists.newArrayList(modules), options); } /** * 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() { 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() { useThreads = false; } private T runInCompilerThread(final Callable callable) { return runCallable(callable, useThreads, options.tracer.isOn()); } static T runCallableWithLargeStack(final Callable callable) { return runCallable(callable, true, false); } @SuppressWarnings("unchecked") static T runCallable( final Callable callable, boolean useLargeStackThread, boolean trace) { // Under JRE 1.6, the jscompiler overflows the stack when running on some // large or complex js code. Here we start a new thread with a larger // stack in order to let the compiler do its thing, without having to // increase the stack size for *every* thread (which is what -Xss does). // Might want to add thread pool support for clients that compile a lot. final boolean dumpTraceReport = trace; final Object[] result = new Object[1]; final Throwable[] exception = new Throwable[1]; Runnable runnable = new Runnable() { public void run() { try { if (dumpTraceReport) { Tracer.initCurrentThreadTrace(); } result[0] = callable.call(); } catch (Throwable e) { exception[0] = e; } finally { if (dumpTraceReport) { Tracer.logAndClearCurrentThreadTrace(); } } } }; if (useLargeStackThread) { Thread th = new Thread(null, runnable, "jscompiler", COMPILER_STACK_SIZE); th.start(); while (true) { try { th.join(); break; } catch (InterruptedException ignore) { // ignore } } } else { runnable.run(); } // Pass on any exception caught by the runnable object. if (exception[0] != null) { throw new RuntimeException(exception[0]); } return (T) result[0]; } private void compileInternal() { parse(); if (hasErrors()) { return; } if (!precheck()) { return; } if (options.nameAnonymousFunctionsOnly) { // TODO(nicksantos): Move this into an instrument() phase maybe? check(); return; } if (!options.skipAllPasses) { check(); if (hasErrors()) { return; } if (options.isExternExportsEnabled() || options.externExportsPath != null) { externExports(); } // IDE-mode is defined to stop here, before the heavy rewriting begins. if (!options.ideMode) { optimize(); } } if (options.recordFunctionInformation) { recordFunctionInformation(); } if (options.devMode == DevMode.START_AND_END) { runSanityCheck(); } } 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 he or she probably meant to use their // own PassConfig object. Preconditions.checkNotNull(passes); if (this.passes != null) { throw new IllegalStateException("this.passes has already been assigned"); } 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 check() { runCustomPasses(CustomPassExecutionTime.BEFORE_CHECKS); PhaseOptimizer phaseOptimizer = new PhaseOptimizer(this, tracker); if (options.devMode == DevMode.EVERY_PASS) { phaseOptimizer.setSanityCheck(sanityCheck); } phaseOptimizer.consume(getPassConfig().getChecks()); phaseOptimizer.process(externsRoot, jsRoot); if (hasErrors()) { return; } // TODO(nicksantos): clean this up. The flow here is too hard to follow. if (options.nameAnonymousFunctionsOnly) { return; } if (options.removeTryCatchFinally) { removeTryCatchFinally(); } if (!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); } private void externExports() { logger.info("Creating extern file for exports"); startPass("externExports"); ExternExportsPass pass = new ExternExportsPass(this); process(pass); externExports = pass.getGeneratedExterns(); endPass(); } void process(CompilerPass p) { p.process(externsRoot, jsRoot); } private final PassFactory sanityCheck = new PassFactory("sanityCheck", false) { @Override protected CompilerPass createInternal(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); } /** * Removes try/catch/finally statements for easier debugging. */ void removeTryCatchFinally() { logger.info("Remove try/catch/finally"); startPass("removeTryCatchFinally"); RemoveTryCatch r = new RemoveTryCatch(this); process(r); endPass(); } /** * 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.info("Strip code"); startPass("stripCode"); StripCode r = new StripCode(this, stripTypes, stripNameSuffixes, stripTypePrefixes, stripNamePrefixes); 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); String passToCheck = currentPassName; currentPassName = null; currentTracer = null; maybeSanityCheck(); } /** * 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); } 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 an array constructed from errors + temporary warnings. */ public JSError[] getMessages() { return getErrors(); } /** * Returns the array of errors (never null). */ public JSError[] getErrors() { return errorManager.getErrors(); } /** * Returns the array of warnings (never null). */ public JSError[] getWarnings() { return errorManager.getWarnings(); } /** * Returns the root node of the AST, which includes both externs and source. */ public Node getRoot() { return externAndJsRoot; } /** * 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() { public String get() { return String.valueOf(self.nextUniqueNameId()); } }; } /** * Set if the normalization pass has been done. * Note: non-private to enable test cases that require the Normalize pass. */ @Override void setNormalized() { normalized = true; } /** * Set once unnormalizing passes have been start. * Note: non-private to enable test cases that require the Normalize pass. */ @Override void setUnnormalized() { normalized = false; } @Override boolean isNormalized() { return normalized; } @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.checkTreeTypeAwareEqualsSilent(n2); } else { return n1.checkTreeEqualsSilent(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(String name) { return inputsByName.get(name); } @Override public CompilerInput newExternInput(String name) { if (inputsByName.containsKey(name)) { throw new IllegalArgumentException("Conflicting externs name: " + name); } SourceAst ast = new SyntheticAst(name); CompilerInput input = new CompilerInput(ast, name, true); inputsByName.put(name, input); externsRoot.addChildToFront(ast.getAstRoot(this)); return input; } /** Add a source input dynamically. Intended for incremental compilation. */ void addIncrementalSourceAst(JsAst ast) { String sourceName = ast.getSourceFile().getName(); Preconditions.checkState( getInput(sourceName) == null, "Duplicate input of name " + sourceName); inputsByName.put(sourceName, new CompilerInput(ast)); } /** * 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) { String sourceName = ast.getSourceFile().getName(); CompilerInput oldInput = Preconditions.checkNotNull( getInput(sourceName), "No input to replace: " + sourceName); 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); } inputsByName.put(sourceName, new CompilerInput(ast)); return true; } @Override JSModuleGraph getModuleGraph() { return moduleGraph; } @Override public JSTypeRegistry getTypeRegistry() { if (typeRegistry == null) { typeRegistry = new JSTypeRegistry(oldErrorReporter, options.looseTypes); } return typeRegistry; } @Override ScopeCreator getScopeCreator() { return getPassConfig().getScopeCreator(); } @Override public Scope getTopScope() { return getPassConfig().getTopScope(); } @Override public ReverseAbstractInterpreter getReverseAbstractInterpreter() { if (abstractInterpreter == null) { ChainableReverseAbstractInterpreter interpreter = new SemanticReverseAbstractInterpreter( getCodingConvention(), getTypeRegistry()); if (options.closurePass) { interpreter = new ClosureReverseAbstractInterpreter( getCodingConvention(), getTypeRegistry()) .append(interpreter).getFirst(); } abstractInterpreter = interpreter; } return abstractInterpreter; } @Override TypeValidator getTypeValidator() { if (typeValidator == null) { typeValidator = new TypeValidator(this); } return typeValidator; } //------------------------------------------------------------------------ // 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. if (externsRoot != null) { externsRoot.detachChildren(); } if (jsRoot != null) { jsRoot.detachChildren(); } // Parse main js sources. jsRoot = new Node(Token.BLOCK); jsRoot.setIsSyntheticBlock(true); if (options.tracer.isOn()) { tracker = new PerformanceTracker(jsRoot, options.tracer == TracerMode.ALL); addChangeHandler(tracker.getCodeChangeHandler()); } Tracer tracer = newTracer("parseInputs"); try { // Parse externs sources. externsRoot = new Node(Token.BLOCK); externsRoot.setIsSyntheticBlock(true); for (CompilerInput input : externs) { Node n = input.getAstRoot(this); if (hasErrors()) { return null; } externsRoot.addChildToBack(n); } // Check if the sources need to be re-ordered. if (options.manageClosureDependencies) { for (CompilerInput input : inputs) { input.setCompiler(this); // 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.manageClosureDependenciesEntryPoints, inputs); } catch (CircularDependencyException e) { report(JSError.make( JSModule.CIRCULAR_DEPENDENCY_ERROR, e.getMessage())); return null; } catch (MissingProvideException e) { report(JSError.make( MISSING_ENTRY_ERROR, e.getMessage())); return null; } } // Check if inputs need to be rebuilt from modules. boolean staleInputs = false; for (CompilerInput input : inputs) { Node n = input.getAstRoot(this); if (hasErrors()) { return null; } // Inputs can have a null AST during initial parse. if (n == null) { continue; } if (n.getJSDocInfo() != null) { JSDocInfo info = n.getJSDocInfo(); if (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; } else if (info.isNoCompile()) { input.getModule().remove(input); staleInputs = true; } } } if (staleInputs) { fillEmptyModules(modules); rebuildInputsFromModules(); } // Build the AST. for (CompilerInput input : inputs) { Node n = input.getAstRoot(this); if (n == null) { continue; } if (devMode) { runSanityCheck(); if (hasErrors()) { return null; } } if (options.sourceMapOutputPath != null || options.nameReferenceReportPath != null) { // 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.traverse(this, n, sia); } jsRoot.addChildToBack(n); } externAndJsRoot = new Node(Token.BLOCK, externsRoot, jsRoot); externAndJsRoot.setIsSyntheticBlock(true); return externAndJsRoot; } finally { stopTracer(tracer, "parseInputs"); } } public Node parse(JSSourceFile file) { initCompilerOptionsIfTesting(); addToDebugLog("Parsing: " + file.getName()); return new JsAst(file).getAstRoot(this); } @Override Node parseSyntheticCode(String js) { CompilerInput input = new CompilerInput( JSSourceFile.fromCode(" [synthetic] ", js)); inputsByName.put(input.getName(), input); return input.getAstRoot(this); } void initCompilerOptionsIfTesting() { if (options == null) { // initialization for tests that don't initialize the compiler // by the normal mechanisms. initOptions(new CompilerOptions()); } } @Override Node parseSyntheticCode(String fileName, String js) { initCompilerOptionsIfTesting(); return parse(JSSourceFile.fromCode(fileName, js)); } @Override Node parseTestCode(String js) { initCompilerOptionsIfTesting(); CompilerInput input = new CompilerInput( JSSourceFile.fromCode(" [testcode] ", js)); if (inputsByName == null) { inputsByName = Maps.newHashMap(); } inputsByName.put(input.getName(), input); return input.getAstRoot(this); } @Override ErrorReporter getDefaultErrorReporter() { return defaultErrorReporter; } //------------------------------------------------------------------------ // Convert back to source code //------------------------------------------------------------------------ /** * Converts the main parse tree back to js code. */ public String toSource() { return runInCompilerThread(new Callable() { 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() { 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() { 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() { 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() { 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.getType() == Token.SCRIPT); String delimiter = options.inputDelimiter; String sourceName = (String)root.getProp(Node.SOURCENAME_PROP); Preconditions.checkState(sourceName != null); Preconditions.checkState(!sourceName.isEmpty()); delimiter = delimiter.replaceAll("%name%", sourceName) .replaceAll("%num%", String.valueOf(inputSeqNum)); cb.append(delimiter) .append("\n"); } if (root.getJSDocInfo() != null && root.getJSDocInfo().getLicense() != null) { cb.append("/*\n") .append(root.getJSDocInfo().getLicense()) .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()); } String code = toSource(root, sourceMap); if (!code.isEmpty()) { cb.append(code); if (!code.endsWith(";")) { cb.append(";"); } } return null; } }); } /** * Generates JavaScript source code for an AST, doesn't generate source * map info. */ @Override String toSource(Node n) { initCompilerOptionsIfTesting(); return toSource(n, null); } /** * Generates JavaScript source code for an AST. */ private String toSource(Node n, SourceMap sourceMap) { CodePrinter.Builder builder = new CodePrinter.Builder(n); builder.setPrettyPrint(options.prettyPrint); builder.setLineBreak(options.lineBreak); builder.setSourceMap(sourceMap); builder.setSourceMapDetailLevel(options.sourceMapDetailLevel); Charset charset = options.outputCharset != null ? Charset.forName(options.outputCharset) : null; builder.setOutputCharset(charset); 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; /** 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())); } } //------------------------------------------------------------------------ // Optimizations //------------------------------------------------------------------------ public void optimize() { // 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(); PhaseOptimizer phaseOptimizer = new PhaseOptimizer(this, tracker); if (options.devMode == DevMode.EVERY_PASS) { phaseOptimizer.setSanityCheck(sanityCheck); } phaseOptimizer.consume(getPassConfig().getOptimizations()); phaseOptimizer.process(externsRoot, jsRoot); if (hasErrors()) { return; } } @Override void setCssRenamingMap(CssRenamingMap map) { options.cssRenamingMap = map; } @Override CssRenamingMap getCssRenamingMap() { return options.cssRenamingMap; } /** * Reprocesses the current defines over the AST. This is used by GwtCompiler * to generate N outputs for different targets from the same (checked) AST. * For each target, we apply the target-specific defines by calling * {@code processDefines} and then {@code optimize} to optimize the AST * specifically for that target. */ public void processDefines() { (new DefaultPassConfig(options)).processDefines.create(this) .process(externsRoot, jsRoot); } boolean isInliningForbidden() { return options.propertyRenaming == PropertyRenamingPolicy.HEURISTIC || options.propertyRenaming == PropertyRenamingPolicy.AGGRESSIVE_HEURISTIC; } /** Control Flow Analysis. */ ControlFlowGraph computeCFG() { logger.info("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.info("Normalizing"); startPass("normalize"); process(new Normalize(this, false)); endPass(); } @Override void prepareAst(Node root) { Tracer tracer = newTracer("prepareAst"); CompilerPass pass = new PrepareAst(this); pass.process(null, root); stopTracer(tracer, "prepareAst"); } void recordFunctionInformation() { logger.info("Recording function information"); startPass("recordFunctionInformation"); RecordFunctionInformation recordFunctionInfoPass = new RecordFunctionInformation( this, getPassConfig().getIntermediateState().functionNames); process(recordFunctionInfoPass); functionInformationMap = recordFunctionInfoPass.getMap(); endPass(); } protected final CodeChangeHandler.RecentChange recentChange = new CodeChangeHandler.RecentChange(); private final List codeChangeHandlers = Lists.newArrayList(); @Override void addChangeHandler(CodeChangeHandler handler) { codeChangeHandlers.add(handler); } @Override void removeChangeHandler(CodeChangeHandler handler) { codeChangeHandlers.remove(handler); } /** * All passes should call reportCodeChange() when they alter * the JS tree structure. This is verified by CompilerTestCase. * This allows us to optimize to a fixed point. */ @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 public boolean acceptEcmaScript5() { return options.languageIn == LanguageMode.ECMASCRIPT5; } @Override Config getParserConfig() { if (parserConfig == null) { parserConfig = ParserRunner.createConfig( isIdeMode(), acceptEcmaScript5()); } return parserConfig; } @Override public boolean isTypeCheckingEnabled() { return options.checkTypes; } //------------------------------------------------------------------------ // Error reporting //------------------------------------------------------------------------ /** * The warning classes that are available from the command-line, and * are suppressable by the {@code @suppress} annotation. */ protected DiagnosticGroups getDiagnosticGroups() { return new DiagnosticGroups(); } @Override public void report(JSError error) { CheckLevel level = error.level; if (warningsGuard != null) { CheckLevel newLevel = warningsGuard.level(error); if (newLevel != null) { level = newLevel; } } if (level.isOn()) { errorManager.report(level, error); } } @Override public CheckLevel getErrorLevel(JSError error) { Preconditions.checkNotNull(options); WarningsGuard guards = options.getWarningsGuard(); if (guards == null) { return error.level; } else { return guards.level(error); } } /** * Report an internal error. */ @Override void throwInternalError(String message, Exception cause) { String finalMessage = "INTERNAL COMPILER ERROR.\n" + "Please report this problem.\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) { debugLog.append(str); debugLog.append('\n'); logger.fine(str); } private SourceFile getSourceFileByName(String sourceName) { if (inputsByName.containsKey(sourceName)) { return inputsByName.get(sourceName).getSourceFile(); } return null; } 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; } 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.size() > 0) { 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; } 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(new CompilerOptions()); } return errorManager; } @Override List getInputsInOrder() { return Collections.unmodifiableList(inputs); } /** * Stores the internal compiler state just before optimization is performed. * This can be saved and restored in order to efficiently optimize multiple * different output targets without having to perform checking multiple times. * * NOTE: This does not include all parts of the compiler's internal state. In * particular, JSSourceFiles and CompilerOptions are not recorded. In * order to recreate a Compiler instance from scratch, you would need to * call {@code init} with the same arguments as in the initial creation before * restoring intermediate state. */ public static class IntermediateState implements Serializable { private static final long serialVersionUID = 1L; Node externsRoot; private Node jsRoot; private List externs; private List inputs; private List modules; private PassConfig.State passConfigState; private JSTypeRegistry typeRegistry; private boolean normalized; private IntermediateState() {} } /** * Returns the current internal state, excluding the input files and modules. */ public IntermediateState getState() { IntermediateState state = new IntermediateState(); state.externsRoot = externsRoot; state.jsRoot = jsRoot; state.externs = externs; state.inputs = inputs; state.modules = modules; state.passConfigState = getPassConfig().getIntermediateState(); state.typeRegistry = typeRegistry; state.normalized = normalized; return state; } /** * Sets the internal state to the capture given. Note that this assumes that * the input files are already set up. */ public void setState(IntermediateState state) { externsRoot = state.externsRoot; jsRoot = state.jsRoot; externs = state.externs; inputs = state.inputs; modules = state.modules; passes = createPassConfigInternal(); getPassConfig().setIntermediateState(state.passConfigState); typeRegistry = state.typeRegistry; normalized = state.normalized; } @VisibleForTesting List getInputsForTesting() { return inputs; } @VisibleForTesting List getExternsForTesting() { return externs; } @Override boolean hasRegExpGlobalReferences() { return hasRegExpGlobalReferences; } @Override void setHasRegExpGlobalReferences(boolean references) { hasRegExpGlobalReferences = references; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy