Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(360)

Side by Side Diff: third_party/closure_compiler/checker.py

Issue 476453002: Python readability review for dbeam@. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: rothwell@ review Created 5 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | third_party/closure_compiler/compile_js.gypi » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
11 import subprocess 11 import subprocess
12 import sys 12 import sys
13 import tempfile 13 import tempfile
14 14
15 import build.inputs 15 import build.inputs
16 import processor 16 import processor
17 import error_filter 17 import error_filter
18 18
19 19
20 class Checker(object): 20 class Checker(object):
21 """Runs the Closure compiler on a given source file and returns the 21 """Runs the Closure compiler on a given source file to typecheck it."""
22 success/errors."""
23 22
24 _COMMON_CLOSURE_ARGS = [ 23 _COMMON_CLOSURE_ARGS = [
25 "--accept_const_keyword", 24 "--accept_const_keyword",
26 "--jscomp_error=accessControls", 25 "--jscomp_error=accessControls",
27 "--jscomp_error=ambiguousFunctionDecl", 26 "--jscomp_error=ambiguousFunctionDecl",
28 "--jscomp_error=checkStructDictInheritance", 27 "--jscomp_error=checkStructDictInheritance",
29 "--jscomp_error=checkTypes", 28 "--jscomp_error=checkTypes",
30 "--jscomp_error=checkVars", 29 "--jscomp_error=checkVars",
31 "--jscomp_error=constantProperty", 30 "--jscomp_error=constantProperty",
32 "--jscomp_error=deprecated", 31 "--jscomp_error=deprecated",
33 "--jscomp_error=externsValidation", 32 "--jscomp_error=externsValidation",
34 "--jscomp_error=globalThis", 33 "--jscomp_error=globalThis",
35 "--jscomp_error=invalidCasts", 34 "--jscomp_error=invalidCasts",
36 "--jscomp_error=missingProperties", 35 "--jscomp_error=missingProperties",
37 "--jscomp_error=missingReturn", 36 "--jscomp_error=missingReturn",
38 "--jscomp_error=nonStandardJsDocs", 37 "--jscomp_error=nonStandardJsDocs",
39 "--jscomp_error=suspiciousCode", 38 "--jscomp_error=suspiciousCode",
40 "--jscomp_error=undefinedNames", 39 "--jscomp_error=undefinedNames",
41 "--jscomp_error=undefinedVars", 40 "--jscomp_error=undefinedVars",
42 "--jscomp_error=unknownDefines", 41 "--jscomp_error=unknownDefines",
43 "--jscomp_error=uselessCode", 42 "--jscomp_error=uselessCode",
44 "--jscomp_error=visibility", 43 "--jscomp_error=visibility",
45 "--language_in=ECMASCRIPT5_STRICT", 44 "--language_in=ECMASCRIPT5_STRICT",
46 "--summary_detail_level=3", 45 "--summary_detail_level=3",
47 ] 46 ]
48 47
49 # These are the extra flags used when compiling in 'strict' mode. 48 # These are the extra flags used when compiling in strict mode.
50 # Flags that are normally disabled are turned on for strict mode. 49 # Flags that are normally disabled are turned on for strict mode.
51 _STRICT_CLOSURE_ARGS = [ 50 _STRICT_CLOSURE_ARGS = [
52 "--jscomp_error=reportUnknownTypes", 51 "--jscomp_error=reportUnknownTypes",
53 "--jscomp_error=duplicate", 52 "--jscomp_error=duplicate",
54 "--jscomp_error=misplacedTypeAnnotation", 53 "--jscomp_error=misplacedTypeAnnotation",
55 ] 54 ]
56 55
57 _DISABLED_CLOSURE_ARGS = [ 56 _DISABLED_CLOSURE_ARGS = [
58 # TODO(dbeam): happens when the same file is <include>d multiple times. 57 # TODO(dbeam): happens when the same file is <include>d multiple times.
59 "--jscomp_off=duplicate", 58 "--jscomp_off=duplicate",
60 # TODO(fukino): happens when cr.defineProperty() has a type annotation. 59 # TODO(fukino): happens when cr.defineProperty() has a type annotation.
61 # Avoiding parse-time warnings needs 2 pass compiling. crbug.com/421562. 60 # Avoiding parse-time warnings needs 2 pass compiling. crbug.com/421562.
62 "--jscomp_off=misplacedTypeAnnotation", 61 "--jscomp_off=misplacedTypeAnnotation",
63 ] 62 ]
64 63
65 _JAR_COMMAND = [ 64 _JAR_COMMAND = [
66 "java", 65 "java",
67 "-jar", 66 "-jar",
68 "-Xms1024m", 67 "-Xms1024m",
69 "-client", 68 "-client",
70 "-XX:+TieredCompilation" 69 "-XX:+TieredCompilation"
71 ] 70 ]
72 71
73 _found_java = False
74
75 def __init__(self, verbose=False, strict=False): 72 def __init__(self, verbose=False, strict=False):
73 """
74 Args:
75 verbose: Whether this class should output diagnostic messages.
76 strict: Whether the Closure Compiler should be invoked more strictly.
77 """
76 current_dir = os.path.join(os.path.dirname(__file__)) 78 current_dir = os.path.join(os.path.dirname(__file__))
77 self._runner_jar = os.path.join(current_dir, "runner", "runner.jar") 79 self._runner_jar = os.path.join(current_dir, "runner", "runner.jar")
78 self._temp_files = [] 80 self._temp_files = []
79 self._verbose = verbose 81 self._verbose = verbose
80 self._strict = strict 82 self._strict = strict
81 self._error_filter = error_filter.PromiseErrorFilter() 83 self._error_filter = error_filter.PromiseErrorFilter()
82 84
83 def _clean_up(self): 85 def _clean_up(self):
86 """Deletes any temp files this class knows about."""
84 if not self._temp_files: 87 if not self._temp_files:
85 return 88 return
86 89
87 self._debug("Deleting temporary files: %s" % ", ".join(self._temp_files)) 90 self._log_debug("Deleting temp files: %s" % ", ".join(self._temp_files))
88 for f in self._temp_files: 91 for f in self._temp_files:
89 os.remove(f) 92 os.remove(f)
90 self._temp_files = [] 93 self._temp_files = []
91 94
92 def _debug(self, msg, error=False): 95 def _log_debug(self, msg, error=False):
96 """Logs |msg| to stdout if --verbose/-v is passed when invoking this script.
rothwell 2015/03/20 19:24:31 is this syntax, |x| common in chromium?
Dan Beam 2015/03/21 01:35:43 yes, |var_names| or |_member_vars| or anything in
97
98 Args:
99 msg: A debug message to log.
100 """
93 if self._verbose: 101 if self._verbose:
94 print "(INFO) %s" % msg 102 print "(INFO) %s" % msg
95 103
96 def _error(self, msg): 104 def _log_error(self, msg):
105 """Logs |msg| to stderr regardless of --flags.
106
107 Args:
108 msg: An error message to log.
109 """
97 print >> sys.stderr, "(ERROR) %s" % msg 110 print >> sys.stderr, "(ERROR) %s" % msg
98 self._clean_up()
99 111
100 def _common_args(self): 112 def _common_args(self):
101 """Returns an array of the common closure compiler args.""" 113 """Returns an array of the common closure compiler args."""
102 if self._strict: 114 if self._strict:
103 return self._COMMON_CLOSURE_ARGS + self._STRICT_CLOSURE_ARGS 115 return self._COMMON_CLOSURE_ARGS + self._STRICT_CLOSURE_ARGS
104 return self._COMMON_CLOSURE_ARGS + self._DISABLED_CLOSURE_ARGS 116 return self._COMMON_CLOSURE_ARGS + self._DISABLED_CLOSURE_ARGS
105 117
106 def _run_command(self, cmd): 118 def _run_jar(self, jar, args):
107 """Runs a shell command. 119 """Runs a .jar from the command line with arguments.
108 120
109 Args: 121 Args:
110 cmd: A list of tokens to be joined into a shell command. 122 jar: A file path to a .jar file
111 123 args: A list of command line arguments to be passed when running the .jar.
112 Return: 124
113 True if the exit code was 0, else False. 125 Return:
114 """ 126 (exit_code, stderr) The exit code of the command (e.g. 0 for success) and
115 cmd_str = " ".join(cmd) 127 the stderr collected while running |jar| (as a string).
116 self._debug("Running command: %s" % cmd_str) 128 """
129 shell_command = " ".join(self._JAR_COMMAND + [jar] + args)
130 self._log_debug("Running jar: %s" % shell_command)
117 131
118 devnull = open(os.devnull, "w") 132 devnull = open(os.devnull, "w")
119 return subprocess.Popen( 133 kwargs = {"stdout": devnull, "stderr": subprocess.PIPE, "shell": True}
120 cmd_str, stdout=devnull, stderr=subprocess.PIPE, shell=True) 134 process = subprocess.Popen(shell_command, **kwargs)
121 135 _, stderr = process.communicate()
122 def _check_java_path(self): 136 return process.returncode, stderr
123 """Checks that `java` is on the system path.""" 137
124 if not self._found_java: 138 def _get_line_number(self, match):
125 proc = self._run_command(["which", "java"]) 139 """When chrome is built, it preprocesses its JavaScript from:
126 proc.communicate() 140
127 if proc.returncode == 0: 141 <include src="blah.js">
128 self._found_java = True 142 alert(1);
129 else: 143
130 self._error("Cannot find java (`which java` => %s)" % proc.returncode) 144 to:
131 145
132 return self._found_java 146 /* contents of blah.js inlined */
133 147 alert(1);
134 def _run_jar(self, jar, args=None): 148
135 args = args or [] 149 Because Closure Compiler requires this inlining already be done (as
136 self._check_java_path() 150 <include> isn't valid JavaScript), this script creates temporary files to
137 return self._run_command(self._JAR_COMMAND + [jar] + args) 151 expand all the <include>s.
138 152
139 def _fix_line_number(self, match): 153 When type errors are hit in temporary files, a developer doesn't know the
140 """Changes a line number from /tmp/file:300 to /orig/file:100. 154 original source location to fix. This method maps from /tmp/file:300 back to
141 155 /original/source/file:100 so fixing errors is faster for developers.
142 Args: 156
143 match: A re.MatchObject from matching against a line number regex. 157 Args:
158 match: A re.MatchObject from matching against a line number regex.
144 159
145 Returns: 160 Returns:
146 The fixed up /file and :line number. 161 The fixed up /file and :line number.
147 """ 162 """
148 real_file = self._processor.get_file_from_line(match.group(1)) 163 real_file = self._processor.get_file_from_line(match.group(1))
149 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number) 164 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number)
150 165
166 def _filter_errors(self, errors):
167 """Removes some extraneous errors. For example, we ignore:
168
169 Variable x first declared in /tmp/expanded/file
170
171 Because it's just a duplicated error (it'll only ever show up 2+ times).
172 We also ignore Promose-based errors:
173
174 found : function (VolumeInfo): (Promise<(DirectoryEntry|null)>|null)
175 required: (function (Promise<VolumeInfo>): ?|null|undefined)
176
177 as templates don't work with Promises in all cases yet. See
178 https://github.com/google/closure-compiler/issues/715 for details.
179
180 Args:
181 errors: A list of string errors extracted from Closure Compiler output.
182
183 Return:
184 A slimmer, sleeker list of relevant errors (strings).
185 """
186 first_declared_in = lambda e: " first declared in " not in e
187 return self._error_filter.filter(filter(first_declared_in, errors))
188
151 def _fix_up_error(self, error): 189 def _fix_up_error(self, error):
152 """Filter out irrelevant errors or fix line numbers. 190 """Reverse the effects that funky <include> preprocessing steps have on
153 191 errors messages.
154 Args: 192
155 error: A Closure compiler error (2 line string with error and source). 193 Args:
156 194 error: A Closure compiler error (2 line string with error and source).
157 Return: 195
158 The fixed up error string (blank if it should be ignored). 196 Return:
159 """ 197 The fixed up error string.
160 if " first declared in " in error: 198 """
161 # Ignore "Variable x first declared in /same/file".
162 return ""
163
164 expanded_file = self._expanded_file 199 expanded_file = self._expanded_file
165 fixed = re.sub("%s:(\d+)" % expanded_file, self._fix_line_number, error) 200 fixed = re.sub("%s:(\d+)" % expanded_file, self._get_line_number, error)
166 return fixed.replace(expanded_file, os.path.abspath(self._file_arg)) 201 return fixed.replace(expanded_file, os.path.abspath(self._file_arg))
167 202
168 def _format_errors(self, errors): 203 def _format_errors(self, errors):
169 """Formats Closure compiler errors to easily spot compiler output.""" 204 """Formats Closure compiler errors to easily spot compiler output.
170 errors = filter(None, errors) 205
206 Args:
207 errors: A list of strings extracted from the Closure compiler's output.
208
209 Returns:
210 A formatted output string.
211 """
171 contents = "\n## ".join("\n\n".join(errors).splitlines()) 212 contents = "\n## ".join("\n\n".join(errors).splitlines())
172 return "## %s" % contents if contents else "" 213 return "## %s" % contents if contents else ""
173 214
174 def _create_temp_file(self, contents): 215 def _create_temp_file(self, contents):
216 """Creates an owned temporary file with |contents|.
217
218 Args:
219 content: A string of the file contens to write to a temporary file.
220
221 Return:
222 The filepath of the newly created, written, and closed temporary file.
223 """
175 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file: 224 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file:
176 self._temp_files.append(tmp_file.name) 225 self._temp_files.append(tmp_file.name)
177 tmp_file.write(contents) 226 tmp_file.write(contents)
178 return tmp_file.name 227 return tmp_file.name
179 228
180 def run_js_check(self, sources, externs=None): 229 def _run_js_check(self, sources, externs):
181 if not self._check_java_path(): 230 """Check |sources| for type errors.
182 return 1, "" 231
183 232 Args:
233 sources: Files to check.
234 externs: @extern files that inform the compiler about custom globals.
235
236 Returns:
237 A list of errors (strings) found by the Closure Compiler.
238 """
184 args = ["--js=%s" % s for s in sources] 239 args = ["--js=%s" % s for s in sources]
185 if externs: 240 args += ["--externs=%s" % e for e in externs]
186 args += ["--externs=%s" % e for e in externs]
187 args_file_content = " %s" % " ".join(self._common_args() + args) 241 args_file_content = " %s" % " ".join(self._common_args() + args)
188 self._debug("Args: %s" % args_file_content.strip()) 242 self._log_debug("Args: %s" % args_file_content.strip())
189 243
190 args_file = self._create_temp_file(args_file_content) 244 args_file = self._create_temp_file(args_file_content)
191 self._debug("Args file: %s" % args_file) 245 self._log_debug("Args file: %s" % args_file)
192 246
193 runner_args = ["--compiler-args-file=%s" % args_file] 247 runner_args = ["--compiler-args-file=%s" % args_file]
194 runner_cmd = self._run_jar(self._runner_jar, args=runner_args) 248 _, stderr = self._run_jar(self._runner_jar, runner_args)
195 _, stderr = runner_cmd.communicate()
196 249
197 errors = stderr.strip().split("\n\n") 250 errors = stderr.strip().split("\n\n")
198 self._debug("Summary: %s" % errors.pop()) 251 maybe_summary = errors.pop()
199 252
200 self._clean_up() 253 if re.search(".*error.*warning.*typed", maybe_summary):
201 254 self._log_debug("Summary: %s" % maybe_summary)
202 return errors, stderr 255 else:
203 256 # Not a summary. Running the jar failed. Bail.
204 def check(self, source_file, depends=None, externs=None): 257 self._log_error(stderr)
205 """Closure compile a file and check for errors. 258 self._clean_up()
206 259 sys.exit(1)
207 Args: 260
208 source_file: A file to check. 261 return errors
209 depends: Other files that would be included with a <script> earlier in 262
210 the page. 263 def check(self, source_file, depends, externs):
211 externs: @extern files that inform the compiler about custom globals. 264 """Check |source_file| for type errors.
212 265
213 Returns: 266 Args:
214 (has_errors, output) A boolean indicating if there were errors and the 267 source_file: A file to check.
215 Closure compiler output (as a string). 268 depends: Files that |source_file| requires to run (e.g. earlier <script>).
216 """ 269 externs: @extern files that inform the compiler about custom globals.
217 depends = depends or [] 270 """
218 externs = externs or set() 271 self._log_debug("FILE: %s" % source_file)
219
220 if not self._check_java_path():
221 return 1, ""
222
223 self._debug("FILE: %s" % source_file)
224 272
225 if source_file.endswith("_externs.js"): 273 if source_file.endswith("_externs.js"):
226 self._debug("Skipping externs: %s" % source_file) 274 self._log_debug("Skipping externs: %s" % source_file)
227 return 275 return
228 276
229 self._file_arg = source_file 277 self._file_arg = source_file
230 278
231 tmp_dir = tempfile.gettempdir() 279 cwd, tmp_dir = os.getcwd(), tempfile.gettempdir()
232 rel_path = lambda f: os.path.join(os.path.relpath(os.getcwd(), tmp_dir), f) 280 rel_path = lambda f: os.path.join(os.path.relpath(cwd, tmp_dir), f)
233 281
234 includes = [rel_path(f) for f in depends + [source_file]] 282 includes = [rel_path(f) for f in depends + [source_file]]
235 contents = ['<include src="%s">' % i for i in includes] 283 contents = ['<include src="%s">' % i for i in includes]
236 meta_file = self._create_temp_file("\n".join(contents)) 284 meta_file = self._create_temp_file("\n".join(contents))
237 self._debug("Meta file: %s" % meta_file) 285 self._log_debug("Meta file: %s" % meta_file)
238 286
239 self._processor = processor.Processor(meta_file) 287 self._processor = processor.Processor(meta_file)
240 self._expanded_file = self._create_temp_file(self._processor.contents) 288 self._expanded_file = self._create_temp_file(self._processor.contents)
241 self._debug("Expanded file: %s" % self._expanded_file) 289 self._log_debug("Expanded file: %s" % self._expanded_file)
242 290
243 errors, stderr = self.run_js_check([self._expanded_file], externs) 291 errors = self._run_js_check([self._expanded_file], externs)
244 292 filtered_errors = self._filter_errors(errors)
245 # Filter out false-positive promise chain errors. 293 fixed_errors = map(self._fix_up_error, filtered_errors)
246 # See https://github.com/google/closure-compiler/issues/715 for details. 294 output = self._format_errors(fixed_errors)
247 errors = self._error_filter.filter(errors); 295
248
249 output = self._format_errors(map(self._fix_up_error, errors))
250 if errors: 296 if errors:
251 prefix = "\n" if output else "" 297 prefix = "\n" if output else ""
252 self._error("Error in: %s%s%s" % (source_file, prefix, output)) 298 self._log_error("Error in: %s%s%s" % (source_file, prefix, output))
253 elif output: 299 elif output:
254 self._debug("Output: %s" % output) 300 self._log_debug("Output: %s" % output)
255 301
256 return bool(errors), output 302 self._clean_up()
257 303
258 def check_multiple(self, sources): 304 def check_multiple(self, sources):
259 """Closure compile a set of files and check for errors. 305 """Closure compile a set of files and check for errors.
260 306
261 Args: 307 Args:
262 sources: An array of files to check. 308 sources: An array of files to check.
263 309 """
264 Returns: 310 self._run_js_check(sources, [])
265 (has_errors, output) A boolean indicating if there were errors and the 311 self._clean_up()
266 Closure compiler output (as a string). 312
267 """
268
269 errors, stderr = self.run_js_check(sources)
270 return bool(errors), stderr
271 313
272 if __name__ == "__main__": 314 if __name__ == "__main__":
273 parser = argparse.ArgumentParser( 315 parser = argparse.ArgumentParser(
274 description="Typecheck JavaScript using Closure compiler") 316 description="Typecheck JavaScript using Closure compiler")
275 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE, 317 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE,
276 help="Path to a source file to typecheck") 318 help="Path to a source file to typecheck")
277 single_file_group = parser.add_mutually_exclusive_group() 319 single_file_group = parser.add_mutually_exclusive_group()
278 single_file_group.add_argument("--single-file", dest="single_file", 320 single_file_group.add_argument("--single-file", dest="single_file",
279 action="store_true", 321 action="store_true",
280 help="Process each source file individually") 322 help="Process each source file individually")
(...skipping 10 matching lines...) Expand all
291 parser.add_argument("--success-stamp", 333 parser.add_argument("--success-stamp",
292 help="Timestamp file to update upon success") 334 help="Timestamp file to update upon success")
293 335
294 parser.set_defaults(single_file=True, strict=False) 336 parser.set_defaults(single_file=True, strict=False)
295 opts = parser.parse_args() 337 opts = parser.parse_args()
296 338
297 depends = opts.depends or [] 339 depends = opts.depends or []
298 externs = opts.externs or set() 340 externs = opts.externs or set()
299 341
300 checker = Checker(verbose=opts.verbose, strict=opts.strict) 342 checker = Checker(verbose=opts.verbose, strict=opts.strict)
343
301 if opts.single_file: 344 if opts.single_file:
302 for source in opts.sources: 345 for source in opts.sources:
303 depends, externs = build.inputs.resolve_recursive_dependencies( 346 depends, externs = build.inputs.resolve_recursive_dependencies(
304 source, 347 source, depends, externs)
305 depends, 348 checker.check(source, depends, externs)
306 externs)
307 has_errors, _ = checker.check(source, depends=depends, externs=externs)
308 if has_errors:
309 sys.exit(1)
310 349
311 if opts.out_file: 350 if opts.out_file:
312 out_dir = os.path.dirname(opts.out_file) 351 out_dir = os.path.dirname(opts.out_file)
313 if not os.path.exists(out_dir): 352 if not os.path.exists(out_dir):
314 os.makedirs(out_dir) 353 os.makedirs(out_dir)
315 # TODO(dbeam): write compiled file to |opts.out_file|. 354 # Write a blank file so incremental builds work. Ninja compares the
355 # modified time of output files to input files and rebuilds only if
356 # input files are newer. TODO(dbeam): write the compiled output instead
357 # when we're sure Closure compiling Chrome's JS doesn't break things.
316 open(opts.out_file, "w").write("") 358 open(opts.out_file, "w").write("")
317 else: 359 else:
318 has_errors, errors = checker.check_multiple(opts.sources) 360 checker.check_multiple(opts.sources)
319 if has_errors:
320 print errors
321 sys.exit(1)
322 361
323 if opts.success_stamp: 362 if opts.success_stamp:
324 with open(opts.success_stamp, 'w'): 363 with open(opts.success_stamp, "w"):
325 os.utime(opts.success_stamp, None) 364 os.utime(opts.success_stamp, None)
OLDNEW
« no previous file with comments | « no previous file | third_party/closure_compiler/compile_js.gypi » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698