OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 # Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Runs Closure compiler on JavaScript files to check for errors and produce | 6 """Runs Closure compiler on JavaScript files to check for errors and produce |
7 minified output.""" | 7 minified output.""" |
8 | 8 |
9 import argparse | 9 import argparse |
10 import os | 10 import os |
11 import re | 11 import re |
12 import subprocess | 12 import subprocess |
13 import sys | 13 import sys |
14 import tempfile | 14 import tempfile |
15 | 15 |
16 import build.inputs | 16 import build.inputs |
17 import processor | 17 import processor |
18 import error_filter | 18 import error_filter |
19 | 19 |
20 | 20 |
21 _CURRENT_DIR = os.path.join(os.path.dirname(__file__)) | 21 _CURRENT_DIR = os.path.join(os.path.dirname(__file__)) |
22 | 22 |
23 | 23 |
24 class Checker(object): | 24 class Checker(object): |
25 """Runs the Closure compiler on given source files to typecheck them | 25 """Runs the Closure compiler on given source files to typecheck them |
26 and produce minified output.""" | 26 and produce minified output.""" |
27 | 27 |
28 _COMMON_JSCOMP_ERRORS = [ | |
29 "accessControls", | |
30 "ambiguousFunctionDecl", | |
31 "checkStructDictInheritance", | |
32 "checkTypes", | |
33 "checkVars", | |
34 "constantProperty", | |
35 "deprecated", | |
36 "externsValidation", | |
37 "globalThis", | |
38 "invalidCasts", | |
39 "missingProperties", | |
40 "missingReturn", | |
41 "nonStandardJsDocs", | |
42 "suspiciousCode", | |
43 "undefinedNames", | |
44 "undefinedVars", | |
45 "unknownDefines", | |
46 "uselessCode", | |
47 "visibility", | |
48 ] | |
49 | |
50 # Extra @jsDocAnnotations used when compiling polymer code. | |
51 _POLYMER_EXTRA_ANNOTATIONS = [ | |
52 "attribute", | |
53 "status", | |
54 "element", | |
55 "homepage", | |
56 "submodule", | |
57 "group", | |
58 ] | |
59 | |
60 _COMMON_CLOSURE_ARGS = [ | |
61 "--accept_const_keyword", | |
62 "--language_in=ECMASCRIPT5_STRICT", | |
63 "--summary_detail_level=3", | |
64 "--compilation_level=SIMPLE_OPTIMIZATIONS", | |
65 "--source_map_format=V3", | |
66 "--polymer_pass", | |
67 ] + [ | |
68 "--jscomp_error=%s" % err for err in _COMMON_JSCOMP_ERRORS | |
69 ] + [ | |
70 "--extra_annotation_name=%s" % a for a in _POLYMER_EXTRA_ANNOTATIONS | |
71 ] | |
72 | |
73 # These are the extra flags used when compiling in strict mode. | |
74 # Flags that are normally disabled are turned on for strict mode. | |
75 _STRICT_CLOSURE_ARGS = [ | |
76 "--jscomp_error=reportUnknownTypes", | |
77 "--jscomp_error=duplicate", | |
78 "--jscomp_error=misplacedTypeAnnotation", | |
79 ] | |
80 | |
81 _DISABLED_CLOSURE_ARGS = [ | |
82 # TODO(dbeam): happens when the same file is <include>d multiple times. | |
83 "--jscomp_off=duplicate", | |
84 # TODO(fukino): happens when cr.defineProperty() has a type annotation. | |
85 # Avoiding parse-time warnings needs 2 pass compiling. crbug.com/421562. | |
86 "--jscomp_off=misplacedTypeAnnotation", | |
87 ] | |
88 | |
89 _JAR_COMMAND = [ | 28 _JAR_COMMAND = [ |
90 "java", | 29 "java", |
91 "-jar", | 30 "-jar", |
92 "-Xms1024m", | 31 "-Xms1024m", |
93 "-client", | 32 "-client", |
94 "-XX:+TieredCompilation" | 33 "-XX:+TieredCompilation" |
95 ] | 34 ] |
96 | 35 |
97 _MAP_FILE_FORMAT = "%s.map" | 36 _MAP_FILE_FORMAT = "%s.map" |
98 | 37 |
(...skipping 29 matching lines...) Expand all Loading... |
128 print "(INFO) %s" % msg | 67 print "(INFO) %s" % msg |
129 | 68 |
130 def _log_error(self, msg): | 69 def _log_error(self, msg): |
131 """Logs |msg| to stderr regardless of --flags. | 70 """Logs |msg| to stderr regardless of --flags. |
132 | 71 |
133 Args: | 72 Args: |
134 msg: An error message to log. | 73 msg: An error message to log. |
135 """ | 74 """ |
136 print >> sys.stderr, "(ERROR) %s" % msg | 75 print >> sys.stderr, "(ERROR) %s" % msg |
137 | 76 |
138 def _common_args(self): | |
139 """Returns an array of the common closure compiler args.""" | |
140 if self._strict: | |
141 return self._COMMON_CLOSURE_ARGS + self._STRICT_CLOSURE_ARGS | |
142 return self._COMMON_CLOSURE_ARGS + self._DISABLED_CLOSURE_ARGS | |
143 | |
144 def _run_jar(self, jar, args): | 77 def _run_jar(self, jar, args): |
145 """Runs a .jar from the command line with arguments. | 78 """Runs a .jar from the command line with arguments. |
146 | 79 |
147 Args: | 80 Args: |
148 jar: A file path to a .jar file | 81 jar: A file path to a .jar file |
149 args: A list of command line arguments to be passed when running the .jar. | 82 args: A list of command line arguments to be passed when running the .jar. |
150 | 83 |
151 Return: | 84 Return: |
152 (exit_code, stderr) The exit code of the command (e.g. 0 for success) and | 85 (exit_code, stderr) The exit code of the command (e.g. 0 for success) and |
153 the stderr collected while running |jar| (as a string). | 86 the stderr collected while running |jar| (as a string). |
(...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
246 | 179 |
247 Return: | 180 Return: |
248 The filepath of the newly created, written, and closed temporary file. | 181 The filepath of the newly created, written, and closed temporary file. |
249 """ | 182 """ |
250 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file: | 183 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file: |
251 self._temp_files.append(tmp_file.name) | 184 self._temp_files.append(tmp_file.name) |
252 tmp_file.write(contents) | 185 tmp_file.write(contents) |
253 return tmp_file.name | 186 return tmp_file.name |
254 | 187 |
255 def _run_js_check(self, sources, out_file=None, externs=None, | 188 def _run_js_check(self, sources, out_file=None, externs=None, |
256 output_wrapper=None): | 189 closure_args=None): |
257 """Check |sources| for type errors. | 190 """Check |sources| for type errors. |
258 | 191 |
259 Args: | 192 Args: |
260 sources: Files to check. | 193 sources: Files to check. |
261 out_file: A file where the compiled output is written to. | 194 out_file: A file where the compiled output is written to. |
262 externs: @extern files that inform the compiler about custom globals. | 195 externs: @extern files that inform the compiler about custom globals. |
263 output_wrapper: Wraps output into this string at the place denoted by the | 196 closure_args: Arguments passed directly to the Closure compiler. |
264 marker token %output%. | |
265 | 197 |
266 Returns: | 198 Returns: |
267 (errors, stderr) A parsed list of errors (strings) found by the compiler | 199 (errors, stderr) A parsed list of errors (strings) found by the compiler |
268 and the raw stderr (as a string). | 200 and the raw stderr (as a string). |
269 """ | 201 """ |
270 args = ["--js=%s" % s for s in sources] | 202 args = ["--js=%s" % s for s in sources] |
271 | 203 |
272 if out_file: | 204 if out_file: |
273 out_dir = os.path.dirname(out_file) | 205 out_dir = os.path.dirname(out_file) |
274 if not os.path.exists(out_dir): | 206 if not os.path.exists(out_dir): |
275 os.makedirs(out_dir) | 207 os.makedirs(out_dir) |
276 args += ["--js_output_file=%s" % out_file] | 208 args += ["--js_output_file=%s" % out_file] |
277 args += ["--create_source_map=%s" % (self._MAP_FILE_FORMAT % out_file)] | 209 args += ["--create_source_map=%s" % (self._MAP_FILE_FORMAT % out_file)] |
278 | 210 |
279 if externs: | 211 if externs: |
280 args += ["--externs=%s" % e for e in externs] | 212 args += ["--externs=%s" % e for e in externs] |
281 | 213 |
282 if output_wrapper: | 214 if closure_args: |
283 args += ['--output_wrapper="%s"' % output_wrapper] | 215 for arg in closure_args: |
| 216 args += ["--%s" % arg] |
284 | 217 |
285 args_file_content = " %s" % " ".join(self._common_args() + args) | 218 args_file_content = " %s" % " ".join(args) |
286 self._log_debug("Args: %s" % args_file_content.strip()) | 219 self._log_debug("Args: %s" % args_file_content.strip()) |
287 | 220 |
288 args_file = self._create_temp_file(args_file_content) | 221 args_file = self._create_temp_file(args_file_content) |
289 self._log_debug("Args file: %s" % args_file) | 222 self._log_debug("Args file: %s" % args_file) |
290 | 223 |
291 runner_args = ["--compiler-args-file=%s" % args_file] | 224 runner_args = ["--compiler-args-file=%s" % args_file] |
292 _, stderr = self._run_jar(self._runner_jar, runner_args) | 225 _, stderr = self._run_jar(self._runner_jar, runner_args) |
293 | 226 |
294 errors = stderr.strip().split("\n\n") | 227 errors = stderr.strip().split("\n\n") |
295 maybe_summary = errors.pop() | 228 maybe_summary = errors.pop() |
296 | 229 |
297 if re.search(".*error.*warning.*typed", maybe_summary): | 230 if re.search(".*error.*warning.*typed", maybe_summary): |
298 self._log_debug("Summary: %s" % maybe_summary) | 231 self._log_debug("Summary: %s" % maybe_summary) |
299 else: | 232 else: |
300 # Not a summary. Running the jar failed. Bail. | 233 # Not a summary. Running the jar failed. Bail. |
301 self._log_error(stderr) | 234 self._log_error(stderr) |
302 self._nuke_temp_files() | 235 self._nuke_temp_files() |
303 sys.exit(1) | 236 sys.exit(1) |
304 | 237 |
305 if errors and out_file: | 238 if errors and out_file: |
306 if os.path.exists(out_file): | 239 if os.path.exists(out_file): |
307 os.remove(out_file) | 240 os.remove(out_file) |
308 if os.path.exists(self._MAP_FILE_FORMAT % out_file): | 241 if os.path.exists(self._MAP_FILE_FORMAT % out_file): |
309 os.remove(self._MAP_FILE_FORMAT % out_file) | 242 os.remove(self._MAP_FILE_FORMAT % out_file) |
310 | 243 |
311 return errors, stderr | 244 return errors, stderr |
312 | 245 |
313 def check(self, source_file, out_file=None, depends=None, externs=None, | 246 def check(self, source_file, out_file=None, depends=None, externs=None, |
314 output_wrapper=None): | 247 closure_args=None): |
315 """Closure compiler |source_file| while checking for errors. | 248 """Closure compiler |source_file| while checking for errors. |
316 | 249 |
317 Args: | 250 Args: |
318 source_file: A file to check. | 251 source_file: A file to check. |
319 out_file: A file where the compiled output is written to. | 252 out_file: A file where the compiled output is written to. |
320 depends: Files that |source_file| requires to run (e.g. earlier <script>). | 253 depends: Files that |source_file| requires to run (e.g. earlier <script>). |
321 externs: @extern files that inform the compiler about custom globals. | 254 externs: @extern files that inform the compiler about custom globals. |
322 output_wrapper: Wraps output into this string at the place denoted by the | 255 closure_args: Arguments passed directly to the Closure compiler. |
323 marker token %output%. | |
324 | 256 |
325 Returns: | 257 Returns: |
326 (found_errors, stderr) A boolean indicating whether errors were found and | 258 (found_errors, stderr) A boolean indicating whether errors were found and |
327 the raw Closure compiler stderr (as a string). | 259 the raw Closure compiler stderr (as a string). |
328 """ | 260 """ |
329 self._log_debug("FILE: %s" % source_file) | 261 self._log_debug("FILE: %s" % source_file) |
330 | 262 |
331 if source_file.endswith("_externs.js"): | 263 if source_file.endswith("_externs.js"): |
332 self._log_debug("Skipping externs: %s" % source_file) | 264 self._log_debug("Skipping externs: %s" % source_file) |
333 return | 265 return |
334 | 266 |
335 self._file_arg = source_file | 267 self._file_arg = source_file |
336 | 268 |
337 cwd, tmp_dir = os.getcwd(), tempfile.gettempdir() | 269 cwd, tmp_dir = os.getcwd(), tempfile.gettempdir() |
338 rel_path = lambda f: os.path.join(os.path.relpath(cwd, tmp_dir), f) | 270 rel_path = lambda f: os.path.join(os.path.relpath(cwd, tmp_dir), f) |
339 | 271 |
340 depends = depends or [] | 272 depends = depends or [] |
341 includes = [rel_path(f) for f in depends + [source_file]] | 273 includes = [rel_path(f) for f in depends + [source_file]] |
342 contents = ['<include src="%s">' % i for i in includes] | 274 contents = ['<include src="%s">' % i for i in includes] |
343 meta_file = self._create_temp_file("\n".join(contents)) | 275 meta_file = self._create_temp_file("\n".join(contents)) |
344 self._log_debug("Meta file: %s" % meta_file) | 276 self._log_debug("Meta file: %s" % meta_file) |
345 | 277 |
346 self._processor = processor.Processor(meta_file) | 278 self._processor = processor.Processor(meta_file) |
347 self._expanded_file = self._create_temp_file(self._processor.contents) | 279 self._expanded_file = self._create_temp_file(self._processor.contents) |
348 self._log_debug("Expanded file: %s" % self._expanded_file) | 280 self._log_debug("Expanded file: %s" % self._expanded_file) |
349 | 281 |
350 errors, stderr = self._run_js_check([self._expanded_file], | 282 errors, stderr = self._run_js_check([self._expanded_file], |
351 out_file=out_file, externs=externs, | 283 out_file=out_file, externs=externs, |
352 output_wrapper=output_wrapper) | 284 closure_args=closure_args) |
353 filtered_errors = self._filter_errors(errors) | 285 filtered_errors = self._filter_errors(errors) |
354 cleaned_errors = map(self._clean_up_error, filtered_errors) | 286 cleaned_errors = map(self._clean_up_error, filtered_errors) |
355 output = self._format_errors(cleaned_errors) | 287 output = self._format_errors(cleaned_errors) |
356 | 288 |
357 if cleaned_errors: | 289 if cleaned_errors: |
358 prefix = "\n" if output else "" | 290 prefix = "\n" if output else "" |
359 self._log_error("Error in: %s%s%s" % (source_file, prefix, output)) | 291 self._log_error("Error in: %s%s%s" % (source_file, prefix, output)) |
360 elif output: | 292 elif output: |
361 self._log_debug("Output: %s" % output) | 293 self._log_debug("Output: %s" % output) |
362 | 294 |
363 self._nuke_temp_files() | 295 self._nuke_temp_files() |
364 return bool(cleaned_errors), stderr | 296 return bool(cleaned_errors), stderr |
365 | 297 |
366 def check_multiple(self, sources, out_file=None, output_wrapper=None, | 298 def check_multiple(self, sources, out_file=None, externs=None, |
367 externs=None): | 299 closure_args=None): |
368 """Closure compile a set of files and check for errors. | 300 """Closure compile a set of files and check for errors. |
369 | 301 |
370 Args: | 302 Args: |
371 sources: An array of files to check. | 303 sources: An array of files to check. |
372 out_file: A file where the compiled output is written to. | 304 out_file: A file where the compiled output is written to. |
373 output_wrapper: Wraps output into this string at the place denoted by the | |
374 marker token %output%. | |
375 externs: @extern files that inform the compiler about custom globals. | 305 externs: @extern files that inform the compiler about custom globals. |
| 306 closure_args: Arguments passed directly to the Closure compiler. |
376 | 307 |
377 Returns: | 308 Returns: |
378 (found_errors, stderr) A boolean indicating whether errors were found and | 309 (found_errors, stderr) A boolean indicating whether errors were found and |
379 the raw Closure Compiler stderr (as a string). | 310 the raw Closure Compiler stderr (as a string). |
380 """ | 311 """ |
381 errors, stderr = self._run_js_check(sources, out_file=out_file, | 312 errors, stderr = self._run_js_check(sources, out_file=out_file, |
382 output_wrapper=output_wrapper, | 313 externs=externs, |
383 externs=externs) | 314 closure_args=closure_args) |
384 self._nuke_temp_files() | 315 self._nuke_temp_files() |
385 return bool(errors), stderr | 316 return bool(errors), stderr |
386 | 317 |
387 | 318 |
388 if __name__ == "__main__": | 319 if __name__ == "__main__": |
389 parser = argparse.ArgumentParser( | 320 parser = argparse.ArgumentParser( |
390 description="Typecheck JavaScript using Closure compiler") | 321 description="Typecheck JavaScript using Closure compiler") |
391 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE, | 322 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE, |
392 help="Path to a source file to typecheck") | 323 help="Path to a source file to typecheck") |
393 single_file_group = parser.add_mutually_exclusive_group() | 324 single_file_group = parser.add_mutually_exclusive_group() |
394 single_file_group.add_argument("--single-file", dest="single_file", | 325 single_file_group.add_argument("--single-file", dest="single_file", |
395 action="store_true", | 326 action="store_true", |
396 help="Process each source file individually") | 327 help="Process each source file individually") |
397 single_file_group.add_argument("--no-single-file", dest="single_file", | 328 single_file_group.add_argument("--no-single-file", dest="single_file", |
398 action="store_false", | 329 action="store_false", |
399 help="Process all source files as a group") | 330 help="Process all source files as a group") |
400 parser.add_argument("-d", "--depends", nargs=argparse.ZERO_OR_MORE) | 331 parser.add_argument("-d", "--depends", nargs=argparse.ZERO_OR_MORE) |
401 parser.add_argument("-e", "--externs", nargs=argparse.ZERO_OR_MORE) | 332 parser.add_argument("-e", "--externs", nargs=argparse.ZERO_OR_MORE) |
402 parser.add_argument("-o", "--out_file", | 333 parser.add_argument("-o", "--out-file", dest="out_file", |
403 help="A file where the compiled output is written to") | 334 help="A file where the compiled output is written to") |
404 parser.add_argument("-w", "--output_wrapper", | 335 parser.add_argument("-c", "--closure-args", dest="closure_args", |
405 help="Wraps output into this string at the place" | 336 nargs=argparse.ZERO_OR_MORE, |
406 + " denoted by the marker token %output%") | 337 help="Arguments passed directly to the Closure compiler") |
407 parser.add_argument("-v", "--verbose", action="store_true", | 338 parser.add_argument("-v", "--verbose", action="store_true", |
408 help="Show more information as this script runs") | 339 help="Show more information as this script runs") |
409 parser.add_argument("--strict", action="store_true", | |
410 help="Enable strict type checking") | |
411 parser.add_argument("--success-stamp", | |
412 help="Timestamp file to update upon success") | |
413 | 340 |
414 parser.set_defaults(single_file=True, strict=False) | 341 parser.set_defaults(single_file=True, strict=False) |
415 opts = parser.parse_args() | 342 opts = parser.parse_args() |
416 | 343 |
417 depends = opts.depends or [] | 344 depends = opts.depends or [] |
418 externs = set(opts.externs or []) | 345 externs = set(opts.externs or []) |
419 | 346 |
420 polymer_externs = os.path.join(os.path.dirname(_CURRENT_DIR), 'polymer', | 347 polymer_externs = os.path.join(os.path.dirname(_CURRENT_DIR), 'polymer', |
421 'v1_0', 'components-chromium', | 348 'v1_0', 'components-chromium', |
422 'polymer-externs', 'polymer.externs.js') | 349 'polymer-externs', 'polymer.externs.js') |
423 externs.add(polymer_externs) | 350 externs.add(polymer_externs) |
424 | 351 |
425 checker = Checker(verbose=opts.verbose, strict=opts.strict) | 352 checker = Checker(verbose=opts.verbose, strict=opts.strict) |
426 if opts.single_file: | 353 if opts.single_file: |
427 for source in opts.sources: | 354 for source in opts.sources: |
428 depends, externs = build.inputs.resolve_recursive_dependencies( | 355 depends, externs = build.inputs.resolve_recursive_dependencies( |
429 source, depends, externs) | 356 source, depends, externs) |
430 found_errors, _ = checker.check(source, out_file=opts.out_file, | 357 found_errors, _ = checker.check(source, out_file=opts.out_file, |
431 depends=depends, externs=externs, | 358 depends=depends, externs=externs, |
432 output_wrapper=opts.output_wrapper) | 359 closure_args=opts.closure_args) |
433 if found_errors: | 360 if found_errors: |
434 sys.exit(1) | 361 sys.exit(1) |
435 else: | 362 else: |
436 found_errors, stderr = checker.check_multiple( | 363 found_errors, stderr = checker.check_multiple( |
437 opts.sources, | 364 opts.sources, |
438 out_file=opts.out_file, | 365 out_file=opts.out_file, |
439 output_wrapper=opts.output_wrapper, | 366 externs=externs, |
440 externs=externs) | 367 closure_args=opts.closure_args) |
441 if found_errors: | 368 if found_errors: |
442 print stderr | 369 print stderr |
443 sys.exit(1) | 370 sys.exit(1) |
444 | |
445 if opts.success_stamp: | |
446 with open(opts.success_stamp, "w"): | |
447 os.utime(opts.success_stamp, None) | |
OLD | NEW |