OLD | NEW |
(Empty) | |
| 1 # 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 # found in the LICENSE file. |
| 4 |
| 5 from collections import defaultdict |
| 6 import os |
| 7 import re |
| 8 import subprocess |
| 9 import sys |
| 10 import tempfile |
| 11 |
| 12 |
| 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 _INCLUDE_REG = "<include[^>]+src=['\"]([^>]*)['\"]>" |
| 35 |
| 36 def __init__(self, file): |
| 37 self.index = 0 |
| 38 self.lines = self._get_file(file) |
| 39 |
| 40 while self.index < len(self.lines): |
| 41 current_line = self.lines[self.index] |
| 42 match = re.search(self._INCLUDE_REG, current_line[2]) |
| 43 if match: |
| 44 file_dir = os.path.dirname(current_line[0]) |
| 45 self._inline_file(os.path.join(file_dir, match.group(1))) |
| 46 else: |
| 47 self.index += 1 |
| 48 |
| 49 self.contents = "\n".join(l[2] for l in self.lines) |
| 50 |
| 51 # Returns a list of tuples in the format: (file, line number, line contents). |
| 52 def _get_file(self, file): |
| 53 lines = FileCache.read(file).splitlines() |
| 54 return [(file, lnum + 1, line) for lnum, line in enumerate(lines)] |
| 55 |
| 56 def _inline_file(self, file): |
| 57 lines = self._get_file(file) |
| 58 self.lines = self.lines[:self.index] + lines + self.lines[self.index + 1:] |
| 59 |
| 60 def get_file_from_line(self, line_number): |
| 61 line_number = int(line_number) - 1 |
| 62 return LineNumber(self.lines[line_number][0], self.lines[line_number][1]) |
| 63 |
| 64 |
| 65 class Checker(object): |
| 66 _common_closure_args = [ |
| 67 "--accept_const_keyword", |
| 68 "--language_in=ECMASCRIPT5", |
| 69 "--summary_detail_level=3", |
| 70 "--warning_level=VERBOSE", |
| 71 "--jscomp_error=accessControls", |
| 72 "--jscomp_error=ambiguousFunctionDecl", |
| 73 "--jscomp_error=checkStructDictInheritance", |
| 74 "--jscomp_error=checkTypes", |
| 75 "--jscomp_error=checkVars", |
| 76 "--jscomp_error=constantProperty", |
| 77 "--jscomp_error=deprecated", |
| 78 "--jscomp_error=externsValidation", |
| 79 "--jscomp_error=globalThis", |
| 80 "--jscomp_error=invalidCasts", |
| 81 "--jscomp_error=misplacedTypeAnnotation", |
| 82 "--jscomp_error=missingProperties", |
| 83 "--jscomp_error=missingReturn", |
| 84 "--jscomp_error=nonStandardJsDocs", |
| 85 "--jscomp_error=suspiciousCode", |
| 86 "--jscomp_error=undefinedNames", |
| 87 "--jscomp_error=undefinedVars", |
| 88 "--jscomp_error=unknownDefines", |
| 89 "--jscomp_error=uselessCode", |
| 90 "--jscomp_error=visibility", |
| 91 ] |
| 92 |
| 93 _found_java = False |
| 94 |
| 95 _jar_command = [ |
| 96 "java", |
| 97 "-jar", |
| 98 "-Xms1024m", |
| 99 "-server", |
| 100 "-XX:+TieredCompilation" |
| 101 ] |
| 102 |
| 103 def __init__(self, verbose=False): |
| 104 current_dir = os.path.join(os.path.dirname(__file__)) |
| 105 self._compiler_jar = os.path.join(current_dir, "lib", "compiler.jar") |
| 106 self._runner_jar = os.path.join(current_dir, "runner", "runner.jar") |
| 107 self._temp_files = [] |
| 108 self._verbose = verbose |
| 109 |
| 110 def _clean_up(self): |
| 111 if not self._temp_files: |
| 112 return |
| 113 |
| 114 self._debug("Deleting temporary files: " + ", ".join(self._temp_files)) |
| 115 for f in self._temp_files: |
| 116 os.remove(f) |
| 117 self._temp_files = [] |
| 118 |
| 119 def _debug(self, msg, error=False): |
| 120 if self._verbose: |
| 121 print "(INFO) " + msg |
| 122 |
| 123 def _fatal(self, msg): |
| 124 print >> sys.stderr, "(FATAL) " + msg |
| 125 self._clean_up() |
| 126 sys.exit(1) |
| 127 |
| 128 def _run_command(self, cmd): |
| 129 cmd_str = " ".join(cmd) |
| 130 self._debug("Running command: " + cmd_str) |
| 131 |
| 132 devnull = open(os.devnull, "w") |
| 133 return subprocess.Popen( |
| 134 cmd_str, stdout=devnull, stderr=subprocess.PIPE, shell=True) |
| 135 |
| 136 def _check_java_path(self): |
| 137 if self._found_java: |
| 138 return |
| 139 |
| 140 proc = self._run_command(["which", "java"]) |
| 141 proc.communicate() |
| 142 if proc.returncode == 0: |
| 143 self._found_java = True |
| 144 else: |
| 145 self._fatal("Cannot find java (`which java` => %s)" % proc.returncode) |
| 146 |
| 147 def _run_jar(self, jar, args=[]): |
| 148 self._check_java_path() |
| 149 return self._run_command(self._jar_command + [jar] + args) |
| 150 |
| 151 def _fix_line_number(self, match): |
| 152 real_file = self._flattener.get_file_from_line(match.group(1)) |
| 153 return "%s:%d" % (os.path.abspath(real_file.file), real_file.line_number) |
| 154 |
| 155 def _fix_up_error(self, error): |
| 156 if " first declared in " in error: |
| 157 # Ignore "Variable x first declared in /same/file". |
| 158 return "" |
| 159 |
| 160 file = self._expanded_file |
| 161 fixed = re.sub("%s:(\d+)" % file, self._fix_line_number, error) |
| 162 return fixed.replace(file, os.path.abspath(self._file_arg)) |
| 163 |
| 164 def _format_errors(self, errors): |
| 165 errors = filter(None, errors) |
| 166 contents = ("\n" + "## ").join("\n\n".join(errors).splitlines()) |
| 167 return "## " + contents if contents else "" |
| 168 |
| 169 def _create_temp_file(self, contents): |
| 170 with tempfile.NamedTemporaryFile(mode='wt', delete=False) as tmp_file: |
| 171 self._temp_files.append(tmp_file.name) |
| 172 tmp_file.write(contents) |
| 173 return tmp_file.name |
| 174 |
| 175 def check(self, file, depends=[], externs=[]): |
| 176 self._debug("FILE: " + file) |
| 177 |
| 178 if file.endswith("_externs.js"): |
| 179 self._debug("Skipping externs: " + file) |
| 180 return |
| 181 |
| 182 self._file_arg = file |
| 183 |
| 184 tmp_dir = tempfile.gettempdir() |
| 185 rel_path = lambda f: os.path.join(os.path.relpath(os.getcwd(), tmp_dir), f) |
| 186 |
| 187 contents = ['<include src="%s">' % rel_path(f) for f in depends + [file]] |
| 188 meta_file = self._create_temp_file("\n".join(contents)) |
| 189 self._debug("Meta file: " + meta_file) |
| 190 |
| 191 self._flattener = Flattener(meta_file) |
| 192 self._expanded_file = self._create_temp_file(self._flattener.contents) |
| 193 self._debug("Expanded file: " + self._expanded_file) |
| 194 |
| 195 args = ["--js=" + self._expanded_file] + ["--externs=" + e for e in externs] |
| 196 args_file_content = " " + " ".join(self._common_closure_args + args) |
| 197 self._debug("Args: " + args_file_content.strip()) |
| 198 |
| 199 args_file = self._create_temp_file(args_file_content) |
| 200 self._debug("Args file: " + args_file) |
| 201 |
| 202 runner_args = ["--compiler-args-file=" + args_file] |
| 203 runner_cmd = self._run_jar(self._runner_jar, args=runner_args) |
| 204 (_, stderr) = runner_cmd.communicate() |
| 205 |
| 206 errors = stderr.strip().split("\n\n") |
| 207 self._debug("Summary: " + errors.pop()) |
| 208 |
| 209 output = self._format_errors(map(self._fix_up_error, errors)) |
| 210 if runner_cmd.returncode: |
| 211 self._fatal("Error in: " + file + ("\n" + output if output else "")) |
| 212 elif output: |
| 213 self._debug("Output: " + output) |
| 214 |
| 215 self._clean_up() |
| 216 |
| 217 return runner_cmd.returncode == 0 |
OLD | NEW |