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 args += closure_args |
283 args += ['--output_wrapper="%s"' % output_wrapper] | |
284 | 215 |
285 args_file_content = " %s" % " ".join(self._common_args() + args) | 216 args_file_content = " %s" % " ".join(args) |
286 self._log_debug("Args: %s" % args_file_content.strip()) | 217 self._log_debug("Args: %s" % args_file_content.strip()) |
287 | 218 |
288 args_file = self._create_temp_file(args_file_content) | 219 args_file = self._create_temp_file(args_file_content) |
289 self._log_debug("Args file: %s" % args_file) | 220 self._log_debug("Args file: %s" % args_file) |
290 | 221 |
291 runner_args = ["--compiler-args-file=%s" % args_file] | 222 runner_args = ["--compiler-args-file=%s" % args_file] |
292 _, stderr = self._run_jar(self._runner_jar, runner_args) | 223 _, stderr = self._run_jar(self._runner_jar, runner_args) |
293 | 224 |
294 errors = stderr.strip().split("\n\n") | 225 errors = stderr.strip().split("\n\n") |
295 maybe_summary = errors.pop() | 226 maybe_summary = errors.pop() |
296 | 227 |
297 if re.search(".*error.*warning.*typed", maybe_summary): | 228 if re.search(".*error.*warning.*typed", maybe_summary): |
298 self._log_debug("Summary: %s" % maybe_summary) | 229 self._log_debug("Summary: %s" % maybe_summary) |
299 else: | 230 else: |
300 # Not a summary. Running the jar failed. Bail. | 231 # Not a summary. Running the jar failed. Bail. |
301 self._log_error(stderr) | 232 self._log_error(stderr) |
302 self._nuke_temp_files() | 233 self._nuke_temp_files() |
303 sys.exit(1) | 234 sys.exit(1) |
304 | 235 |
305 if errors and out_file: | 236 if errors and out_file: |
306 if os.path.exists(out_file): | 237 if os.path.exists(out_file): |
307 os.remove(out_file) | 238 os.remove(out_file) |
308 if os.path.exists(self._MAP_FILE_FORMAT % out_file): | 239 if os.path.exists(self._MAP_FILE_FORMAT % out_file): |
309 os.remove(self._MAP_FILE_FORMAT % out_file) | 240 os.remove(self._MAP_FILE_FORMAT % out_file) |
310 | 241 |
311 return errors, stderr | 242 return errors, stderr |
312 | 243 |
313 def check(self, source_file, out_file=None, depends=None, externs=None, | 244 def check(self, source_file, out_file=None, depends=None, externs=None, |
314 output_wrapper=None): | 245 closure_args=None): |
315 """Closure compiler |source_file| while checking for errors. | 246 """Closure compiler |source_file| while checking for errors. |
316 | 247 |
317 Args: | 248 Args: |
318 source_file: A file to check. | 249 source_file: A file to check. |
319 out_file: A file where the compiled output is written to. | 250 out_file: A file where the compiled output is written to. |
320 depends: Files that |source_file| requires to run (e.g. earlier <script>). | 251 depends: Files that |source_file| requires to run (e.g. earlier <script>). |
321 externs: @extern files that inform the compiler about custom globals. | 252 externs: @extern files that inform the compiler about custom globals. |
322 output_wrapper: Wraps output into this string at the place denoted by the | 253 closure_args: Arguments passed directly to the closure compiler. |
323 marker token %output%. | |
324 | 254 |
325 Returns: | 255 Returns: |
326 (found_errors, stderr) A boolean indicating whether errors were found and | 256 (found_errors, stderr) A boolean indicating whether errors were found and |
327 the raw Closure compiler stderr (as a string). | 257 the raw Closure compiler stderr (as a string). |
328 """ | 258 """ |
329 self._log_debug("FILE: %s" % source_file) | 259 self._log_debug("FILE: %s" % source_file) |
330 | 260 |
331 if source_file.endswith("_externs.js"): | 261 if source_file.endswith("_externs.js"): |
332 self._log_debug("Skipping externs: %s" % source_file) | 262 self._log_debug("Skipping externs: %s" % source_file) |
333 return | 263 return |
334 | 264 |
335 self._file_arg = source_file | 265 self._file_arg = source_file |
336 | 266 |
337 cwd, tmp_dir = os.getcwd(), tempfile.gettempdir() | 267 cwd, tmp_dir = os.getcwd(), tempfile.gettempdir() |
338 rel_path = lambda f: os.path.join(os.path.relpath(cwd, tmp_dir), f) | 268 rel_path = lambda f: os.path.join(os.path.relpath(cwd, tmp_dir), f) |
339 | 269 |
340 depends = depends or [] | 270 depends = depends or [] |
341 includes = [rel_path(f) for f in depends + [source_file]] | 271 includes = [rel_path(f) for f in depends + [source_file]] |
342 contents = ['<include src="%s">' % i for i in includes] | 272 contents = ['<include src="%s">' % i for i in includes] |
343 meta_file = self._create_temp_file("\n".join(contents)) | 273 meta_file = self._create_temp_file("\n".join(contents)) |
344 self._log_debug("Meta file: %s" % meta_file) | 274 self._log_debug("Meta file: %s" % meta_file) |
345 | 275 |
346 self._processor = processor.Processor(meta_file) | 276 self._processor = processor.Processor(meta_file) |
347 self._expanded_file = self._create_temp_file(self._processor.contents) | 277 self._expanded_file = self._create_temp_file(self._processor.contents) |
348 self._log_debug("Expanded file: %s" % self._expanded_file) | 278 self._log_debug("Expanded file: %s" % self._expanded_file) |
349 | 279 |
350 errors, stderr = self._run_js_check([self._expanded_file], | 280 errors, stderr = self._run_js_check([self._expanded_file], |
351 out_file=out_file, externs=externs, | 281 out_file=out_file, externs=externs, |
352 output_wrapper=output_wrapper) | 282 closure_args=closure_args) |
353 filtered_errors = self._filter_errors(errors) | 283 filtered_errors = self._filter_errors(errors) |
354 cleaned_errors = map(self._clean_up_error, filtered_errors) | 284 cleaned_errors = map(self._clean_up_error, filtered_errors) |
355 output = self._format_errors(cleaned_errors) | 285 output = self._format_errors(cleaned_errors) |
356 | 286 |
357 if cleaned_errors: | 287 if cleaned_errors: |
358 prefix = "\n" if output else "" | 288 prefix = "\n" if output else "" |
359 self._log_error("Error in: %s%s%s" % (source_file, prefix, output)) | 289 self._log_error("Error in: %s%s%s" % (source_file, prefix, output)) |
360 elif output: | 290 elif output: |
361 self._log_debug("Output: %s" % output) | 291 self._log_debug("Output: %s" % output) |
362 | 292 |
363 self._nuke_temp_files() | 293 self._nuke_temp_files() |
364 return bool(cleaned_errors), stderr | 294 return bool(cleaned_errors), stderr |
365 | 295 |
366 def check_multiple(self, sources, out_file=None, output_wrapper=None, | 296 def check_multiple(self, sources, out_file=None, closure_args=None, |
367 externs=None): | 297 externs=None): |
368 """Closure compile a set of files and check for errors. | 298 """Closure compile a set of files and check for errors. |
369 | 299 |
370 Args: | 300 Args: |
371 sources: An array of files to check. | 301 sources: An array of files to check. |
372 out_file: A file where the compiled output is written to. | 302 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 | 303 closure_args: Arguments passed directly to the closure compiler. |
374 marker token %output%. | |
375 externs: @extern files that inform the compiler about custom globals. | 304 externs: @extern files that inform the compiler about custom globals. |
376 | 305 |
377 Returns: | 306 Returns: |
378 (found_errors, stderr) A boolean indicating whether errors were found and | 307 (found_errors, stderr) A boolean indicating whether errors were found and |
379 the raw Closure Compiler stderr (as a string). | 308 the raw Closure Compiler stderr (as a string). |
380 """ | 309 """ |
381 errors, stderr = self._run_js_check(sources, out_file=out_file, | 310 errors, stderr = self._run_js_check(sources, out_file=out_file, |
382 output_wrapper=output_wrapper, | 311 closure_args=closure_args, |
383 externs=externs) | 312 externs=externs) |
384 self._nuke_temp_files() | 313 self._nuke_temp_files() |
385 return bool(errors), stderr | 314 return bool(errors), stderr |
386 | 315 |
387 | 316 |
388 if __name__ == "__main__": | 317 if __name__ == "__main__": |
389 parser = argparse.ArgumentParser( | 318 parser = argparse.ArgumentParser( |
390 description="Typecheck JavaScript using Closure compiler") | 319 description="Typecheck JavaScript using Closure compiler") |
391 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE, | 320 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE, |
392 help="Path to a source file to typecheck") | 321 help="Path to a source file to typecheck") |
393 single_file_group = parser.add_mutually_exclusive_group() | 322 single_file_group = parser.add_mutually_exclusive_group() |
394 single_file_group.add_argument("--single-file", dest="single_file", | 323 single_file_group.add_argument("--single-file", dest="single_file", |
395 action="store_true", | 324 action="store_true", |
396 help="Process each source file individually") | 325 help="Process each source file individually") |
397 single_file_group.add_argument("--no-single-file", dest="single_file", | 326 single_file_group.add_argument("--no-single-file", dest="single_file", |
398 action="store_false", | 327 action="store_false", |
399 help="Process all source files as a group") | 328 help="Process all source files as a group") |
400 parser.add_argument("-d", "--depends", nargs=argparse.ZERO_OR_MORE) | 329 parser.add_argument("-d", "--depends", nargs=argparse.ZERO_OR_MORE) |
401 parser.add_argument("-e", "--externs", nargs=argparse.ZERO_OR_MORE) | 330 parser.add_argument("-e", "--externs", nargs=argparse.ZERO_OR_MORE) |
402 parser.add_argument("-o", "--out_file", | 331 parser.add_argument("-o", "--out_file", |
403 help="A file where the compiled output is written to") | 332 help="A file where the compiled output is written to") |
404 parser.add_argument("-w", "--output_wrapper", | 333 parser.add_argument("-c", "--closure_args", |
Dan Beam
2015/06/01 22:57:39
i'd say all of these args should be --dash-form fo
Theresa
2015/06/02 22:02:12
Done.
| |
405 help="Wraps output into this string at the place" | 334 help="Arguments passed directly to the closure compiler") |
406 + " denoted by the marker token %output%") | |
407 parser.add_argument("-v", "--verbose", action="store_true", | 335 parser.add_argument("-v", "--verbose", action="store_true", |
408 help="Show more information as this script runs") | 336 help="Show more information as this script runs") |
409 parser.add_argument("--strict", action="store_true", | 337 parser.add_argument("--strict", action="store_true", |
Dan Beam
2015/06/01 22:57:39
kill --strict
Theresa
2015/06/02 22:02:12
Done.
| |
410 help="Enable strict type checking") | 338 help="Enable strict type checking") |
411 parser.add_argument("--success-stamp", | 339 parser.add_argument("--success-stamp", |
412 help="Timestamp file to update upon success") | 340 help="Timestamp file to update upon success") |
413 | 341 |
414 parser.set_defaults(single_file=True, strict=False) | 342 parser.set_defaults(single_file=True, strict=False) |
415 opts = parser.parse_args() | 343 opts = parser.parse_args() |
416 | 344 |
417 depends = opts.depends or [] | 345 depends = opts.depends or [] |
418 externs = set(opts.externs or []) | 346 externs = set(opts.externs or []) |
419 | 347 |
420 polymer_externs = os.path.join(os.path.dirname(_CURRENT_DIR), 'polymer', | 348 polymer_externs = os.path.join(os.path.dirname(_CURRENT_DIR), 'polymer', |
421 'v0_8', 'components-chromium', | 349 'v0_8', 'components-chromium', |
422 'polymer-externs', 'polymer.externs.js') | 350 'polymer-externs', 'polymer.externs.js') |
423 externs.add(polymer_externs) | 351 externs.add(polymer_externs) |
424 | 352 |
425 checker = Checker(verbose=opts.verbose, strict=opts.strict) | 353 checker = Checker(verbose=opts.verbose, strict=opts.strict) |
426 if opts.single_file: | 354 if opts.single_file: |
427 for source in opts.sources: | 355 for source in opts.sources: |
428 depends, externs = build.inputs.resolve_recursive_dependencies( | 356 depends, externs = build.inputs.resolve_recursive_dependencies( |
429 source, depends, externs) | 357 source, depends, externs) |
430 found_errors, _ = checker.check(source, out_file=opts.out_file, | 358 found_errors, _ = checker.check(source, out_file=opts.out_file, |
431 depends=depends, externs=externs, | 359 depends=depends, externs=externs, |
432 output_wrapper=opts.output_wrapper) | 360 closure_args=opts.closure_args) |
433 if found_errors: | 361 if found_errors: |
434 sys.exit(1) | 362 sys.exit(1) |
435 else: | 363 else: |
436 found_errors, stderr = checker.check_multiple( | 364 found_errors, stderr = checker.check_multiple( |
437 opts.sources, | 365 opts.sources, |
438 out_file=opts.out_file, | 366 out_file=opts.out_file, |
439 output_wrapper=opts.output_wrapper, | 367 closure_args=opts.closure_args, |
440 externs=externs) | 368 externs=externs) |
441 if found_errors: | 369 if found_errors: |
442 print stderr | 370 print stderr |
443 sys.exit(1) | 371 sys.exit(1) |
444 | 372 |
445 if opts.success_stamp: | 373 if opts.success_stamp: |
446 with open(opts.success_stamp, "w"): | 374 with open(opts.success_stamp, "w"): |
447 os.utime(opts.success_stamp, None) | 375 os.utime(opts.success_stamp, None) |
OLD | NEW |