OLD | NEW |
| (Empty) |
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. | |
5 | |
6 """Runs Closure compiler on JavaScript files to check for errors and produce | |
7 minified output.""" | |
8 | |
9 import argparse | |
10 import os | |
11 import re | |
12 import subprocess | |
13 import sys | |
14 import tempfile | |
15 | |
16 import build.inputs | |
17 import processor | |
18 import error_filter | |
19 | |
20 | |
21 _CURRENT_DIR = os.path.join(os.path.dirname(__file__)) | |
22 | |
23 | |
24 class Checker(object): | |
25 """Runs the Closure compiler on given source files to typecheck them | |
26 and produce minified output.""" | |
27 | |
28 _JAR_COMMAND = [ | |
29 "java", | |
30 "-jar", | |
31 "-Xms1024m", | |
32 "-client", | |
33 "-XX:+TieredCompilation" | |
34 ] | |
35 | |
36 _MAP_FILE_FORMAT = "%s.map" | |
37 | |
38 def __init__(self, verbose=False): | |
39 """ | |
40 Args: | |
41 verbose: Whether this class should output diagnostic messages. | |
42 """ | |
43 self._compiler_jar = os.path.join(_CURRENT_DIR, "compiler", "compiler.jar") | |
44 self._temp_files = [] | |
45 self._verbose = verbose | |
46 self._error_filter = error_filter.PromiseErrorFilter() | |
47 | |
48 def _nuke_temp_files(self): | |
49 """Deletes any temp files this class knows about.""" | |
50 if not self._temp_files: | |
51 return | |
52 | |
53 self._log_debug("Deleting temp files: %s" % ", ".join(self._temp_files)) | |
54 for f in self._temp_files: | |
55 os.remove(f) | |
56 self._temp_files = [] | |
57 | |
58 def _log_debug(self, msg, error=False): | |
59 """Logs |msg| to stdout if --verbose/-v is passed when invoking this script. | |
60 | |
61 Args: | |
62 msg: A debug message to log. | |
63 """ | |
64 if self._verbose: | |
65 print "(INFO) %s" % msg | |
66 | |
67 def _log_error(self, msg): | |
68 """Logs |msg| to stderr regardless of --flags. | |
69 | |
70 Args: | |
71 msg: An error message to log. | |
72 """ | |
73 print >> sys.stderr, "(ERROR) %s" % msg | |
74 | |
75 def _run_jar(self, jar, args): | |
76 """Runs a .jar from the command line with arguments. | |
77 | |
78 Args: | |
79 jar: A file path to a .jar file | |
80 args: A list of command line arguments to be passed when running the .jar. | |
81 | |
82 Return: | |
83 (exit_code, stderr) The exit code of the command (e.g. 0 for success) and | |
84 the stderr collected while running |jar| (as a string). | |
85 """ | |
86 shell_command = " ".join(self._JAR_COMMAND + [jar] + args) | |
87 self._log_debug("Running jar: %s" % shell_command) | |
88 | |
89 devnull = open(os.devnull, "w") | |
90 kwargs = {"stdout": devnull, "stderr": subprocess.PIPE, "shell": True} | |
91 process = subprocess.Popen(shell_command, **kwargs) | |
92 _, stderr = process.communicate() | |
93 return process.returncode, stderr | |
94 | |
95 def _get_line_number(self, match): | |
96 """When chrome is built, it preprocesses its JavaScript from: | |
97 | |
98 <include src="blah.js"> | |
99 alert(1); | |
100 | |
101 to: | |
102 | |
103 /* contents of blah.js inlined */ | |
104 alert(1); | |
105 | |
106 Because Closure Compiler requires this inlining already be done (as | |
107 <include> isn't valid JavaScript), this script creates temporary files to | |
108 expand all the <include>s. | |
109 | |
110 When type errors are hit in temporary files, a developer doesn't know the | |
111 original source location to fix. This method maps from /tmp/file:300 back to | |
112 /original/source/file:100 so fixing errors is faster for developers. | |
113 | |
114 Args: | |
115 match: A re.MatchObject from matching against a line number regex. | |
116 | |
117 Returns: | |
118 The fixed up /file and :line number. | |
119 """ | |
120 real_file = self._processor.get_file_from_line(match.group(1)) | |
121 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number) | |
122 | |
123 def _filter_errors(self, errors): | |
124 """Removes some extraneous errors. For example, we ignore: | |
125 | |
126 Variable x first declared in /tmp/expanded/file | |
127 | |
128 Because it's just a duplicated error (it'll only ever show up 2+ times). | |
129 We also ignore Promose-based errors: | |
130 | |
131 found : function (VolumeInfo): (Promise<(DirectoryEntry|null)>|null) | |
132 required: (function (Promise<VolumeInfo>): ?|null|undefined) | |
133 | |
134 as templates don't work with Promises in all cases yet. See | |
135 https://github.com/google/closure-compiler/issues/715 for details. | |
136 | |
137 Args: | |
138 errors: A list of string errors extracted from Closure Compiler output. | |
139 | |
140 Return: | |
141 A slimmer, sleeker list of relevant errors (strings). | |
142 """ | |
143 first_declared_in = lambda e: " first declared in " not in e | |
144 return self._error_filter.filter(filter(first_declared_in, errors)) | |
145 | |
146 def _clean_up_error(self, error): | |
147 """Reverse the effects that funky <include> preprocessing steps have on | |
148 errors messages. | |
149 | |
150 Args: | |
151 error: A Closure compiler error (2 line string with error and source). | |
152 | |
153 Return: | |
154 The fixed up error string. | |
155 """ | |
156 expanded_file = self._expanded_file | |
157 fixed = re.sub("%s:(\d+)" % expanded_file, self._get_line_number, error) | |
158 return fixed.replace(expanded_file, os.path.abspath(self._file_arg)) | |
159 | |
160 def _format_errors(self, errors): | |
161 """Formats Closure compiler errors to easily spot compiler output. | |
162 | |
163 Args: | |
164 errors: A list of strings extracted from the Closure compiler's output. | |
165 | |
166 Returns: | |
167 A formatted output string. | |
168 """ | |
169 contents = "\n## ".join("\n\n".join(errors).splitlines()) | |
170 return "## %s" % contents if contents else "" | |
171 | |
172 def _create_temp_file(self, contents): | |
173 """Creates an owned temporary file with |contents|. | |
174 | |
175 Args: | |
176 content: A string of the file contens to write to a temporary file. | |
177 | |
178 Return: | |
179 The filepath of the newly created, written, and closed temporary file. | |
180 """ | |
181 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file: | |
182 self._temp_files.append(tmp_file.name) | |
183 tmp_file.write(contents) | |
184 return tmp_file.name | |
185 | |
186 def _run_js_check(self, sources, out_file, externs=None, | |
187 closure_args=None): | |
188 """Check |sources| for type errors. | |
189 | |
190 Args: | |
191 sources: Files to check. | |
192 out_file: A file where the compiled output is written to. | |
193 externs: @extern files that inform the compiler about custom globals. | |
194 closure_args: Arguments passed directly to the Closure compiler. | |
195 | |
196 Returns: | |
197 (errors, stderr) A parsed list of errors (strings) found by the compiler | |
198 and the raw stderr (as a string). | |
199 """ | |
200 args = ["--js=%s" % s for s in sources] | |
201 | |
202 assert out_file | |
203 | |
204 out_dir = os.path.dirname(out_file) | |
205 if not os.path.exists(out_dir): | |
206 os.makedirs(out_dir) | |
207 | |
208 checks_only = 'checks_only' in closure_args | |
209 | |
210 if not checks_only: | |
211 args += ["--js_output_file=%s" % out_file] | |
212 args += ["--create_source_map=%s" % (self._MAP_FILE_FORMAT % out_file)] | |
213 | |
214 args += ["--externs=%s" % e for e in externs or []] | |
215 | |
216 closure_args = closure_args or [] | |
217 closure_args += ["summary_detail_level=3", "continue_after_errors"] | |
218 args += ["--%s" % arg for arg in closure_args] | |
219 | |
220 self._log_debug("Args: %s" % " ".join(args)) | |
221 | |
222 _, stderr = self._run_jar(self._compiler_jar, args) | |
223 | |
224 errors = stderr.strip().split("\n\n") | |
225 maybe_summary = errors.pop() | |
226 | |
227 if re.search(".*error.*warning.*typed", maybe_summary): | |
228 self._log_debug("Summary: %s" % maybe_summary) | |
229 else: | |
230 # Not a summary. Running the jar failed. Bail. | |
231 self._log_error(stderr) | |
232 self._nuke_temp_files() | |
233 sys.exit(1) | |
234 | |
235 if errors: | |
236 if os.path.exists(out_file): | |
237 os.remove(out_file) | |
238 if os.path.exists(self._MAP_FILE_FORMAT % out_file): | |
239 os.remove(self._MAP_FILE_FORMAT % out_file) | |
240 elif checks_only: | |
241 # Compile succeeded but --checks_only disables --js_output_file from | |
242 # actually writing a file. Write a file ourselves so incremental builds | |
243 # still work. | |
244 with open(out_file, 'w') as f: | |
245 f.write('') | |
246 | |
247 return errors, stderr | |
248 | |
249 def check(self, source_file, out_file=None, depends=None, externs=None, | |
250 closure_args=None): | |
251 """Closure compiler |source_file| while checking for errors. | |
252 | |
253 Args: | |
254 source_file: A file to check. | |
255 out_file: A file where the compiled output is written to. | |
256 depends: Files that |source_file| requires to run (e.g. earlier <script>). | |
257 externs: @extern files that inform the compiler about custom globals. | |
258 closure_args: Arguments passed directly to the Closure compiler. | |
259 | |
260 Returns: | |
261 (found_errors, stderr) A boolean indicating whether errors were found and | |
262 the raw Closure compiler stderr (as a string). | |
263 """ | |
264 self._log_debug("FILE: %s" % source_file) | |
265 | |
266 if source_file.endswith("_externs.js"): | |
267 self._log_debug("Skipping externs: %s" % source_file) | |
268 return | |
269 | |
270 self._file_arg = source_file | |
271 | |
272 cwd, tmp_dir = os.getcwd(), tempfile.gettempdir() | |
273 rel_path = lambda f: os.path.join(os.path.relpath(cwd, tmp_dir), f) | |
274 | |
275 depends = depends or [] | |
276 includes = [rel_path(f) for f in depends + [source_file]] | |
277 contents = ['<include src="%s">' % i for i in includes] | |
278 meta_file = self._create_temp_file("\n".join(contents)) | |
279 self._log_debug("Meta file: %s" % meta_file) | |
280 | |
281 self._processor = processor.Processor(meta_file) | |
282 self._expanded_file = self._create_temp_file(self._processor.contents) | |
283 self._log_debug("Expanded file: %s" % self._expanded_file) | |
284 | |
285 errors, stderr = self._run_js_check([self._expanded_file], | |
286 out_file=out_file, externs=externs, | |
287 closure_args=closure_args) | |
288 filtered_errors = self._filter_errors(errors) | |
289 cleaned_errors = map(self._clean_up_error, filtered_errors) | |
290 output = self._format_errors(cleaned_errors) | |
291 | |
292 if cleaned_errors: | |
293 prefix = "\n" if output else "" | |
294 self._log_error("Error in: %s%s%s" % (source_file, prefix, output)) | |
295 elif output: | |
296 self._log_debug("Output: %s" % output) | |
297 | |
298 self._nuke_temp_files() | |
299 return bool(cleaned_errors), stderr | |
300 | |
301 def check_multiple(self, sources, out_file=None, externs=None, | |
302 closure_args=None): | |
303 """Closure compile a set of files and check for errors. | |
304 | |
305 Args: | |
306 sources: An array of files to check. | |
307 out_file: A file where the compiled output is written to. | |
308 externs: @extern files that inform the compiler about custom globals. | |
309 closure_args: Arguments passed directly to the Closure compiler. | |
310 | |
311 Returns: | |
312 (found_errors, stderr) A boolean indicating whether errors were found and | |
313 the raw Closure Compiler stderr (as a string). | |
314 """ | |
315 errors, stderr = self._run_js_check(sources, out_file=out_file, | |
316 externs=externs, | |
317 closure_args=closure_args) | |
318 self._nuke_temp_files() | |
319 return bool(errors), stderr | |
320 | |
321 | |
322 if __name__ == "__main__": | |
323 parser = argparse.ArgumentParser( | |
324 description="Typecheck JavaScript using Closure compiler") | |
325 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE, | |
326 help="Path to a source file to typecheck") | |
327 single_file_group = parser.add_mutually_exclusive_group() | |
328 single_file_group.add_argument("--single_file", dest="single_file", | |
329 action="store_true", | |
330 help="Process each source file individually") | |
331 # TODO(twellington): remove --no_single_file and use len(opts.sources). | |
332 single_file_group.add_argument("--no_single_file", dest="single_file", | |
333 action="store_false", | |
334 help="Process all source files as a group") | |
335 parser.add_argument("-d", "--depends", nargs=argparse.ZERO_OR_MORE) | |
336 parser.add_argument("-e", "--externs", nargs=argparse.ZERO_OR_MORE) | |
337 parser.add_argument("-o", "--out_file", required=True, | |
338 help="A file where the compiled output is written to") | |
339 parser.add_argument("-c", "--closure_args", nargs=argparse.ZERO_OR_MORE, | |
340 help="Arguments passed directly to the Closure compiler") | |
341 parser.add_argument("-v", "--verbose", action="store_true", | |
342 help="Show more information as this script runs") | |
343 | |
344 parser.set_defaults(single_file=True) | |
345 opts = parser.parse_args() | |
346 | |
347 depends = opts.depends or [] | |
348 # TODO(devlin): should we run normpath() on this first and/or do this for | |
349 # depends as well? | |
350 externs = set(opts.externs or []) | |
351 sources = set(opts.sources) | |
352 | |
353 externs.add(os.path.join(_CURRENT_DIR, "externs", "polymer-1.0.js")) | |
354 | |
355 checker = Checker(verbose=opts.verbose) | |
356 if opts.single_file: | |
357 for source in sources: | |
358 # Normalize source to the current directory. | |
359 source = os.path.normpath(os.path.join(os.getcwd(), source)) | |
360 depends, externs = build.inputs.resolve_recursive_dependencies( | |
361 source, depends, externs) | |
362 | |
363 found_errors, _ = checker.check(source, out_file=opts.out_file, | |
364 depends=depends, externs=externs, | |
365 closure_args=opts.closure_args) | |
366 if found_errors: | |
367 sys.exit(1) | |
368 else: | |
369 found_errors, stderr = checker.check_multiple( | |
370 sources, | |
371 out_file=opts.out_file, | |
372 externs=externs, | |
373 closure_args=opts.closure_args) | |
374 if found_errors: | |
375 print stderr | |
376 sys.exit(1) | |
OLD | NEW |