Chromium Code Reviews| 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 |