Home | History | Annotate | Download | only in closure_compiler
      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 
      8 import argparse
      9 import os
     10 import re
     11 import subprocess
     12 import sys
     13 import tempfile
     14 import processor
     15 
     16 
     17 class 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 
    222 if __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