Roll src/third_party/WebKit bf18a82:a9cee16 (svn 185297:185304)
[chromium-blink-merge.git] / third_party / closure_compiler / checker.py
blobb3b75971c416dc1bf287df6c8c49504b6af96170
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
19 class Checker(object):
20 """Runs the Closure compiler on a given source file and returns the
21 success/errors."""
23 _COMMON_CLOSURE_ARGS = [
24 "--accept_const_keyword",
25 "--jscomp_error=accessControls",
26 "--jscomp_error=ambiguousFunctionDecl",
27 "--jscomp_error=checkStructDictInheritance",
28 "--jscomp_error=checkTypes",
29 "--jscomp_error=checkVars",
30 "--jscomp_error=constantProperty",
31 "--jscomp_error=deprecated",
32 "--jscomp_error=externsValidation",
33 "--jscomp_error=globalThis",
34 "--jscomp_error=invalidCasts",
35 "--jscomp_error=missingProperties",
36 "--jscomp_error=missingReturn",
37 "--jscomp_error=nonStandardJsDocs",
38 "--jscomp_error=suspiciousCode",
39 "--jscomp_error=undefinedNames",
40 "--jscomp_error=undefinedVars",
41 "--jscomp_error=unknownDefines",
42 "--jscomp_error=uselessCode",
43 "--jscomp_error=visibility",
44 # TODO(dbeam): happens when the same file is <include>d multiple times.
45 "--jscomp_off=duplicate",
46 # TODO(fukino): happens when cr.defineProperty() has a type annotation.
47 # Avoiding parse-time warnings needs 2 pass compiling. crbug.com/421562.
48 "--jscomp_off=misplacedTypeAnnotation",
49 "--language_in=ECMASCRIPT5_STRICT",
50 "--summary_detail_level=3",
53 _JAR_COMMAND = [
54 "java",
55 "-jar",
56 "-Xms1024m",
57 "-client",
58 "-XX:+TieredCompilation"
61 _found_java = False
63 def __init__(self, verbose=False):
64 current_dir = os.path.join(os.path.dirname(__file__))
65 self._compiler_jar = os.path.join(current_dir, "lib", "compiler.jar")
66 self._runner_jar = os.path.join(current_dir, "runner", "runner.jar")
67 self._temp_files = []
68 self._verbose = verbose
70 def _clean_up(self):
71 if not self._temp_files:
72 return
74 self._debug("Deleting temporary files: %s" % ", ".join(self._temp_files))
75 for f in self._temp_files:
76 os.remove(f)
77 self._temp_files = []
79 def _debug(self, msg, error=False):
80 if self._verbose:
81 print "(INFO) %s" % msg
83 def _error(self, msg):
84 print >> sys.stderr, "(ERROR) %s" % msg
85 self._clean_up()
87 def _run_command(self, cmd):
88 """Runs a shell command.
90 Args:
91 cmd: A list of tokens to be joined into a shell command.
93 Return:
94 True if the exit code was 0, else False.
95 """
96 cmd_str = " ".join(cmd)
97 self._debug("Running command: %s" % cmd_str)
99 devnull = open(os.devnull, "w")
100 return subprocess.Popen(
101 cmd_str, stdout=devnull, stderr=subprocess.PIPE, shell=True)
103 def _check_java_path(self):
104 """Checks that `java` is on the system path."""
105 if not self._found_java:
106 proc = self._run_command(["which", "java"])
107 proc.communicate()
108 if proc.returncode == 0:
109 self._found_java = True
110 else:
111 self._error("Cannot find java (`which java` => %s)" % proc.returncode)
113 return self._found_java
115 def _run_jar(self, jar, args=None):
116 args = args or []
117 self._check_java_path()
118 return self._run_command(self._JAR_COMMAND + [jar] + args)
120 def _fix_line_number(self, match):
121 """Changes a line number from /tmp/file:300 to /orig/file:100.
123 Args:
124 match: A re.MatchObject from matching against a line number regex.
126 Returns:
127 The fixed up /file and :line number.
129 real_file = self._processor.get_file_from_line(match.group(1))
130 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number)
132 def _fix_up_error(self, error):
133 """Filter out irrelevant errors or fix line numbers.
135 Args:
136 error: A Closure compiler error (2 line string with error and source).
138 Return:
139 The fixed up erorr string (blank if it should be ignored).
141 if " first declared in " in error:
142 # Ignore "Variable x first declared in /same/file".
143 return ""
145 expanded_file = self._expanded_file
146 fixed = re.sub("%s:(\d+)" % expanded_file, self._fix_line_number, error)
147 return fixed.replace(expanded_file, os.path.abspath(self._file_arg))
149 def _format_errors(self, errors):
150 """Formats Closure compiler errors to easily spot compiler output."""
151 errors = filter(None, errors)
152 contents = "\n## ".join("\n\n".join(errors).splitlines())
153 return "## %s" % contents if contents else ""
155 def _create_temp_file(self, contents):
156 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file:
157 self._temp_files.append(tmp_file.name)
158 tmp_file.write(contents)
159 return tmp_file.name
161 def check(self, source_file, depends=None, externs=None):
162 """Closure compile a file and check for errors.
164 Args:
165 source_file: A file to check.
166 depends: Other files that would be included with a <script> earlier in
167 the page.
168 externs: @extern files that inform the compiler about custom globals.
170 Returns:
171 (exitcode, output) The exit code of the Closure compiler (as a number)
172 and its output (as a string).
174 depends = depends or []
175 externs = externs or set()
177 if not self._check_java_path():
178 return 1, ""
180 self._debug("FILE: %s" % source_file)
182 if source_file.endswith("_externs.js"):
183 self._debug("Skipping externs: %s" % source_file)
184 return
186 self._file_arg = source_file
188 tmp_dir = tempfile.gettempdir()
189 rel_path = lambda f: os.path.join(os.path.relpath(os.getcwd(), tmp_dir), f)
191 includes = [rel_path(f) for f in depends + [source_file]]
192 contents = ['<include src="%s">' % i for i in includes]
193 meta_file = self._create_temp_file("\n".join(contents))
194 self._debug("Meta file: %s" % meta_file)
196 self._processor = processor.Processor(meta_file)
197 self._expanded_file = self._create_temp_file(self._processor.contents)
198 self._debug("Expanded file: %s" % self._expanded_file)
200 args = ["--js=%s" % self._expanded_file]
201 args += ["--externs=%s" % e for e in externs]
202 args_file_content = " %s" % " ".join(self._COMMON_CLOSURE_ARGS + args)
203 self._debug("Args: %s" % args_file_content.strip())
205 args_file = self._create_temp_file(args_file_content)
206 self._debug("Args file: %s" % args_file)
208 runner_args = ["--compiler-args-file=%s" % args_file]
209 runner_cmd = self._run_jar(self._runner_jar, args=runner_args)
210 (_, stderr) = runner_cmd.communicate()
212 errors = stderr.strip().split("\n\n")
213 self._debug("Summary: %s" % errors.pop())
215 output = self._format_errors(map(self._fix_up_error, errors))
216 if errors:
217 self._error("Error in: %s%s" % (source_file, "\n" + output if output else ""))
218 elif output:
219 self._debug("Output: %s" % output)
221 self._clean_up()
223 return bool(errors), output
226 if __name__ == "__main__":
227 parser = argparse.ArgumentParser(
228 description="Typecheck JavaScript using Closure compiler")
229 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE,
230 help="Path to a source file to typecheck")
231 parser.add_argument("-d", "--depends", nargs=argparse.ZERO_OR_MORE)
232 parser.add_argument("-e", "--externs", nargs=argparse.ZERO_OR_MORE)
233 parser.add_argument("-o", "--out_file", help="A place to output results")
234 parser.add_argument("-v", "--verbose", action="store_true",
235 help="Show more information as this script runs")
236 opts = parser.parse_args()
238 checker = Checker(verbose=opts.verbose)
239 for source in opts.sources:
240 depends, externs = build.inputs.resolve_recursive_dependencies(
241 source,
242 opts.depends,
243 opts.externs)
244 exit, _ = checker.check(source, depends=depends, externs=externs)
245 if exit != 0:
246 sys.exit(exit)
248 if opts.out_file:
249 out_dir = os.path.dirname(opts.out_file)
250 if not os.path.exists(out_dir):
251 os.makedirs(out_dir)
252 # TODO(dbeam): write compiled file to |opts.out_file|.
253 open(opts.out_file, "w").write("")