|  | #!/usr/bin/python | 
|  | # Copyright 2015 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 processor | 
|  |  | 
|  |  | 
|  | _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" | 
|  |  | 
|  | _POLYMER_EXTERNS = os.path.join(_CURRENT_DIR, "externs", "polymer-1.0.js") | 
|  |  | 
|  | 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._target = None | 
|  | self._temp_files = [] | 
|  | self._verbose = verbose | 
|  |  | 
|  | 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 _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. | 
|  | """ | 
|  | assert self._target | 
|  | assert self._expanded_file | 
|  | 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._target)) | 
|  |  | 
|  | 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 check(self, sources, out_file, closure_args=None, | 
|  | custom_sources=False, custom_includes=False): | 
|  | """Closure compile |sources| while checking for errors. | 
|  |  | 
|  | Args: | 
|  | sources: Files to check. sources[0] is the typically the target file. | 
|  | sources[1:] are externs and dependencies in topological order. Order | 
|  | is not guaranteed if custom_sources is True. | 
|  | out_file: A file where the compiled output is written to. | 
|  | closure_args: Arguments passed directly to the Closure compiler. | 
|  | custom_sources: Whether |sources| was customized by the target (e.g. not | 
|  | in GYP dependency order). | 
|  |  | 
|  | Returns: | 
|  | (found_errors, stderr) A boolean indicating whether errors were found and | 
|  | the raw Closure compiler stderr (as a string). | 
|  | """ | 
|  | is_extern = lambda f: 'externs' in f | 
|  | externs_and_deps = [self._POLYMER_EXTERNS] | 
|  |  | 
|  | if custom_sources: | 
|  | if custom_includes: | 
|  | # TODO(dbeam): this is fairly hacky. Can we just remove custom_sources | 
|  | # soon when all the things kept on life support using it die? | 
|  | self._target = sources.pop() | 
|  | externs_and_deps += sources | 
|  | else: | 
|  | self._target = sources[0] | 
|  | externs_and_deps += sources[1:] | 
|  |  | 
|  | externs = filter(is_extern, externs_and_deps) | 
|  | deps = filter(lambda f: not is_extern(f), externs_and_deps) | 
|  |  | 
|  | assert externs or deps or self._target | 
|  |  | 
|  | self._log_debug("Externs: %s" % externs) | 
|  | self._log_debug("Dependencies: %s" % deps) | 
|  | self._log_debug("Target: %s" % self._target) | 
|  |  | 
|  | js_args = deps + ([self._target] if self._target else []) | 
|  |  | 
|  | process_includes = custom_includes or not custom_sources | 
|  | if process_includes: | 
|  | # TODO(dbeam): compiler.jar automatically detects "@externs" in a --js arg | 
|  | # and moves these files to a different AST tree. However, because we use | 
|  | # one big funky <include> meta-file, it thinks all the code is one big | 
|  | # externs. Just use --js when <include> dies. | 
|  |  | 
|  | cwd, tmp_dir = os.getcwd(), tempfile.gettempdir() | 
|  | rel_path = lambda f: os.path.join(os.path.relpath(cwd, tmp_dir), f) | 
|  | contents = ['<include src="%s">' % rel_path(f) for f in js_args] | 
|  | 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) | 
|  |  | 
|  | js_args = [self._expanded_file] | 
|  |  | 
|  | closure_args = closure_args or [] | 
|  | closure_args += ["summary_detail_level=3", "continue_after_errors"] | 
|  |  | 
|  | args = ["--externs=%s" % e for e in externs] + \ | 
|  | ["--js=%s" % s for s in js_args] + \ | 
|  | ["--%s" % arg for arg in closure_args] | 
|  |  | 
|  | 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)] | 
|  |  | 
|  | self._log_debug("Args: %s" % " ".join(args)) | 
|  |  | 
|  | return_code, stderr = self.run_jar(self._compiler_jar, args) | 
|  |  | 
|  | errors = stderr.strip().split("\n\n") | 
|  | maybe_summary = errors.pop() | 
|  |  | 
|  | summary = re.search("(?P<error_count>\d+).*error.*warning", maybe_summary) | 
|  | if 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 summary.group('error_count') != "0": | 
|  | 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 and return_code == 0: | 
|  | # 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('') | 
|  |  | 
|  | if process_includes: | 
|  | errors = map(self._clean_up_error, errors) | 
|  | output = self._format_errors(errors) | 
|  |  | 
|  | if errors: | 
|  | prefix = "\n" if output else "" | 
|  | self._log_error("Error in: %s%s%s" % (self._target, prefix, output)) | 
|  | elif output: | 
|  | self._log_debug("Output: %s" % output) | 
|  |  | 
|  | self._nuke_temp_files() | 
|  | return bool(errors) or return_code > 0, 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") | 
|  | parser.add_argument("--custom_sources", action="store_true", | 
|  | help="Whether this rules has custom sources.") | 
|  | parser.add_argument("--custom_includes", action="store_true", | 
|  | help="If present, <include>s are processed when" | 
|  | "using --custom_files.") | 
|  | 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") | 
|  | opts = parser.parse_args() | 
|  |  | 
|  | checker = Checker(verbose=opts.verbose) | 
|  |  | 
|  | found_errors, stderr = checker.check(opts.sources, out_file=opts.out_file, | 
|  | closure_args=opts.closure_args, | 
|  | custom_sources=opts.custom_sources, | 
|  | custom_includes=opts.custom_includes) | 
|  |  | 
|  | if found_errors: | 
|  | if opts.custom_sources: | 
|  | print stderr | 
|  | sys.exit(1) |