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