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 a JavaScript file to check for errors.""" | 6 """Runs Closure compiler on a JavaScript file to check for errors.""" |
7 | 7 |
8 import argparse | 8 import argparse |
9 import os | 9 import os |
10 import re | 10 import re |
(...skipping 24 matching lines...) Expand all Loading... | |
35 "--jscomp_error=invalidCasts", | 35 "--jscomp_error=invalidCasts", |
36 "--jscomp_error=missingProperties", | 36 "--jscomp_error=missingProperties", |
37 "--jscomp_error=missingReturn", | 37 "--jscomp_error=missingReturn", |
38 "--jscomp_error=nonStandardJsDocs", | 38 "--jscomp_error=nonStandardJsDocs", |
39 "--jscomp_error=suspiciousCode", | 39 "--jscomp_error=suspiciousCode", |
40 "--jscomp_error=undefinedNames", | 40 "--jscomp_error=undefinedNames", |
41 "--jscomp_error=undefinedVars", | 41 "--jscomp_error=undefinedVars", |
42 "--jscomp_error=unknownDefines", | 42 "--jscomp_error=unknownDefines", |
43 "--jscomp_error=uselessCode", | 43 "--jscomp_error=uselessCode", |
44 "--jscomp_error=visibility", | 44 "--jscomp_error=visibility", |
45 "--language_in=ECMASCRIPT5_STRICT", | |
46 "--summary_detail_level=3", | |
47 ] | |
48 | |
49 # These are the extra flags used when compiling in 'strict' mode. | |
50 # Flags that are normally disabled are turned on for strict mode. | |
51 _STRICT_CLOSURE_ARGS = [ | |
52 "--jscomp_error=reportUnknownTypes", | |
53 "--jscomp_error=duplicate", | |
54 "--jscomp_error=misplacedTypeAnnotation", | |
55 ] | |
56 | |
57 _DISABLED_CLOSURE_ARGS = [ | |
45 # TODO(dbeam): happens when the same file is <include>d multiple times. | 58 # TODO(dbeam): happens when the same file is <include>d multiple times. |
46 "--jscomp_off=duplicate", | 59 "--jscomp_off=duplicate", |
47 # TODO(fukino): happens when cr.defineProperty() has a type annotation. | 60 # TODO(fukino): happens when cr.defineProperty() has a type annotation. |
48 # Avoiding parse-time warnings needs 2 pass compiling. crbug.com/421562. | 61 # Avoiding parse-time warnings needs 2 pass compiling. crbug.com/421562. |
49 "--jscomp_off=misplacedTypeAnnotation", | 62 "--jscomp_off=misplacedTypeAnnotation", |
50 "--language_in=ECMASCRIPT5_STRICT", | |
51 "--summary_detail_level=3", | |
52 ] | 63 ] |
53 | 64 |
54 _JAR_COMMAND = [ | 65 _JAR_COMMAND = [ |
55 "java", | 66 "java", |
56 "-jar", | 67 "-jar", |
57 "-Xms1024m", | 68 "-Xms1024m", |
58 "-client", | 69 "-client", |
59 "-XX:+TieredCompilation" | 70 "-XX:+TieredCompilation" |
60 ] | 71 ] |
61 | 72 |
62 _found_java = False | 73 _found_java = False |
63 | 74 |
64 def __init__(self, verbose=False): | 75 def __init__(self, verbose=False, strict=False): |
65 current_dir = os.path.join(os.path.dirname(__file__)) | 76 current_dir = os.path.join(os.path.dirname(__file__)) |
66 self._compiler_jar = os.path.join(current_dir, "lib", "compiler.jar") | |
67 self._runner_jar = os.path.join(current_dir, "runner", "runner.jar") | 77 self._runner_jar = os.path.join(current_dir, "runner", "runner.jar") |
68 self._temp_files = [] | 78 self._temp_files = [] |
69 self._verbose = verbose | 79 self._verbose = verbose |
80 self._strict = strict | |
70 self._error_filter = error_filter.PromiseErrorFilter() | 81 self._error_filter = error_filter.PromiseErrorFilter() |
71 | 82 |
72 def _clean_up(self): | 83 def _clean_up(self): |
73 if not self._temp_files: | 84 if not self._temp_files: |
74 return | 85 return |
75 | 86 |
76 self._debug("Deleting temporary files: %s" % ", ".join(self._temp_files)) | 87 self._debug("Deleting temporary files: %s" % ", ".join(self._temp_files)) |
77 for f in self._temp_files: | 88 for f in self._temp_files: |
78 os.remove(f) | 89 os.remove(f) |
79 self._temp_files = [] | 90 self._temp_files = [] |
80 | 91 |
81 def _debug(self, msg, error=False): | 92 def _debug(self, msg, error=False): |
82 if self._verbose: | 93 if self._verbose: |
83 print "(INFO) %s" % msg | 94 print "(INFO) %s" % msg |
84 | 95 |
85 def _error(self, msg): | 96 def _error(self, msg): |
86 print >> sys.stderr, "(ERROR) %s" % msg | 97 print >> sys.stderr, "(ERROR) %s" % msg |
87 self._clean_up() | 98 self._clean_up() |
88 | 99 |
100 def _common_args(self): | |
101 """Returns an array of the common closure compiler args.""" | |
102 if self._strict: | |
103 return self._COMMON_CLOSURE_ARGS + self._STRICT_CLOSURE_ARGS | |
104 return self._COMMON_CLOSURE_ARGS + self._DISABLED_CLOSURE_ARGS | |
105 | |
89 def _run_command(self, cmd): | 106 def _run_command(self, cmd): |
90 """Runs a shell command. | 107 """Runs a shell command. |
91 | 108 |
92 Args: | 109 Args: |
93 cmd: A list of tokens to be joined into a shell command. | 110 cmd: A list of tokens to be joined into a shell command. |
94 | 111 |
95 Return: | 112 Return: |
96 True if the exit code was 0, else False. | 113 True if the exit code was 0, else False. |
97 """ | 114 """ |
98 cmd_str = " ".join(cmd) | 115 cmd_str = " ".join(cmd) |
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
153 errors = filter(None, errors) | 170 errors = filter(None, errors) |
154 contents = "\n## ".join("\n\n".join(errors).splitlines()) | 171 contents = "\n## ".join("\n\n".join(errors).splitlines()) |
155 return "## %s" % contents if contents else "" | 172 return "## %s" % contents if contents else "" |
156 | 173 |
157 def _create_temp_file(self, contents): | 174 def _create_temp_file(self, contents): |
158 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file: | 175 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file: |
159 self._temp_files.append(tmp_file.name) | 176 self._temp_files.append(tmp_file.name) |
160 tmp_file.write(contents) | 177 tmp_file.write(contents) |
161 return tmp_file.name | 178 return tmp_file.name |
162 | 179 |
180 def run_js_check(self, sources, externs=None): | |
181 if not self._check_java_path(): | |
182 return 1, "" | |
183 | |
184 args = ["--js=%s" % s for s in sources] | |
185 if externs: | |
186 args += ["--externs=%s" % e for e in externs] | |
187 args_file_content = " %s" % " ".join(self._common_args() + args) | |
188 self._debug("Args: %s" % args_file_content.strip()) | |
189 | |
190 args_file = self._create_temp_file(args_file_content) | |
191 self._debug("Args file: %s" % args_file) | |
192 | |
193 runner_args = ["--compiler-args-file=%s" % args_file] | |
194 runner_cmd = self._run_jar(self._runner_jar, args=runner_args) | |
195 (_, stderr) = runner_cmd.communicate() | |
Tyler Breisacher (Chromium)
2015/01/13 03:01:00
nit: You don't need the parens here:
_, stderr =
garykac
2015/01/13 18:11:36
Done.
| |
196 | |
197 errors = stderr.strip().split("\n\n") | |
198 self._debug("Summary: %s" % errors.pop()) | |
199 | |
200 self._clean_up() | |
201 | |
202 return errors, stderr | |
203 | |
163 def check(self, source_file, depends=None, externs=None): | 204 def check(self, source_file, depends=None, externs=None): |
164 """Closure compile a file and check for errors. | 205 """Closure compile a file and check for errors. |
165 | 206 |
166 Args: | 207 Args: |
167 source_file: A file to check. | 208 source_file: A file to check. |
168 depends: Other files that would be included with a <script> earlier in | 209 depends: Other files that would be included with a <script> earlier in |
169 the page. | 210 the page. |
170 externs: @extern files that inform the compiler about custom globals. | 211 externs: @extern files that inform the compiler about custom globals. |
171 | 212 |
172 Returns: | 213 Returns: |
173 (exitcode, output) The exit code of the Closure compiler (as a number) | 214 (has_errors, output) A boolean indicating if there were errors and the |
174 and its output (as a string). | 215 Closure compiler output (as a string). |
175 """ | 216 """ |
176 depends = depends or [] | 217 depends = depends or [] |
177 externs = externs or set() | 218 externs = externs or set() |
178 | 219 |
179 if not self._check_java_path(): | 220 if not self._check_java_path(): |
180 return 1, "" | 221 return 1, "" |
181 | 222 |
182 self._debug("FILE: %s" % source_file) | 223 self._debug("FILE: %s" % source_file) |
183 | 224 |
184 if source_file.endswith("_externs.js"): | 225 if source_file.endswith("_externs.js"): |
185 self._debug("Skipping externs: %s" % source_file) | 226 self._debug("Skipping externs: %s" % source_file) |
186 return | 227 return |
187 | 228 |
188 self._file_arg = source_file | 229 self._file_arg = source_file |
189 | 230 |
190 tmp_dir = tempfile.gettempdir() | 231 tmp_dir = tempfile.gettempdir() |
191 rel_path = lambda f: os.path.join(os.path.relpath(os.getcwd(), tmp_dir), f) | 232 rel_path = lambda f: os.path.join(os.path.relpath(os.getcwd(), tmp_dir), f) |
192 | 233 |
193 includes = [rel_path(f) for f in depends + [source_file]] | 234 includes = [rel_path(f) for f in depends + [source_file]] |
194 contents = ['<include src="%s">' % i for i in includes] | 235 contents = ['<include src="%s">' % i for i in includes] |
195 meta_file = self._create_temp_file("\n".join(contents)) | 236 meta_file = self._create_temp_file("\n".join(contents)) |
196 self._debug("Meta file: %s" % meta_file) | 237 self._debug("Meta file: %s" % meta_file) |
197 | 238 |
198 self._processor = processor.Processor(meta_file) | 239 self._processor = processor.Processor(meta_file) |
199 self._expanded_file = self._create_temp_file(self._processor.contents) | 240 self._expanded_file = self._create_temp_file(self._processor.contents) |
200 self._debug("Expanded file: %s" % self._expanded_file) | 241 self._debug("Expanded file: %s" % self._expanded_file) |
201 | 242 |
202 args = ["--js=%s" % self._expanded_file] | 243 (errors, stderr) = self.run_js_check(self._expanded_file, depends) |
Tyler Breisacher (Chromium)
2015/01/13 03:01:00
same here
garykac
2015/01/13 18:11:36
Done.
| |
203 args += ["--externs=%s" % e for e in externs] | |
204 args_file_content = " %s" % " ".join(self._COMMON_CLOSURE_ARGS + args) | |
205 self._debug("Args: %s" % args_file_content.strip()) | |
206 | |
207 args_file = self._create_temp_file(args_file_content) | |
208 self._debug("Args file: %s" % args_file) | |
209 | |
210 runner_args = ["--compiler-args-file=%s" % args_file] | |
211 runner_cmd = self._run_jar(self._runner_jar, args=runner_args) | |
212 (_, stderr) = runner_cmd.communicate() | |
213 | |
214 errors = stderr.strip().split("\n\n") | |
215 | 244 |
216 # Filter out false-positive promise chain errors. | 245 # Filter out false-positive promise chain errors. |
217 # See https://github.com/google/closure-compiler/issues/715 for details. | 246 # See https://github.com/google/closure-compiler/issues/715 for details. |
218 errors = self._error_filter.filter(errors); | 247 errors = self._error_filter.filter(errors); |
219 | 248 |
220 self._debug("Summary: %s" % errors.pop()) | |
221 | |
222 output = self._format_errors(map(self._fix_up_error, errors)) | 249 output = self._format_errors(map(self._fix_up_error, errors)) |
223 if errors: | 250 if errors: |
224 prefix = "\n" if output else "" | 251 prefix = "\n" if output else "" |
225 self._error("Error in: %s%s%s" % (source_file, prefix, output)) | 252 self._error("Error in: %s%s%s" % (source_file, prefix, output)) |
226 elif output: | 253 elif output: |
227 self._debug("Output: %s" % output) | 254 self._debug("Output: %s" % output) |
228 | 255 |
229 self._clean_up() | |
230 | |
231 return bool(errors), output | 256 return bool(errors), output |
232 | 257 |
258 def check_multiple(self, sources): | |
259 """Closure compile a set of files and check for errors. | |
260 | |
261 Args: | |
262 sources: An array of files to check. | |
263 | |
264 Returns: | |
265 (has_errors, output) A boolean indicating if there were errors and the | |
266 Closure compiler output (as a string). | |
267 """ | |
268 | |
269 (errors, stderr) = self.run_js_check(sources) | |
Tyler Breisacher (Chromium)
2015/01/13 03:01:01
and here
garykac
2015/01/13 18:11:36
Done.
| |
270 return bool(errors), stderr | |
233 | 271 |
234 if __name__ == "__main__": | 272 if __name__ == "__main__": |
235 parser = argparse.ArgumentParser( | 273 parser = argparse.ArgumentParser( |
236 description="Typecheck JavaScript using Closure compiler") | 274 description="Typecheck JavaScript using Closure compiler") |
237 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE, | 275 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE, |
238 help="Path to a source file to typecheck") | 276 help="Path to a source file to typecheck") |
277 single_file_group = parser.add_mutually_exclusive_group() | |
278 single_file_group.add_argument("--single-file", dest="single_file", | |
279 action="store_true", | |
280 help="Process each source file individually") | |
281 single_file_group.add_argument("--no-single-file", dest="single_file", | |
282 action="store_false", | |
283 help="Process all source files as a group") | |
239 parser.add_argument("-d", "--depends", nargs=argparse.ZERO_OR_MORE) | 284 parser.add_argument("-d", "--depends", nargs=argparse.ZERO_OR_MORE) |
240 parser.add_argument("-e", "--externs", nargs=argparse.ZERO_OR_MORE) | 285 parser.add_argument("-e", "--externs", nargs=argparse.ZERO_OR_MORE) |
241 parser.add_argument("-o", "--out_file", help="A place to output results") | 286 parser.add_argument("-o", "--out_file", help="A place to output results") |
242 parser.add_argument("-v", "--verbose", action="store_true", | 287 parser.add_argument("-v", "--verbose", action="store_true", |
243 help="Show more information as this script runs") | 288 help="Show more information as this script runs") |
289 parser.add_argument("--strict", action="store_true", | |
290 help="Enable strict type checking") | |
291 parser.add_argument("--success-stamp", | |
292 help="Timestamp file to update upon success") | |
293 | |
294 parser.set_defaults(single_file=True, strict=False) | |
244 opts = parser.parse_args() | 295 opts = parser.parse_args() |
245 | 296 |
246 checker = Checker(verbose=opts.verbose) | 297 depends = opts.depends or [] |
247 for source in opts.sources: | 298 externs = opts.externs or set() |
248 depends, externs = build.inputs.resolve_recursive_dependencies( | |
249 source, | |
250 opts.depends, | |
251 opts.externs) | |
252 exit, _ = checker.check(source, depends=depends, externs=externs) | |
253 if exit != 0: | |
254 sys.exit(exit) | |
255 | 299 |
256 if opts.out_file: | 300 checker = Checker(verbose=opts.verbose, strict=opts.strict) |
257 out_dir = os.path.dirname(opts.out_file) | 301 if opts.single_file: |
258 if not os.path.exists(out_dir): | 302 for source in opts.sources: |
259 os.makedirs(out_dir) | 303 depends, externs = build.inputs.resolve_recursive_dependencies( |
260 # TODO(dbeam): write compiled file to |opts.out_file|. | 304 source, |
261 open(opts.out_file, "w").write("") | 305 depends, |
306 externs) | |
307 has_errors, _ = checker.check(source, depends=depends, externs=externs) | |
308 if has_errors: | |
309 sys.exit(1) | |
310 | |
311 if opts.out_file: | |
312 out_dir = os.path.dirname(opts.out_file) | |
313 if not os.path.exists(out_dir): | |
314 os.makedirs(out_dir) | |
315 # TODO(dbeam): write compiled file to |opts.out_file|. | |
316 open(opts.out_file, "w").write("") | |
317 else: | |
318 has_errors, _ = checker.check_multiple(opts.sources) | |
319 if has_errors: | |
320 print _ | |
Tyler Breisacher (Chromium)
2015/01/13 03:01:00
I think the convention is that '_' is only for unu
garykac
2015/01/13 18:11:36
Done.
| |
321 sys.exit(1) | |
322 | |
323 if opts.success_stamp: | |
324 with open(opts.success_stamp, 'w'): | |
325 os.utime(opts.success_stamp, None) | |
OLD | NEW |