Fix broken path in extensions/common/PRESUBMIT.py
[chromium-blink-merge.git] / third_party / closure_compiler / compile.py
blobb40dcfc962f0dae97c82b16825b4604e2f056f15
1 #!/usr/bin/python
2 # Copyright 2014 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Runs Closure compiler on JavaScript files to check for errors and produce
7 minified output."""
9 import argparse
10 import os
11 import re
12 import subprocess
13 import sys
14 import tempfile
16 import build.inputs
17 import processor
18 import error_filter
21 class Checker(object):
22 """Runs the Closure compiler on given source files to typecheck them
23 and produce minified output."""
25 _COMMON_CLOSURE_ARGS = [
26 "--accept_const_keyword",
27 "--jscomp_error=accessControls",
28 "--jscomp_error=ambiguousFunctionDecl",
29 "--jscomp_error=checkStructDictInheritance",
30 "--jscomp_error=checkTypes",
31 "--jscomp_error=checkVars",
32 "--jscomp_error=constantProperty",
33 "--jscomp_error=deprecated",
34 "--jscomp_error=externsValidation",
35 "--jscomp_error=globalThis",
36 "--jscomp_error=invalidCasts",
37 "--jscomp_error=missingProperties",
38 "--jscomp_error=missingReturn",
39 "--jscomp_error=nonStandardJsDocs",
40 "--jscomp_error=suspiciousCode",
41 "--jscomp_error=undefinedNames",
42 "--jscomp_error=undefinedVars",
43 "--jscomp_error=unknownDefines",
44 "--jscomp_error=uselessCode",
45 "--jscomp_error=visibility",
46 "--language_in=ECMASCRIPT5_STRICT",
47 "--summary_detail_level=3",
48 "--compilation_level=SIMPLE_OPTIMIZATIONS",
49 "--source_map_format=V3",
52 # These are the extra flags used when compiling in strict mode.
53 # Flags that are normally disabled are turned on for strict mode.
54 _STRICT_CLOSURE_ARGS = [
55 "--jscomp_error=reportUnknownTypes",
56 "--jscomp_error=duplicate",
57 "--jscomp_error=misplacedTypeAnnotation",
60 _DISABLED_CLOSURE_ARGS = [
61 # TODO(dbeam): happens when the same file is <include>d multiple times.
62 "--jscomp_off=duplicate",
63 # TODO(fukino): happens when cr.defineProperty() has a type annotation.
64 # Avoiding parse-time warnings needs 2 pass compiling. crbug.com/421562.
65 "--jscomp_off=misplacedTypeAnnotation",
68 _JAR_COMMAND = [
69 "java",
70 "-jar",
71 "-Xms1024m",
72 "-client",
73 "-XX:+TieredCompilation"
76 _MAP_FILE_FORMAT = "%s.map"
78 def __init__(self, verbose=False, strict=False):
79 """
80 Args:
81 verbose: Whether this class should output diagnostic messages.
82 strict: Whether the Closure Compiler should be invoked more strictly.
83 """
84 current_dir = os.path.join(os.path.dirname(__file__))
85 self._runner_jar = os.path.join(current_dir, "runner", "runner.jar")
86 self._temp_files = []
87 self._verbose = verbose
88 self._strict = strict
89 self._error_filter = error_filter.PromiseErrorFilter()
91 def _nuke_temp_files(self):
92 """Deletes any temp files this class knows about."""
93 if not self._temp_files:
94 return
96 self._log_debug("Deleting temp files: %s" % ", ".join(self._temp_files))
97 for f in self._temp_files:
98 os.remove(f)
99 self._temp_files = []
101 def _log_debug(self, msg, error=False):
102 """Logs |msg| to stdout if --verbose/-v is passed when invoking this script.
104 Args:
105 msg: A debug message to log.
107 if self._verbose:
108 print "(INFO) %s" % msg
110 def _log_error(self, msg):
111 """Logs |msg| to stderr regardless of --flags.
113 Args:
114 msg: An error message to log.
116 print >> sys.stderr, "(ERROR) %s" % msg
118 def _common_args(self):
119 """Returns an array of the common closure compiler args."""
120 if self._strict:
121 return self._COMMON_CLOSURE_ARGS + self._STRICT_CLOSURE_ARGS
122 return self._COMMON_CLOSURE_ARGS + self._DISABLED_CLOSURE_ARGS
124 def _run_jar(self, jar, args):
125 """Runs a .jar from the command line with arguments.
127 Args:
128 jar: A file path to a .jar file
129 args: A list of command line arguments to be passed when running the .jar.
131 Return:
132 (exit_code, stderr) The exit code of the command (e.g. 0 for success) and
133 the stderr collected while running |jar| (as a string).
135 shell_command = " ".join(self._JAR_COMMAND + [jar] + args)
136 self._log_debug("Running jar: %s" % shell_command)
138 devnull = open(os.devnull, "w")
139 kwargs = {"stdout": devnull, "stderr": subprocess.PIPE, "shell": True}
140 process = subprocess.Popen(shell_command, **kwargs)
141 _, stderr = process.communicate()
142 return process.returncode, stderr
144 def _get_line_number(self, match):
145 """When chrome is built, it preprocesses its JavaScript from:
147 <include src="blah.js">
148 alert(1);
152 /* contents of blah.js inlined */
153 alert(1);
155 Because Closure Compiler requires this inlining already be done (as
156 <include> isn't valid JavaScript), this script creates temporary files to
157 expand all the <include>s.
159 When type errors are hit in temporary files, a developer doesn't know the
160 original source location to fix. This method maps from /tmp/file:300 back to
161 /original/source/file:100 so fixing errors is faster for developers.
163 Args:
164 match: A re.MatchObject from matching against a line number regex.
166 Returns:
167 The fixed up /file and :line number.
169 real_file = self._processor.get_file_from_line(match.group(1))
170 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number)
172 def _filter_errors(self, errors):
173 """Removes some extraneous errors. For example, we ignore:
175 Variable x first declared in /tmp/expanded/file
177 Because it's just a duplicated error (it'll only ever show up 2+ times).
178 We also ignore Promose-based errors:
180 found : function (VolumeInfo): (Promise<(DirectoryEntry|null)>|null)
181 required: (function (Promise<VolumeInfo>): ?|null|undefined)
183 as templates don't work with Promises in all cases yet. See
184 https://github.com/google/closure-compiler/issues/715 for details.
186 Args:
187 errors: A list of string errors extracted from Closure Compiler output.
189 Return:
190 A slimmer, sleeker list of relevant errors (strings).
192 first_declared_in = lambda e: " first declared in " not in e
193 return self._error_filter.filter(filter(first_declared_in, errors))
195 def _clean_up_error(self, error):
196 """Reverse the effects that funky <include> preprocessing steps have on
197 errors messages.
199 Args:
200 error: A Closure compiler error (2 line string with error and source).
202 Return:
203 The fixed up error string.
205 expanded_file = self._expanded_file
206 fixed = re.sub("%s:(\d+)" % expanded_file, self._get_line_number, error)
207 return fixed.replace(expanded_file, os.path.abspath(self._file_arg))
209 def _format_errors(self, errors):
210 """Formats Closure compiler errors to easily spot compiler output.
212 Args:
213 errors: A list of strings extracted from the Closure compiler's output.
215 Returns:
216 A formatted output string.
218 contents = "\n## ".join("\n\n".join(errors).splitlines())
219 return "## %s" % contents if contents else ""
221 def _create_temp_file(self, contents):
222 """Creates an owned temporary file with |contents|.
224 Args:
225 content: A string of the file contens to write to a temporary file.
227 Return:
228 The filepath of the newly created, written, and closed temporary file.
230 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file:
231 self._temp_files.append(tmp_file.name)
232 tmp_file.write(contents)
233 return tmp_file.name
235 def _run_js_check(self, sources, out_file=None, externs=None,
236 output_wrapper=None):
237 """Check |sources| for type errors.
239 Args:
240 sources: Files to check.
241 out_file: A file where the compiled output is written to.
242 externs: @extern files that inform the compiler about custom globals.
243 output_wrapper: Wraps output into this string at the place denoted by the
244 marker token %output%.
246 Returns:
247 (errors, stderr) A parsed list of errors (strings) found by the compiler
248 and the raw stderr (as a string).
250 args = ["--js=%s" % s for s in sources]
252 if out_file:
253 args += ["--js_output_file=%s" % out_file]
254 args += ["--create_source_map=%s" % (self._MAP_FILE_FORMAT % out_file)]
256 if externs:
257 args += ["--externs=%s" % e for e in externs]
259 if output_wrapper:
260 args += ['--output_wrapper="%s"' % output_wrapper]
262 args_file_content = " %s" % " ".join(self._common_args() + args)
263 self._log_debug("Args: %s" % args_file_content.strip())
265 args_file = self._create_temp_file(args_file_content)
266 self._log_debug("Args file: %s" % args_file)
268 runner_args = ["--compiler-args-file=%s" % args_file]
269 _, stderr = self._run_jar(self._runner_jar, runner_args)
271 errors = stderr.strip().split("\n\n")
272 maybe_summary = errors.pop()
274 if re.search(".*error.*warning.*typed", maybe_summary):
275 self._log_debug("Summary: %s" % maybe_summary)
276 else:
277 # Not a summary. Running the jar failed. Bail.
278 self._log_error(stderr)
279 self._nuke_temp_files()
280 sys.exit(1)
282 if errors and out_file:
283 if os.path.exists(out_file):
284 os.remove(out_file)
285 if os.path.exists(self._MAP_FILE_FORMAT % out_file):
286 os.remove(self._MAP_FILE_FORMAT % out_file)
288 return errors, stderr
290 def check(self, source_file, out_file=None, depends=None, externs=None,
291 output_wrapper=None):
292 """Closure compiler |source_file| while checking for errors.
294 Args:
295 source_file: A file to check.
296 out_file: A file where the compiled output is written to.
297 depends: Files that |source_file| requires to run (e.g. earlier <script>).
298 externs: @extern files that inform the compiler about custom globals.
299 output_wrapper: Wraps output into this string at the place denoted by the
300 marker token %output%.
302 Returns:
303 (found_errors, stderr) A boolean indicating whether errors were found and
304 the raw Closure compiler stderr (as a string).
306 self._log_debug("FILE: %s" % source_file)
308 if source_file.endswith("_externs.js"):
309 self._log_debug("Skipping externs: %s" % source_file)
310 return
312 self._file_arg = source_file
314 cwd, tmp_dir = os.getcwd(), tempfile.gettempdir()
315 rel_path = lambda f: os.path.join(os.path.relpath(cwd, tmp_dir), f)
317 depends = depends or []
318 includes = [rel_path(f) for f in depends + [source_file]]
319 contents = ['<include src="%s">' % i for i in includes]
320 meta_file = self._create_temp_file("\n".join(contents))
321 self._log_debug("Meta file: %s" % meta_file)
323 self._processor = processor.Processor(meta_file)
324 self._expanded_file = self._create_temp_file(self._processor.contents)
325 self._log_debug("Expanded file: %s" % self._expanded_file)
327 errors, stderr = self._run_js_check([self._expanded_file],
328 out_file=out_file, externs=externs,
329 output_wrapper=output_wrapper)
330 filtered_errors = self._filter_errors(errors)
331 cleaned_errors = map(self._clean_up_error, filtered_errors)
332 output = self._format_errors(cleaned_errors)
334 if cleaned_errors:
335 prefix = "\n" if output else ""
336 self._log_error("Error in: %s%s%s" % (source_file, prefix, output))
337 elif output:
338 self._log_debug("Output: %s" % output)
340 self._nuke_temp_files()
341 return bool(cleaned_errors), stderr
343 def check_multiple(self, sources, out_file=None, output_wrapper=None):
344 """Closure compile a set of files and check for errors.
346 Args:
347 sources: An array of files to check.
348 out_file: A file where the compiled output is written to.
349 output_wrapper: Wraps output into this string at the place denoted by the
350 marker token %output%.
352 Returns:
353 (found_errors, stderr) A boolean indicating whether errors were found and
354 the raw Closure Compiler stderr (as a string).
356 errors, stderr = self._run_js_check(sources, out_file=out_file,
357 output_wrapper=output_wrapper)
358 self._nuke_temp_files()
359 return bool(errors), stderr
362 if __name__ == "__main__":
363 parser = argparse.ArgumentParser(
364 description="Typecheck JavaScript using Closure compiler")
365 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE,
366 help="Path to a source file to typecheck")
367 single_file_group = parser.add_mutually_exclusive_group()
368 single_file_group.add_argument("--single-file", dest="single_file",
369 action="store_true",
370 help="Process each source file individually")
371 single_file_group.add_argument("--no-single-file", dest="single_file",
372 action="store_false",
373 help="Process all source files as a group")
374 parser.add_argument("-d", "--depends", nargs=argparse.ZERO_OR_MORE)
375 parser.add_argument("-e", "--externs", nargs=argparse.ZERO_OR_MORE)
376 parser.add_argument("-o", "--out_file",
377 help="A file where the compiled output is written to")
378 parser.add_argument("-w", "--output_wrapper",
379 help="Wraps output into this string at the place"
380 + " denoted by the marker token %output%")
381 parser.add_argument("-v", "--verbose", action="store_true",
382 help="Show more information as this script runs")
383 parser.add_argument("--strict", action="store_true",
384 help="Enable strict type checking")
385 parser.add_argument("--success-stamp",
386 help="Timestamp file to update upon success")
388 parser.set_defaults(single_file=True, strict=False)
389 opts = parser.parse_args()
391 depends = opts.depends or []
392 externs = opts.externs or set()
394 if opts.out_file:
395 out_dir = os.path.dirname(opts.out_file)
396 if not os.path.exists(out_dir):
397 os.makedirs(out_dir)
399 checker = Checker(verbose=opts.verbose, strict=opts.strict)
400 if opts.single_file:
401 for source in opts.sources:
402 depends, externs = build.inputs.resolve_recursive_dependencies(
403 source, depends, externs)
404 found_errors, _ = checker.check(source, out_file=opts.out_file,
405 depends=depends, externs=externs,
406 output_wrapper=opts.output_wrapper)
407 if found_errors:
408 sys.exit(1)
409 else:
410 found_errors, stderr = checker.check_multiple(
411 opts.sources,
412 out_file=opts.out_file,
413 output_wrapper=opts.output_wrapper)
414 if found_errors:
415 print stderr
416 sys.exit(1)
418 if opts.success_stamp:
419 with open(opts.success_stamp, "w"):
420 os.utime(opts.success_stamp, None)