OLD | NEW |
| 1 #!/usr/bin/python |
1 # Copyright 2014 The Chromium Authors. All rights reserved. | 2 # Copyright 2014 The Chromium Authors. All rights reserved. |
2 # 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 |
3 # found in the LICENSE file. | 4 # found in the LICENSE file. |
4 | 5 |
5 from collections import defaultdict | 6 import argparse |
6 import os | 7 import os |
| 8 import processor |
7 import re | 9 import re |
8 import subprocess | 10 import subprocess |
9 import sys | 11 import sys |
10 import tempfile | 12 import tempfile |
11 | 13 |
12 | 14 |
13 | |
14 class LineNumber(object): | |
15 def __init__(self, file, line_number): | |
16 self.file = file | |
17 self.line_number = int(line_number) | |
18 | |
19 | |
20 class FileCache(object): | |
21 _cache = defaultdict(str) | |
22 | |
23 def _read(self, file): | |
24 file = os.path.abspath(file) | |
25 self._cache[file] = self._cache[file] or open(file, "r").read() | |
26 return self._cache[file] | |
27 | |
28 @staticmethod | |
29 def read(file): | |
30 return FileCache()._read(file) | |
31 | |
32 | |
33 class Flattener(object): | |
34 _IF_TAGS_REG = "</?if[^>]*?>" | |
35 _INCLUDE_REG = "<include[^>]+src=['\"]([^>]*)['\"]>" | |
36 | |
37 def __init__(self, file): | |
38 self.index = 0 | |
39 self.lines = self._get_file(file) | |
40 | |
41 while self.index < len(self.lines): | |
42 current_line = self.lines[self.index] | |
43 match = re.search(self._INCLUDE_REG, current_line[2]) | |
44 if match: | |
45 file_dir = os.path.dirname(current_line[0]) | |
46 self._inline_file(os.path.join(file_dir, match.group(1))) | |
47 else: | |
48 self.index += 1 | |
49 | |
50 # Replace every occurrence of tags like <if expr="..."> and </if> | |
51 # with an empty string. | |
52 for i, line in enumerate(self.lines): | |
53 self.lines[i] = line[:2] + (re.sub(self._IF_TAGS_REG, "", line[2]),) | |
54 | |
55 self.contents = "\n".join(l[2] for l in self.lines) | |
56 | |
57 # Returns a list of tuples in the format: (file, line number, line contents). | |
58 def _get_file(self, file): | |
59 lines = FileCache.read(file).splitlines() | |
60 return [(file, lnum + 1, line) for lnum, line in enumerate(lines)] | |
61 | |
62 def _inline_file(self, file): | |
63 lines = self._get_file(file) | |
64 self.lines = self.lines[:self.index] + lines + self.lines[self.index + 1:] | |
65 | |
66 def get_file_from_line(self, line_number): | |
67 line_number = int(line_number) - 1 | |
68 return LineNumber(self.lines[line_number][0], self.lines[line_number][1]) | |
69 | |
70 | |
71 class Checker(object): | 15 class Checker(object): |
72 _common_closure_args = [ | 16 _common_closure_args = [ |
73 "--accept_const_keyword", | 17 "--accept_const_keyword", |
74 "--language_in=ECMASCRIPT5", | 18 "--language_in=ECMASCRIPT5", |
75 "--summary_detail_level=3", | 19 "--summary_detail_level=3", |
76 "--warning_level=VERBOSE", | 20 "--warning_level=VERBOSE", |
77 "--jscomp_error=accessControls", | 21 "--jscomp_error=accessControls", |
78 "--jscomp_error=ambiguousFunctionDecl", | 22 "--jscomp_error=ambiguousFunctionDecl", |
79 "--jscomp_error=checkStructDictInheritance", | 23 "--jscomp_error=checkStructDictInheritance", |
80 "--jscomp_error=checkTypes", | 24 "--jscomp_error=checkTypes", |
(...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
149 else: | 93 else: |
150 self._error("Cannot find java (`which java` => %s)" % proc.returncode) | 94 self._error("Cannot find java (`which java` => %s)" % proc.returncode) |
151 | 95 |
152 return self._found_java | 96 return self._found_java |
153 | 97 |
154 def _run_jar(self, jar, args=[]): | 98 def _run_jar(self, jar, args=[]): |
155 self._check_java_path() | 99 self._check_java_path() |
156 return self._run_command(self._jar_command + [jar] + args) | 100 return self._run_command(self._jar_command + [jar] + args) |
157 | 101 |
158 def _fix_line_number(self, match): | 102 def _fix_line_number(self, match): |
159 real_file = self._flattener.get_file_from_line(match.group(1)) | 103 real_file = self._processor.get_file_from_line(match.group(1)) |
160 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number) | 104 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number) |
161 | 105 |
162 def _fix_up_error(self, error): | 106 def _fix_up_error(self, error): |
163 if " first declared in " in error: | 107 if " first declared in " in error: |
164 # Ignore "Variable x first declared in /same/file". | 108 # Ignore "Variable x first declared in /same/file". |
165 return "" | 109 return "" |
166 | 110 |
167 file = self._expanded_file | 111 file = self._expanded_file |
168 fixed = re.sub("%s:(\d+)" % file, self._fix_line_number, error) | 112 fixed = re.sub("%s:(\d+)" % file, self._fix_line_number, error) |
169 return fixed.replace(file, os.path.abspath(self._file_arg)) | 113 return fixed.replace(file, os.path.abspath(self._file_arg)) |
170 | 114 |
171 def _format_errors(self, errors): | 115 def _format_errors(self, errors): |
172 errors = filter(None, errors) | 116 errors = filter(None, errors) |
173 contents = ("\n" + "## ").join("\n\n".join(errors).splitlines()) | 117 contents = ("\n" + "## ").join("\n\n".join(errors).splitlines()) |
174 return "## " + contents if contents else "" | 118 return "## " + contents if contents else "" |
175 | 119 |
176 def _create_temp_file(self, contents): | 120 def _create_temp_file(self, contents): |
177 with tempfile.NamedTemporaryFile(mode='wt', delete=False) as tmp_file: | 121 with tempfile.NamedTemporaryFile(mode="wt", delete=False) as tmp_file: |
178 self._temp_files.append(tmp_file.name) | 122 self._temp_files.append(tmp_file.name) |
179 tmp_file.write(contents) | 123 tmp_file.write(contents) |
180 return tmp_file.name | 124 return tmp_file.name |
181 | 125 |
182 def check(self, file, depends=[], externs=[]): | 126 def check(self, file, depends=[], externs=[]): |
183 if not self._check_java_path(): | 127 if not self._check_java_path(): |
184 return 1, "" | 128 return 1, "" |
185 | 129 |
186 self._debug("FILE: " + file) | 130 self._debug("FILE: " + file) |
187 | 131 |
188 if file.endswith("_externs.js"): | 132 if file.endswith("_externs.js"): |
189 self._debug("Skipping externs: " + file) | 133 self._debug("Skipping externs: " + file) |
190 return | 134 return |
191 | 135 |
192 self._file_arg = file | 136 self._file_arg = file |
193 | 137 |
194 tmp_dir = tempfile.gettempdir() | 138 tmp_dir = tempfile.gettempdir() |
195 rel_path = lambda f: os.path.join(os.path.relpath(os.getcwd(), tmp_dir), f) | 139 rel_path = lambda f: os.path.join(os.path.relpath(os.getcwd(), tmp_dir), f) |
196 | 140 |
197 contents = ['<include src="%s">' % rel_path(f) for f in depends + [file]] | 141 contents = ['<include src="%s">' % rel_path(f) for f in depends + [file]] |
198 meta_file = self._create_temp_file("\n".join(contents)) | 142 meta_file = self._create_temp_file("\n".join(contents)) |
199 self._debug("Meta file: " + meta_file) | 143 self._debug("Meta file: " + meta_file) |
200 | 144 |
201 self._flattener = Flattener(meta_file) | 145 self._processor = processor.Processor(meta_file) |
202 self._expanded_file = self._create_temp_file(self._flattener.contents) | 146 self._expanded_file = self._create_temp_file(self._processor.contents) |
203 self._debug("Expanded file: " + self._expanded_file) | 147 self._debug("Expanded file: " + self._expanded_file) |
204 | 148 |
205 args = ["--js=" + self._expanded_file] + ["--externs=" + e for e in externs] | 149 args = ["--js=" + self._expanded_file] + ["--externs=" + e for e in externs] |
206 args_file_content = " " + " ".join(self._common_closure_args + args) | 150 args_file_content = " " + " ".join(self._common_closure_args + args) |
207 self._debug("Args: " + args_file_content.strip()) | 151 self._debug("Args: " + args_file_content.strip()) |
208 | 152 |
209 args_file = self._create_temp_file(args_file_content) | 153 args_file = self._create_temp_file(args_file_content) |
210 self._debug("Args file: " + args_file) | 154 self._debug("Args file: " + args_file) |
211 | 155 |
212 runner_args = ["--compiler-args-file=" + args_file] | 156 runner_args = ["--compiler-args-file=" + args_file] |
213 runner_cmd = self._run_jar(self._runner_jar, args=runner_args) | 157 runner_cmd = self._run_jar(self._runner_jar, args=runner_args) |
214 (_, stderr) = runner_cmd.communicate() | 158 (_, stderr) = runner_cmd.communicate() |
215 | 159 |
216 errors = stderr.strip().split("\n\n") | 160 errors = stderr.strip().split("\n\n") |
217 self._debug("Summary: " + errors.pop()) | 161 self._debug("Summary: " + errors.pop()) |
218 | 162 |
219 output = self._format_errors(map(self._fix_up_error, errors)) | 163 output = self._format_errors(map(self._fix_up_error, errors)) |
220 if runner_cmd.returncode: | 164 if runner_cmd.returncode: |
221 self._error("Error in: " + file + ("\n" + output if output else "")) | 165 self._error("Error in: " + file + ("\n" + output if output else "")) |
222 elif output: | 166 elif output: |
223 self._debug("Output: " + output) | 167 self._debug("Output: " + output) |
224 | 168 |
225 self._clean_up() | 169 self._clean_up() |
226 | 170 |
227 return runner_cmd.returncode, output | 171 return runner_cmd.returncode, output |
| 172 |
| 173 |
| 174 if __name__ == "__main__": |
| 175 parser = argparse.ArgumentParser( |
| 176 description="Typecheck JavaScript using Closure compiler") |
| 177 parser.add_argument("sources", nargs=argparse.ONE_OR_MORE, |
| 178 help="Path to a source file to typecheck") |
| 179 parser.add_argument("-d", "--depends", nargs=argparse.ZERO_OR_MORE) |
| 180 parser.add_argument("-e", "--externs", nargs=argparse.ZERO_OR_MORE) |
| 181 parser.add_argument("-o", "--out_file", help="A place to output results") |
| 182 parser.add_argument("-v", "--verbose", action="store_true", |
| 183 help="Show more information as this script runs") |
| 184 opts = parser.parse_args() |
| 185 |
| 186 checker = Checker(verbose=opts.verbose) |
| 187 for source in opts.sources: |
| 188 if not checker.check(source, depends=opts.depends, externs=opts.externs): |
| 189 sys.exit(1) |
| 190 |
| 191 if opts.out_file: |
| 192 out_dir = os.path.dirname(opts.out_file) |
| 193 if not os.path.exists(out_dir): |
| 194 os.makedirs(out_dir) |
| 195 # TODO(dbeam): write compiled file to |opts.out_file|. |
| 196 open(opts.out_file, "w").write("") |
OLD | NEW |