Add per-user preferences support.
[chromium-blink-merge.git] / third_party / closure_compiler / checker.py
blob5d372b21304912c4305ea2e9a9fc23942f7f692c
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.
6 """Runs Closure compiler on a JavaScript file to check for errors."""
8 import argparse
9 import os
10 import re
11 import subprocess
12 import sys
13 import tempfile
15 import build.inputs
16 import processor
17 import error_filter
20 class Checker(object):
21 """Runs the Closure compiler on a given source file and returns the
22 success/errors."""
24 _COMMON_CLOSURE_ARGS = [
25 "--accept_const_keyword",
26 "--jscomp_error=accessControls",
27 "--jscomp_error=ambiguousFunctionDecl",
28 "--jscomp_error=checkStructDictInheritance",
29 "--jscomp_error=checkTypes",
30 "--jscomp_error=checkVars",
31 "--jscomp_error=constantProperty",
32 "--jscomp_error=deprecated",
33 "--jscomp_error=externsValidation",
34 "--jscomp_error=globalThis",
35 "--jscomp_error=invalidCasts",
36 "--jscomp_error=missingProperties",
37 "--jscomp_error=missingReturn",
38 "--jscomp_error=nonStandardJsDocs",
39 "--jscomp_error=suspiciousCode",
40 "--jscomp_error=undefinedNames",
41 "--jscomp_error=undefinedVars",
42 "--jscomp_error=unknownDefines",
43 "--jscomp_error=uselessCode",
44 "--jscomp_error=visibility",
45 "--language_in=ECMASCRIPT5_STRICT",
46 "--summary_detail_level=3",
47 "--compilation_level=SIMPLE_OPTIMIZATIONS",
48 "--source_map_format=V3",
51 # These are the extra flags used when compiling in 'strict' mode.
52 # Flags that are normally disabled are turned on for strict mode.
53 _STRICT_CLOSURE_ARGS = [
54 "--jscomp_error=reportUnknownTypes",
55 "--jscomp_error=duplicate",
56 "--jscomp_error=misplacedTypeAnnotation",
59 _DISABLED_CLOSURE_ARGS = [
60 # TODO(dbeam): happens when the same file is <include>d multiple times.
61 "--jscomp_off=duplicate",
62 # TODO(fukino): happens when cr.defineProperty() has a type annotation.
63 # Avoiding parse-time warnings needs 2 pass compiling. crbug.com/421562.
64 "--jscomp_off=misplacedTypeAnnotation",
67 _JAR_COMMAND = [
68 "java",
69 "-jar",
70 "-Xms1024m",
71 "-client",
72 "-XX:+TieredCompilation"
75 _found_java = False
77 def __init__(self, verbose=False, strict=False):
78 current_dir = os.path.join(os.path.dirname(__file__))
79 self._runner_jar = os.path.join(current_dir, "runner", "runner.jar")
80 self._temp_files = []
81 self._verbose = verbose
82 self._strict = strict
83 self._error_filter = error_filter.PromiseErrorFilter()
85 def _clean_up(self):
86 if not self._temp_files:
87 return
89 self._debug("Deleting temporary files: %s" % ", ".join(self._temp_files))
90 for f in self._temp_files:
91 os.remove(f)
92 self._temp_files = []
94 def _debug(self, msg, error=False):
95 if self._verbose:
96 print "(INFO) %s" % msg
98 def _error(self, msg):
99 print >> sys.stderr, "(ERROR) %s" % msg
100 self._clean_up()
102 def _common_args(self):
103 """Returns an array of the common closure compiler args."""
104 if self._strict:
105 return self._COMMON_CLOSURE_ARGS + self._STRICT_CLOSURE_ARGS
106 return self._COMMON_CLOSURE_ARGS + self._DISABLED_CLOSURE_ARGS
108 def _run_command(self, cmd):
109 """Runs a shell command.
111 Args:
112 cmd: A list of tokens to be joined into a shell command.
114 Return:
115 True if the exit code was 0, else False.
117 cmd_str = " ".join(cmd)
118 self._debug("Running command: %s" % cmd_str)
120 devnull = open(os.devnull, "w")
121 return subprocess.Popen(
122 cmd_str, stdout=devnull, stderr=subprocess.PIPE, shell=True)
124 def _check_java_path(self):
125 """Checks that `java` is on the system path."""
126 if not self._found_java:
127 proc = self._run_command(["which", "java"])
128 proc.communicate()
129 if proc.returncode == 0:
130 self._found_java = True
131 else:
132 self._error("Cannot find java (`which java` => %s)" % proc.returncode)
134 return self._found_java
136 def _run_jar(self, jar, args=None):
137 args = args or []
138 self._check_java_path()
139 return self._run_command(self._JAR_COMMAND + [jar] + args)
141 def _fix_line_number(self, match):
142 """Changes a line number from /tmp/file:300 to /orig/file:100.
144 Args:
145 match: A re.MatchObject from matching against a line number regex.
147 Returns:
148 The fixed up /file and :line number.
150 real_file = self._processor.get_file_from_line(match.group(1))
151 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number)
153 def _fix_up_error(self, error):
154 """Filter out irrelevant errors or fix line numbers.
156 Args:
157 error: A Closure compiler error (2 line string with error and source).
159 Return:
160 The fixed up error string (blank if it should be ignored).
162 if " first declared in " in error:
163 # Ignore "Variable x first declared in /same/file".
164 return ""
166 expanded_file = self._expanded_file
167 fixed = re.sub("%s:(\d+)" % expanded_file, self._fix_line_number, error)
168 return fixed.replace(expanded_file, os.path.abspath(self._file_arg))
170 def _format_errors(self, errors):
171 """Formats Closure compiler errors to easily spot compiler output."""
172 errors = filter(None, errors)
173 contents = "\n## ".join("\n\n".join(errors).splitlines())
174 return "## %s" % contents if contents else ""
176 def _create_temp_file(self, contents):
177 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file:
178 self._temp_files.append(tmp_file.name)
179 tmp_file.write(contents)
180 return tmp_file.name
182 def _run_js_check(self, sources, out_file=None, externs=None):
183 if not self._check_java_path():
184 return 1, ""
186 args = ["--js=%s" % s for s in sources]
188 if out_file:
189 args += ["--js_output_file=%s" % out_file]
190 args += ["--create_source_map=%s.map" % out_file]
192 if externs:
193 args += ["--externs=%s" % e for e in externs]
194 args_file_content = " %s" % " ".join(self._common_args() + args)
195 self._debug("Args: %s" % args_file_content.strip())
197 args_file = self._create_temp_file(args_file_content)
198 self._debug("Args file: %s" % args_file)
200 runner_args = ["--compiler-args-file=%s" % args_file]
201 runner_cmd = self._run_jar(self._runner_jar, args=runner_args)
202 _, stderr = runner_cmd.communicate()
204 errors = stderr.strip().split("\n\n")
205 self._debug("Summary: %s" % errors.pop())
207 self._clean_up()
209 return errors, stderr
211 def check(self, source_file, out_file=None, depends=None, externs=None):
212 """Closure compile a file and check for errors.
214 Args:
215 source_file: A file to check.
216 out_file: A file where the compiled output is written to.
217 depends: Other files that would be included with a <script> earlier in
218 the page.
219 externs: @extern files that inform the compiler about custom globals.
221 Returns:
222 (has_errors, output) A boolean indicating if there were errors and the
223 Closure compiler output (as a string).
225 depends = depends or []
226 externs = externs or set()
228 if not self._check_java_path():
229 return 1, ""
231 self._debug("FILE: %s" % source_file)
233 if source_file.endswith("_externs.js"):
234 self._debug("Skipping externs: %s" % source_file)
235 return
237 self._file_arg = source_file
239 tmp_dir = tempfile.gettempdir()
240 rel_path = lambda f: os.path.join(os.path.relpath(os.getcwd(), tmp_dir), f)
242 includes = [rel_path(f) for f in depends + [source_file]]
243 contents = ['<include src="%s">' % i for i in includes]
244 meta_file = self._create_temp_file("\n".join(contents))
245 self._debug("Meta file: %s" % meta_file)
247 self._processor = processor.Processor(meta_file)
248 self._expanded_file = self._create_temp_file(self._processor.contents)
249 self._debug("Expanded file: %s" % self._expanded_file)
251 errors, stderr = self._run_js_check([self._expanded_file],
252 out_file=out_file, externs=externs)
254 # Filter out false-positive promise chain errors.
255 # See https://github.com/google/closure-compiler/issues/715 for details.
256 errors = self._error_filter.filter(errors);
258 output = self._format_errors(map(self._fix_up_error, errors))
259 if errors:
260 prefix = "\n" if output else ""
261 self._error("Error in: %s%s%s" % (source_file, prefix, output))
262 elif output:
263 self._debug("Output: %s" % output)
265 return bool(errors), output
267 def check_multiple(self, sources):
268 """Closure compile a set of files and check for errors.
270 Args:
271 sources: An array of files to check.
273 Returns:
274 (has_errors, output) A boolean indicating if there were errors and the
275 Closure compiler output (as a string).
278 errors, stderr = self._run_js_check(sources)
279 return bool(errors), stderr
281 if __name__ == "__main__":
282 parser = argparse.ArgumentParser(
283 description="Typecheck JavaScript using Closure compiler")
284 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE,
285 help="Path to a source file to typecheck")
286 single_file_group = parser.add_mutually_exclusive_group()
287 single_file_group.add_argument("--single-file", dest="single_file",
288 action="store_true",
289 help="Process each source file individually")
290 single_file_group.add_argument("--no-single-file", dest="single_file",
291 action="store_false",
292 help="Process all source files as a group")
293 parser.add_argument("-d", "--depends", nargs=argparse.ZERO_OR_MORE)
294 parser.add_argument("-e", "--externs", nargs=argparse.ZERO_OR_MORE)
295 parser.add_argument("-o", "--out_file",
296 help="A file where the compiled output is written to")
297 parser.add_argument("-v", "--verbose", action="store_true",
298 help="Show more information as this script runs")
299 parser.add_argument("--strict", action="store_true",
300 help="Enable strict type checking")
301 parser.add_argument("--success-stamp",
302 help="Timestamp file to update upon success")
304 parser.set_defaults(single_file=True, strict=False)
305 opts = parser.parse_args()
307 depends = opts.depends or []
308 externs = opts.externs or set()
310 if opts.out_file:
311 out_dir = os.path.dirname(opts.out_file)
312 if not os.path.exists(out_dir):
313 os.makedirs(out_dir)
315 checker = Checker(verbose=opts.verbose, strict=opts.strict)
316 if opts.single_file:
317 for source in opts.sources:
318 depends, externs = build.inputs.resolve_recursive_dependencies(
319 source,
320 depends,
321 externs)
322 has_errors, _ = checker.check(source, out_file=opts.out_file,
323 depends=depends, externs=externs)
324 if has_errors:
325 sys.exit(1)
327 else:
328 has_errors, errors = checker.check_multiple(opts.sources)
329 if has_errors:
330 print errors
331 sys.exit(1)
333 if opts.success_stamp:
334 with open(opts.success_stamp, 'w'):
335 os.utime(opts.success_stamp, None)