Index: third_party/closure_compiler/compile.py |
diff --git a/third_party/closure_compiler/compile.py b/third_party/closure_compiler/compile.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..9132b7d7801d51714aa408e0feea144f2be938e3 |
--- /dev/null |
+++ b/third_party/closure_compiler/compile.py |
@@ -0,0 +1,376 @@ |
+#!/usr/bin/python |
+# Copyright 2014 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+"""Runs Closure compiler on JavaScript files to check for errors and produce |
+minified output.""" |
+ |
+import argparse |
+import os |
+import re |
+import subprocess |
+import sys |
+import tempfile |
+ |
+import build.inputs |
+import processor |
+import error_filter |
+ |
+ |
+_CURRENT_DIR = os.path.join(os.path.dirname(__file__)) |
+ |
+ |
+class Checker(object): |
+ """Runs the Closure compiler on given source files to typecheck them |
+ and produce minified output.""" |
+ |
+ _JAR_COMMAND = [ |
+ "java", |
+ "-jar", |
+ "-Xms1024m", |
+ "-client", |
+ "-XX:+TieredCompilation" |
+ ] |
+ |
+ _MAP_FILE_FORMAT = "%s.map" |
+ |
+ def __init__(self, verbose=False): |
+ """ |
+ Args: |
+ verbose: Whether this class should output diagnostic messages. |
+ """ |
+ self._compiler_jar = os.path.join(_CURRENT_DIR, "compiler", "compiler.jar") |
+ self._temp_files = [] |
+ self._verbose = verbose |
+ self._error_filter = error_filter.PromiseErrorFilter() |
+ |
+ def _nuke_temp_files(self): |
+ """Deletes any temp files this class knows about.""" |
+ if not self._temp_files: |
+ return |
+ |
+ self._log_debug("Deleting temp files: %s" % ", ".join(self._temp_files)) |
+ for f in self._temp_files: |
+ os.remove(f) |
+ self._temp_files = [] |
+ |
+ def _log_debug(self, msg, error=False): |
+ """Logs |msg| to stdout if --verbose/-v is passed when invoking this script. |
+ |
+ Args: |
+ msg: A debug message to log. |
+ """ |
+ if self._verbose: |
+ print "(INFO) %s" % msg |
+ |
+ def _log_error(self, msg): |
+ """Logs |msg| to stderr regardless of --flags. |
+ |
+ Args: |
+ msg: An error message to log. |
+ """ |
+ print >> sys.stderr, "(ERROR) %s" % msg |
+ |
+ def _run_jar(self, jar, args): |
+ """Runs a .jar from the command line with arguments. |
+ |
+ Args: |
+ jar: A file path to a .jar file |
+ args: A list of command line arguments to be passed when running the .jar. |
+ |
+ Return: |
+ (exit_code, stderr) The exit code of the command (e.g. 0 for success) and |
+ the stderr collected while running |jar| (as a string). |
+ """ |
+ shell_command = " ".join(self._JAR_COMMAND + [jar] + args) |
+ self._log_debug("Running jar: %s" % shell_command) |
+ |
+ devnull = open(os.devnull, "w") |
+ kwargs = {"stdout": devnull, "stderr": subprocess.PIPE, "shell": True} |
+ process = subprocess.Popen(shell_command, **kwargs) |
+ _, stderr = process.communicate() |
+ return process.returncode, stderr |
+ |
+ def _get_line_number(self, match): |
+ """When chrome is built, it preprocesses its JavaScript from: |
+ |
+ <include src="blah.js"> |
+ alert(1); |
+ |
+ to: |
+ |
+ /* contents of blah.js inlined */ |
+ alert(1); |
+ |
+ Because Closure Compiler requires this inlining already be done (as |
+ <include> isn't valid JavaScript), this script creates temporary files to |
+ expand all the <include>s. |
+ |
+ When type errors are hit in temporary files, a developer doesn't know the |
+ original source location to fix. This method maps from /tmp/file:300 back to |
+ /original/source/file:100 so fixing errors is faster for developers. |
+ |
+ Args: |
+ match: A re.MatchObject from matching against a line number regex. |
+ |
+ Returns: |
+ The fixed up /file and :line number. |
+ """ |
+ real_file = self._processor.get_file_from_line(match.group(1)) |
+ return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number) |
+ |
+ def _filter_errors(self, errors): |
+ """Removes some extraneous errors. For example, we ignore: |
+ |
+ Variable x first declared in /tmp/expanded/file |
+ |
+ Because it's just a duplicated error (it'll only ever show up 2+ times). |
+ We also ignore Promose-based errors: |
+ |
+ found : function (VolumeInfo): (Promise<(DirectoryEntry|null)>|null) |
+ required: (function (Promise<VolumeInfo>): ?|null|undefined) |
+ |
+ as templates don't work with Promises in all cases yet. See |
+ https://github.com/google/closure-compiler/issues/715 for details. |
+ |
+ Args: |
+ errors: A list of string errors extracted from Closure Compiler output. |
+ |
+ Return: |
+ A slimmer, sleeker list of relevant errors (strings). |
+ """ |
+ first_declared_in = lambda e: " first declared in " not in e |
+ return self._error_filter.filter(filter(first_declared_in, errors)) |
+ |
+ def _clean_up_error(self, error): |
+ """Reverse the effects that funky <include> preprocessing steps have on |
+ errors messages. |
+ |
+ Args: |
+ error: A Closure compiler error (2 line string with error and source). |
+ |
+ Return: |
+ The fixed up error string. |
+ """ |
+ expanded_file = self._expanded_file |
+ fixed = re.sub("%s:(\d+)" % expanded_file, self._get_line_number, error) |
+ return fixed.replace(expanded_file, os.path.abspath(self._file_arg)) |
+ |
+ def _format_errors(self, errors): |
+ """Formats Closure compiler errors to easily spot compiler output. |
+ |
+ Args: |
+ errors: A list of strings extracted from the Closure compiler's output. |
+ |
+ Returns: |
+ A formatted output string. |
+ """ |
+ contents = "\n## ".join("\n\n".join(errors).splitlines()) |
+ return "## %s" % contents if contents else "" |
+ |
+ def _create_temp_file(self, contents): |
+ """Creates an owned temporary file with |contents|. |
+ |
+ Args: |
+ content: A string of the file contens to write to a temporary file. |
+ |
+ Return: |
+ The filepath of the newly created, written, and closed temporary file. |
+ """ |
+ with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file: |
+ self._temp_files.append(tmp_file.name) |
+ tmp_file.write(contents) |
+ return tmp_file.name |
+ |
+ def _run_js_check(self, sources, out_file, externs=None, |
+ closure_args=None): |
+ """Check |sources| for type errors. |
+ |
+ Args: |
+ sources: Files to check. |
+ out_file: A file where the compiled output is written to. |
+ externs: @extern files that inform the compiler about custom globals. |
+ closure_args: Arguments passed directly to the Closure compiler. |
+ |
+ Returns: |
+ (errors, stderr) A parsed list of errors (strings) found by the compiler |
+ and the raw stderr (as a string). |
+ """ |
+ args = ["--js=%s" % s for s in sources] |
+ |
+ assert out_file |
+ |
+ out_dir = os.path.dirname(out_file) |
+ if not os.path.exists(out_dir): |
+ os.makedirs(out_dir) |
+ |
+ checks_only = 'checks_only' in closure_args |
+ |
+ if not checks_only: |
+ args += ["--js_output_file=%s" % out_file] |
+ args += ["--create_source_map=%s" % (self._MAP_FILE_FORMAT % out_file)] |
+ |
+ args += ["--externs=%s" % e for e in externs or []] |
+ |
+ closure_args = closure_args or [] |
+ closure_args += ["summary_detail_level=3", "continue_after_errors"] |
+ args += ["--%s" % arg for arg in closure_args] |
+ |
+ self._log_debug("Args: %s" % " ".join(args)) |
+ |
+ _, stderr = self._run_jar(self._compiler_jar, args) |
+ |
+ errors = stderr.strip().split("\n\n") |
+ maybe_summary = errors.pop() |
+ |
+ if re.search(".*error.*warning.*typed", maybe_summary): |
+ self._log_debug("Summary: %s" % maybe_summary) |
+ else: |
+ # Not a summary. Running the jar failed. Bail. |
+ self._log_error(stderr) |
+ self._nuke_temp_files() |
+ sys.exit(1) |
+ |
+ if errors: |
+ if os.path.exists(out_file): |
+ os.remove(out_file) |
+ if os.path.exists(self._MAP_FILE_FORMAT % out_file): |
+ os.remove(self._MAP_FILE_FORMAT % out_file) |
+ elif checks_only: |
+ # Compile succeeded but --checks_only disables --js_output_file from |
+ # actually writing a file. Write a file ourselves so incremental builds |
+ # still work. |
+ with open(out_file, 'w') as f: |
+ f.write('') |
+ |
+ return errors, stderr |
+ |
+ def check(self, source_file, out_file=None, depends=None, externs=None, |
+ closure_args=None): |
+ """Closure compiler |source_file| while checking for errors. |
+ |
+ Args: |
+ source_file: A file to check. |
+ out_file: A file where the compiled output is written to. |
+ depends: Files that |source_file| requires to run (e.g. earlier <script>). |
+ externs: @extern files that inform the compiler about custom globals. |
+ closure_args: Arguments passed directly to the Closure compiler. |
+ |
+ Returns: |
+ (found_errors, stderr) A boolean indicating whether errors were found and |
+ the raw Closure compiler stderr (as a string). |
+ """ |
+ self._log_debug("FILE: %s" % source_file) |
+ |
+ if source_file.endswith("_externs.js"): |
+ self._log_debug("Skipping externs: %s" % source_file) |
+ return |
+ |
+ self._file_arg = source_file |
+ |
+ cwd, tmp_dir = os.getcwd(), tempfile.gettempdir() |
+ rel_path = lambda f: os.path.join(os.path.relpath(cwd, tmp_dir), f) |
+ |
+ depends = depends or [] |
+ includes = [rel_path(f) for f in depends + [source_file]] |
+ contents = ['<include src="%s">' % i for i in includes] |
+ meta_file = self._create_temp_file("\n".join(contents)) |
+ self._log_debug("Meta file: %s" % meta_file) |
+ |
+ self._processor = processor.Processor(meta_file) |
+ self._expanded_file = self._create_temp_file(self._processor.contents) |
+ self._log_debug("Expanded file: %s" % self._expanded_file) |
+ |
+ errors, stderr = self._run_js_check([self._expanded_file], |
+ out_file=out_file, externs=externs, |
+ closure_args=closure_args) |
+ filtered_errors = self._filter_errors(errors) |
+ cleaned_errors = map(self._clean_up_error, filtered_errors) |
+ output = self._format_errors(cleaned_errors) |
+ |
+ if cleaned_errors: |
+ prefix = "\n" if output else "" |
+ self._log_error("Error in: %s%s%s" % (source_file, prefix, output)) |
+ elif output: |
+ self._log_debug("Output: %s" % output) |
+ |
+ self._nuke_temp_files() |
+ return bool(cleaned_errors), stderr |
+ |
+ def check_multiple(self, sources, out_file=None, externs=None, |
+ closure_args=None): |
+ """Closure compile a set of files and check for errors. |
+ |
+ Args: |
+ sources: An array of files to check. |
+ out_file: A file where the compiled output is written to. |
+ externs: @extern files that inform the compiler about custom globals. |
+ closure_args: Arguments passed directly to the Closure compiler. |
+ |
+ Returns: |
+ (found_errors, stderr) A boolean indicating whether errors were found and |
+ the raw Closure Compiler stderr (as a string). |
+ """ |
+ errors, stderr = self._run_js_check(sources, out_file=out_file, |
+ externs=externs, |
+ closure_args=closure_args) |
+ self._nuke_temp_files() |
+ return bool(errors), stderr |
+ |
+ |
+if __name__ == "__main__": |
+ parser = argparse.ArgumentParser( |
+ description="Typecheck JavaScript using Closure compiler") |
+ parser.add_argument("sources", nargs=argparse.ONE_OR_MORE, |
+ help="Path to a source file to typecheck") |
+ single_file_group = parser.add_mutually_exclusive_group() |
+ single_file_group.add_argument("--single_file", dest="single_file", |
+ action="store_true", |
+ help="Process each source file individually") |
+ # TODO(twellington): remove --no_single_file and use len(opts.sources). |
+ single_file_group.add_argument("--no_single_file", dest="single_file", |
+ action="store_false", |
+ help="Process all source files as a group") |
+ parser.add_argument("-d", "--depends", nargs=argparse.ZERO_OR_MORE) |
+ parser.add_argument("-e", "--externs", nargs=argparse.ZERO_OR_MORE) |
+ parser.add_argument("-o", "--out_file", required=True, |
+ help="A file where the compiled output is written to") |
+ parser.add_argument("-c", "--closure_args", nargs=argparse.ZERO_OR_MORE, |
+ help="Arguments passed directly to the Closure compiler") |
+ parser.add_argument("-v", "--verbose", action="store_true", |
+ help="Show more information as this script runs") |
+ |
+ parser.set_defaults(single_file=True) |
+ opts = parser.parse_args() |
+ |
+ depends = opts.depends or [] |
+ # TODO(devlin): should we run normpath() on this first and/or do this for |
+ # depends as well? |
+ externs = set(opts.externs or []) |
+ sources = set(opts.sources) |
+ |
+ externs.add(os.path.join(_CURRENT_DIR, "externs", "polymer-1.0.js")) |
+ |
+ checker = Checker(verbose=opts.verbose) |
+ if opts.single_file: |
+ for source in sources: |
+ # Normalize source to the current directory. |
+ source = os.path.normpath(os.path.join(os.getcwd(), source)) |
+ depends, externs = build.inputs.resolve_recursive_dependencies( |
+ source, depends, externs) |
+ |
+ found_errors, _ = checker.check(source, out_file=opts.out_file, |
+ depends=depends, externs=externs, |
+ closure_args=opts.closure_args) |
+ if found_errors: |
+ sys.exit(1) |
+ else: |
+ found_errors, stderr = checker.check_multiple( |
+ sources, |
+ out_file=opts.out_file, |
+ externs=externs, |
+ closure_args=opts.closure_args) |
+ if found_errors: |
+ print stderr |
+ sys.exit(1) |