| 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 |