OLD | NEW |
---|---|
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 """A script to run Closure compiler on JavaScript file and check for errors.""" | |
Tyler Breisacher (Chromium)
2014/08/13 19:59:42
nit: "Runs Closure Compiler on ..."
"A script to"
Dan Beam
2014/08/13 20:23:45
Done.
| |
7 | |
6 import argparse | 8 import argparse |
7 import os | 9 import os |
8 import processor | |
9 import re | 10 import re |
10 import subprocess | 11 import subprocess |
11 import sys | 12 import sys |
12 import tempfile | 13 import tempfile |
14 import processor | |
Tyler Breisacher (Chromium)
2014/08/13 19:59:42
These are generally supposed to be alphabetical ri
Dan Beam
2014/08/13 20:23:45
processor is not a system import
| |
13 | 15 |
14 | 16 |
15 class Checker(object): | 17 class Checker(object): |
16 _common_closure_args = [ | 18 """A class to run the Closure compiler .jar on a given source file and return |
19 the success/errors.""" | |
20 | |
21 _COMMON_CLOSURE_ARGS = [ | |
17 "--accept_const_keyword", | 22 "--accept_const_keyword", |
18 "--language_in=ECMASCRIPT5", | 23 "--language_in=ECMASCRIPT5", |
19 "--summary_detail_level=3", | 24 "--summary_detail_level=3", |
20 "--warning_level=VERBOSE", | 25 "--warning_level=VERBOSE", |
21 "--jscomp_error=accessControls", | 26 "--jscomp_error=accessControls", |
22 "--jscomp_error=ambiguousFunctionDecl", | 27 "--jscomp_error=ambiguousFunctionDecl", |
23 "--jscomp_error=checkStructDictInheritance", | 28 "--jscomp_error=checkStructDictInheritance", |
24 "--jscomp_error=checkTypes", | 29 "--jscomp_error=checkTypes", |
25 "--jscomp_error=checkVars", | 30 "--jscomp_error=checkVars", |
26 "--jscomp_error=constantProperty", | 31 "--jscomp_error=constantProperty", |
27 "--jscomp_error=deprecated", | 32 "--jscomp_error=deprecated", |
28 "--jscomp_error=externsValidation", | 33 "--jscomp_error=externsValidation", |
29 "--jscomp_error=globalThis", | 34 "--jscomp_error=globalThis", |
30 "--jscomp_error=invalidCasts", | 35 "--jscomp_error=invalidCasts", |
31 "--jscomp_error=misplacedTypeAnnotation", | 36 "--jscomp_error=misplacedTypeAnnotation", |
32 "--jscomp_error=missingProperties", | 37 "--jscomp_error=missingProperties", |
33 "--jscomp_error=missingReturn", | 38 "--jscomp_error=missingReturn", |
34 "--jscomp_error=nonStandardJsDocs", | 39 "--jscomp_error=nonStandardJsDocs", |
35 "--jscomp_error=suspiciousCode", | 40 "--jscomp_error=suspiciousCode", |
36 "--jscomp_error=undefinedNames", | 41 "--jscomp_error=undefinedNames", |
37 "--jscomp_error=undefinedVars", | 42 "--jscomp_error=undefinedVars", |
38 "--jscomp_error=unknownDefines", | 43 "--jscomp_error=unknownDefines", |
39 "--jscomp_error=uselessCode", | 44 "--jscomp_error=uselessCode", |
40 "--jscomp_error=visibility", | 45 "--jscomp_error=visibility", |
41 # TODO(dbeam): happens when the same file is <include>d multiple times. | 46 # TODO(dbeam): happens when the same file is <include>d multiple times. |
42 "--jscomp_off=duplicate", | 47 "--jscomp_off=duplicate", |
43 ] | 48 ] |
44 | 49 |
45 _found_java = False | 50 _JAR_COMMAND = [ |
46 | |
47 _jar_command = [ | |
48 "java", | 51 "java", |
49 "-jar", | 52 "-jar", |
50 "-Xms1024m", | 53 "-Xms1024m", |
51 "-server", | 54 "-server", |
Tyler Breisacher (Chromium)
2014/08/13 19:59:42
Side note: I believe -client is generally recommen
Dan Beam
2014/08/13 20:23:45
Acknowledged.
| |
52 "-XX:+TieredCompilation" | 55 "-XX:+TieredCompilation" |
53 ] | 56 ] |
54 | 57 |
58 _found_java = False | |
Tyler Breisacher (Chromium)
2014/08/13 19:59:42
If I remember how Python works (which is a big if)
Dan Beam
2014/08/13 20:23:45
i don't want it to be an instance property
Tyler Breisacher (Chromium)
2014/08/13 20:27:01
It looks like it's being used that way at line 102
| |
59 | |
55 def __init__(self, verbose=False): | 60 def __init__(self, verbose=False): |
56 current_dir = os.path.join(os.path.dirname(__file__)) | 61 current_dir = os.path.join(os.path.dirname(__file__)) |
57 self._compiler_jar = os.path.join(current_dir, "lib", "compiler.jar") | 62 self._compiler_jar = os.path.join(current_dir, "lib", "compiler.jar") |
58 self._runner_jar = os.path.join(current_dir, "runner", "runner.jar") | 63 self._runner_jar = os.path.join(current_dir, "runner", "runner.jar") |
59 self._temp_files = [] | 64 self._temp_files = [] |
60 self._verbose = verbose | 65 self._verbose = verbose |
61 | 66 |
62 def _clean_up(self): | 67 def _clean_up(self): |
63 if not self._temp_files: | 68 if not self._temp_files: |
64 return | 69 return |
65 | 70 |
66 self._debug("Deleting temporary files: " + ", ".join(self._temp_files)) | 71 self._debug("Deleting temporary files: " + ", ".join(self._temp_files)) |
67 for f in self._temp_files: | 72 for f in self._temp_files: |
68 os.remove(f) | 73 os.remove(f) |
69 self._temp_files = [] | 74 self._temp_files = [] |
70 | 75 |
71 def _debug(self, msg, error=False): | 76 def _debug(self, msg, error=False): |
72 if self._verbose: | 77 if self._verbose: |
73 print "(INFO) " + msg | 78 print "(INFO) " + msg |
74 | 79 |
75 def _error(self, msg): | 80 def _error(self, msg): |
76 print >> sys.stderr, "(ERROR) " + msg | 81 print >> sys.stderr, "(ERROR) " + msg |
77 self._clean_up() | 82 self._clean_up() |
78 | 83 |
79 def _run_command(self, cmd): | 84 def _run_command(self, cmd): |
85 """Runs a shell command. | |
86 | |
87 Args: | |
88 cmd: A list of tokens to be joined into a shell command. | |
89 | |
90 Return: | |
91 True if the exit code was 0, else False. | |
92 """ | |
80 cmd_str = " ".join(cmd) | 93 cmd_str = " ".join(cmd) |
81 self._debug("Running command: " + cmd_str) | 94 self._debug("Running command: " + cmd_str) |
82 | 95 |
83 devnull = open(os.devnull, "w") | 96 devnull = open(os.devnull, "w") |
84 return subprocess.Popen( | 97 return subprocess.Popen( |
85 cmd_str, stdout=devnull, stderr=subprocess.PIPE, shell=True) | 98 cmd_str, stdout=devnull, stderr=subprocess.PIPE, shell=True) |
86 | 99 |
87 def _check_java_path(self): | 100 def _check_java_path(self): |
101 """Checks that `java` is on the system path.""" | |
88 if not self._found_java: | 102 if not self._found_java: |
89 proc = self._run_command(["which", "java"]) | 103 proc = self._run_command(["which", "java"]) |
90 proc.communicate() | 104 proc.communicate() |
91 if proc.returncode == 0: | 105 if proc.returncode == 0: |
92 self._found_java = True | 106 self._found_java = True |
93 else: | 107 else: |
94 self._error("Cannot find java (`which java` => %s)" % proc.returncode) | 108 self._error("Cannot find java (`which java` => %s)" % proc.returncode) |
95 | 109 |
96 return self._found_java | 110 return self._found_java |
97 | 111 |
98 def _run_jar(self, jar, args=[]): | 112 def _run_jar(self, jar, args=None): |
113 args = args or [] | |
99 self._check_java_path() | 114 self._check_java_path() |
100 return self._run_command(self._jar_command + [jar] + args) | 115 return self._run_command(self._JAR_COMMAND + [jar] + args) |
101 | 116 |
102 def _fix_line_number(self, match): | 117 def _fix_line_number(self, match): |
118 """Changes a line number from /tmp/file:300 to /orig/file:100. | |
119 | |
120 Args: | |
121 match: A re.MatchObject from matching against a line number regex. | |
122 | |
123 Returns: | |
124 The fixed up /file and :line number. | |
125 """ | |
103 real_file = self._processor.get_file_from_line(match.group(1)) | 126 real_file = self._processor.get_file_from_line(match.group(1)) |
104 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number) | 127 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number) |
105 | 128 |
106 def _fix_up_error(self, error): | 129 def _fix_up_error(self, error): |
130 """Filter out irrelevant errors or fix line numbers. | |
131 | |
132 Args: | |
133 error: An Closure compiler error (2 line string with error and source). | |
Tyler Breisacher (Chromium)
2014/08/13 19:59:41
s/An/A/
Dan Beam
2014/08/13 20:23:45
Done.
| |
134 | |
135 Return: | |
136 The fixed up erorr string (blank if it should be ignored). | |
137 """ | |
107 if " first declared in " in error: | 138 if " first declared in " in error: |
108 # Ignore "Variable x first declared in /same/file". | 139 # Ignore "Variable x first declared in /same/file". |
109 return "" | 140 return "" |
110 | 141 |
111 file = self._expanded_file | 142 expanded_file = self._expanded_file |
112 fixed = re.sub("%s:(\d+)" % file, self._fix_line_number, error) | 143 fixed = re.sub("%s:(\d+)" % expanded_file, self._fix_line_number, error) |
113 return fixed.replace(file, os.path.abspath(self._file_arg)) | 144 return fixed.replace(expanded_file, os.path.abspath(self._file_arg)) |
114 | 145 |
115 def _format_errors(self, errors): | 146 def _format_errors(self, errors): |
147 """Formats Closure compiler errors to easily spot compiler output.""" | |
116 errors = filter(None, errors) | 148 errors = filter(None, errors) |
117 contents = ("\n" + "## ").join("\n\n".join(errors).splitlines()) | 149 contents = ("\n" + "## ").join("\n\n".join(errors).splitlines()) |
118 return "## " + contents if contents else "" | 150 return "## " + contents if contents else "" |
119 | 151 |
120 def _create_temp_file(self, contents): | 152 def _create_temp_file(self, contents): |
121 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file: | 153 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file: |
122 self._temp_files.append(tmp_file.name) | 154 self._temp_files.append(tmp_file.name) |
123 tmp_file.write(contents) | 155 tmp_file.write(contents) |
124 return tmp_file.name | 156 return tmp_file.name |
125 | 157 |
126 def check(self, file, depends=[], externs=[]): | 158 def check(self, source_file, depends=None, externs=None): |
159 """Closure compile a file and check for errors. | |
160 | |
161 Args: | |
162 source_file: A file to check. | |
163 depends: Other files that would be included with a <script> earlier in | |
164 the page. | |
165 externs: @extern files that inform the compiler about custom globals. | |
166 | |
167 Returns: | |
168 (exitcode, output) The exit code of the Closure compiler (as a number) | |
169 and its output (as a string). | |
170 """ | |
171 depends = depends or [] | |
172 externs = externs or [] | |
173 | |
127 if not self._check_java_path(): | 174 if not self._check_java_path(): |
128 return 1, "" | 175 return 1, "" |
129 | 176 |
130 self._debug("FILE: " + file) | 177 self._debug("FILE: " + source_file) |
131 | 178 |
132 if file.endswith("_externs.js"): | 179 if source_file.endswith("_externs.js"): |
133 self._debug("Skipping externs: " + file) | 180 self._debug("Skipping externs: " + source_file) |
134 return | 181 return |
135 | 182 |
136 self._file_arg = file | 183 self._file_arg = source_file |
137 | 184 |
138 tmp_dir = tempfile.gettempdir() | 185 tmp_dir = tempfile.gettempdir() |
139 rel_path = lambda f: os.path.join(os.path.relpath(os.getcwd(), tmp_dir), f) | 186 rel_path = lambda f: os.path.join(os.path.relpath(os.getcwd(), tmp_dir), f) |
140 | 187 |
141 contents = ['<include src="%s">' % rel_path(f) for f in depends + [file]] | 188 includes = [rel_path(f) for f in depends + [source_file]] |
189 contents = ['<include src="%s">' % i for i in includes] | |
142 meta_file = self._create_temp_file("\n".join(contents)) | 190 meta_file = self._create_temp_file("\n".join(contents)) |
143 self._debug("Meta file: " + meta_file) | 191 self._debug("Meta file: " + meta_file) |
144 | 192 |
145 self._processor = processor.Processor(meta_file) | 193 self._processor = processor.Processor(meta_file) |
146 self._expanded_file = self._create_temp_file(self._processor.contents) | 194 self._expanded_file = self._create_temp_file(self._processor.contents) |
147 self._debug("Expanded file: " + self._expanded_file) | 195 self._debug("Expanded file: " + self._expanded_file) |
148 | 196 |
149 args = ["--js=" + self._expanded_file] + ["--externs=" + e for e in externs] | 197 args = ["--js=" + self._expanded_file] + ["--externs=" + e for e in externs] |
150 args_file_content = " " + " ".join(self._common_closure_args + args) | 198 args_file_content = " " + " ".join(self._COMMON_CLOSURE_ARGS + args) |
151 self._debug("Args: " + args_file_content.strip()) | 199 self._debug("Args: " + args_file_content.strip()) |
152 | 200 |
153 args_file = self._create_temp_file(args_file_content) | 201 args_file = self._create_temp_file(args_file_content) |
154 self._debug("Args file: " + args_file) | 202 self._debug("Args file: " + args_file) |
155 | 203 |
156 runner_args = ["--compiler-args-file=" + args_file] | 204 runner_args = ["--compiler-args-file=" + args_file] |
157 runner_cmd = self._run_jar(self._runner_jar, args=runner_args) | 205 runner_cmd = self._run_jar(self._runner_jar, args=runner_args) |
158 (_, stderr) = runner_cmd.communicate() | 206 (_, stderr) = runner_cmd.communicate() |
159 | 207 |
160 errors = stderr.strip().split("\n\n") | 208 errors = stderr.strip().split("\n\n") |
161 self._debug("Summary: " + errors.pop()) | 209 self._debug("Summary: " + errors.pop()) |
162 | 210 |
163 output = self._format_errors(map(self._fix_up_error, errors)) | 211 output = self._format_errors(map(self._fix_up_error, errors)) |
164 if runner_cmd.returncode: | 212 if runner_cmd.returncode: |
165 self._error("Error in: " + file + ("\n" + output if output else "")) | 213 self._error("Error in: " + source_file + ("\n" + output if output else "") ) |
166 elif output: | 214 elif output: |
167 self._debug("Output: " + output) | 215 self._debug("Output: " + output) |
168 | 216 |
169 self._clean_up() | 217 self._clean_up() |
170 | 218 |
171 return runner_cmd.returncode, output | 219 return runner_cmd.returncode, output |
172 | 220 |
173 | 221 |
174 if __name__ == "__main__": | 222 if __name__ == "__main__": |
175 parser = argparse.ArgumentParser( | 223 parser = argparse.ArgumentParser( |
(...skipping 11 matching lines...) Expand all Loading... | |
187 for source in opts.sources: | 235 for source in opts.sources: |
188 if not checker.check(source, depends=opts.depends, externs=opts.externs): | 236 if not checker.check(source, depends=opts.depends, externs=opts.externs): |
189 sys.exit(1) | 237 sys.exit(1) |
190 | 238 |
191 if opts.out_file: | 239 if opts.out_file: |
192 out_dir = os.path.dirname(opts.out_file) | 240 out_dir = os.path.dirname(opts.out_file) |
193 if not os.path.exists(out_dir): | 241 if not os.path.exists(out_dir): |
194 os.makedirs(out_dir) | 242 os.makedirs(out_dir) |
195 # TODO(dbeam): write compiled file to |opts.out_file|. | 243 # TODO(dbeam): write compiled file to |opts.out_file|. |
196 open(opts.out_file, "w").write("") | 244 open(opts.out_file, "w").write("") |
OLD | NEW |