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."""
20 class Checker(object):
21 """Runs the Closure compiler on a given source file and returns the
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",
72 "-XX:+TieredCompilation"
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")
81 self
._verbose
= verbose
83 self
._error
_filter
= error_filter
.PromiseErrorFilter()
86 if not self
._temp
_files
:
89 self
._debug
("Deleting temporary files: %s" % ", ".join(self
._temp
_files
))
90 for f
in self
._temp
_files
:
94 def _debug(self
, msg
, error
=False):
96 print "(INFO) %s" % msg
98 def _error(self
, msg
):
99 print >> sys
.stderr
, "(ERROR) %s" % msg
102 def _common_args(self
):
103 """Returns an array of the common closure compiler args."""
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.
112 cmd: A list of tokens to be joined into a shell command.
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"])
129 if proc
.returncode
== 0:
130 self
._found
_java
= True
132 self
._error
("Cannot find java (`which java` => %s)" % proc
.returncode
)
134 return self
._found
_java
136 def _run_jar(self
, jar
, args
=None):
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.
145 match: A re.MatchObject from matching against a line number regex.
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.
157 error: A Closure compiler error (2 line string with error and source).
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".
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
)
182 def _run_js_check(self
, sources
, out_file
=None, externs
=None):
183 if not self
._check
_java
_path
():
186 args
= ["--js=%s" % s
for s
in sources
]
189 args
+= ["--js_output_file=%s" % out_file
]
190 args
+= ["--create_source_map=%s.map" % out_file
]
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())
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.
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
219 externs: @extern files that inform the compiler about custom globals.
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
():
231 self
._debug
("FILE: %s" % source_file
)
233 if source_file
.endswith("_externs.js"):
234 self
._debug
("Skipping externs: %s" % source_file
)
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
))
260 prefix
= "\n" if output
else ""
261 self
._error
("Error in: %s%s%s" % (source_file
, prefix
, 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.
271 sources: An array of files to check.
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",
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()
311 out_dir
= os
.path
.dirname(opts
.out_file
)
312 if not os
.path
.exists(out_dir
):
315 checker
= Checker(verbose
=opts
.verbose
, strict
=opts
.strict
)
317 for source
in opts
.sources
:
318 depends
, externs
= build
.inputs
.resolve_recursive_dependencies(
322 has_errors
, _
= checker
.check(source
, out_file
=opts
.out_file
,
323 depends
=depends
, externs
=externs
)
328 has_errors
, errors
= checker
.check_multiple(opts
.sources
)
333 if opts
.success_stamp
:
334 with
open(opts
.success_stamp
, 'w'):
335 os
.utime(opts
.success_stamp
, None)