Home | History | Annotate | Download | only in catapult_build
      1 # Copyright (c) 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 import os
      6 import re
      7 import sys
      8 import warnings
      9 
     10 from py_vulcanize import strip_js_comments
     11 
     12 from catapult_build import parse_html
     13 
     14 
     15 class JSChecker(object):
     16 
     17   def __init__(self, input_api, output_api, file_filter=None):
     18     self.input_api = input_api
     19     self.output_api = output_api
     20     if file_filter:
     21       self.file_filter = file_filter
     22     else:
     23       self.file_filter = lambda x: True
     24 
     25   def RegexCheck(self, line_number, line, regex, message):
     26     """Searches for |regex| in |line| to check for a style violation.
     27 
     28     The |regex| must have exactly one capturing group so that the relevant
     29     part of |line| can be highlighted. If more groups are needed, use
     30     "(?:...)" to make a non-capturing group. Sample message:
     31 
     32     Returns a message like the one below if the regex matches.
     33        line 6: Use var instead of const.
     34            const foo = bar();
     35            ^^^^^
     36     """
     37     match = re.search(regex, line)
     38     if match:
     39       assert len(match.groups()) == 1
     40       start = match.start(1)
     41       length = match.end(1) - start
     42       return '  line %d: %s\n%s\n%s' % (
     43           line_number,
     44           message,
     45           line,
     46           _ErrorHighlight(start, length))
     47     return ''
     48 
     49   def ConstCheck(self, i, line):
     50     """Checks for use of the 'const' keyword."""
     51     if re.search(r'\*\s+@const', line):
     52       # Probably a JsDoc line.
     53       return ''
     54 
     55     return self.RegexCheck(
     56         i, line, r'(?:^|\s|\()(const)\s', 'Use var instead of const.')
     57 
     58   def RunChecks(self):
     59     """Checks for violations of the Chromium JavaScript style guide.
     60 
     61     See:
     62     http://chromium.org/developers/web-development-style-guide#TOC-JavaScript
     63     """
     64     old_path = sys.path
     65     old_filters = warnings.filters
     66 
     67     try:
     68       base_path = os.path.abspath(os.path.join(
     69           os.path.dirname(__file__), '..'))
     70       closure_linter_path = os.path.join(
     71           base_path, 'third_party', 'closure_linter')
     72       gflags_path = os.path.join(
     73           base_path, 'third_party', 'python_gflags')
     74       sys.path.insert(0, closure_linter_path)
     75       sys.path.insert(0, gflags_path)
     76 
     77       warnings.filterwarnings('ignore', category=DeprecationWarning)
     78 
     79       from closure_linter import runner, errors
     80       from closure_linter.common import errorhandler
     81 
     82     finally:
     83       sys.path = old_path
     84       warnings.filters = old_filters
     85 
     86     class ErrorHandlerImpl(errorhandler.ErrorHandler):
     87       """Filters out errors that don't apply to Chromium JavaScript code."""
     88 
     89       def __init__(self):
     90         super(ErrorHandlerImpl, self).__init__()
     91         self._errors = []
     92         self._filename = None
     93 
     94       def HandleFile(self, filename, _):
     95         self._filename = filename
     96 
     97       def HandleError(self, error):
     98         if self._Valid(error):
     99           error.filename = self._filename
    100           self._errors.append(error)
    101 
    102       def GetErrors(self):
    103         return self._errors
    104 
    105       def HasErrors(self):
    106         return bool(self._errors)
    107 
    108       def _Valid(self, error):
    109         """Checks whether an error is valid.
    110 
    111         Most errors are valid, with a few exceptions which are listed here.
    112         """
    113         if re.search('</?(include|if)', error.token.line):
    114           return False  # GRIT statement.
    115 
    116         if (error.code == errors.MISSING_SEMICOLON and
    117             error.token.string == 'of'):
    118           return False  # ES6 for...of statement.
    119 
    120         return error.code not in [
    121             errors.JSDOC_ILLEGAL_QUESTION_WITH_PIPE,
    122             errors.MISSING_JSDOC_TAG_THIS,
    123             errors.MISSING_MEMBER_DOCUMENTATION,
    124         ]
    125 
    126     results = []
    127 
    128     affected_files = self.input_api.AffectedFiles(
    129         file_filter=self.file_filter,
    130         include_deletes=False)
    131 
    132     def ShouldCheck(f):
    133       if f.LocalPath().endswith('.js'):
    134         return True
    135       if f.LocalPath().endswith('.html'):
    136         return True
    137       return False
    138 
    139     affected_js_files = filter(ShouldCheck, affected_files)
    140     for f in affected_js_files:
    141       error_lines = []
    142 
    143       contents = list(f.NewContents())
    144       error_lines += CheckStrictMode(
    145           '\n'.join(contents),
    146           is_html_file=f.LocalPath().endswith('.html'))
    147 
    148       for i, line in enumerate(contents, start=1):
    149         error_lines += filter(None, [self.ConstCheck(i, line)])
    150 
    151       # Use closure_linter to check for several different errors.
    152       import gflags as flags
    153       flags.FLAGS.strict = True
    154       error_handler = ErrorHandlerImpl()
    155       runner.Run(f.AbsoluteLocalPath(), error_handler)
    156 
    157       for error in error_handler.GetErrors():
    158         highlight = _ErrorHighlight(
    159             error.token.start_index, error.token.length)
    160         error_msg = '  line %d: E%04d: %s\n%s\n%s' % (
    161             error.token.line_number,
    162             error.code,
    163             error.message,
    164             error.token.line.rstrip(),
    165             highlight)
    166         error_lines.append(error_msg)
    167 
    168       if error_lines:
    169         error_lines = [
    170             'Found JavaScript style violations in %s:' %
    171             f.LocalPath()] + error_lines
    172         results.append(
    173             _MakeErrorOrWarning(self.output_api, '\n'.join(error_lines)))
    174 
    175     return results
    176 
    177 
    178 def _ErrorHighlight(start, length):
    179   """Produces a row of '^'s to underline part of a string."""
    180   return start * ' ' + length * '^'
    181 
    182 
    183 def _MakeErrorOrWarning(output_api, error_text):
    184   return output_api.PresubmitError(error_text)
    185 
    186 
    187 def CheckStrictMode(contents, is_html_file=False):
    188   statements_to_check = []
    189   if is_html_file:
    190     statements_to_check.extend(_FirstStatementsInScriptElements(contents))
    191   else:
    192     statements_to_check.append(_FirstStatement(contents))
    193   error_lines = []
    194   for s in statements_to_check:
    195     if s != "'use strict'":
    196       error_lines.append('Expected "\'use strict\'" as first statement, '
    197                          'but found "%s" instead.' % s)
    198   return error_lines
    199 
    200 
    201 def _FirstStatementsInScriptElements(contents):
    202   """Returns a list of first statements found in each <script> element."""
    203   soup = parse_html.BeautifulSoup(contents)
    204   script_elements = soup.find_all('script', src=None)
    205   return [_FirstStatement(e.get_text()) for e in script_elements]
    206 
    207 
    208 def _FirstStatement(contents):
    209   """Extracts the first statement in some JS source code."""
    210   stripped_contents = strip_js_comments.StripJSComments(contents).strip()
    211   matches = re.match('^(.*?);', stripped_contents, re.DOTALL)
    212   if not matches:
    213     return ''
    214   return matches.group(1).strip()
    215 
    216 
    217 def RunChecks(input_api, output_api, excluded_paths=None):
    218 
    219   def ShouldCheck(affected_file):
    220     if not excluded_paths:
    221       return True
    222     path = affected_file.LocalPath()
    223     return not any(re.match(pattern, path) for pattern in excluded_paths)
    224 
    225   return JSChecker(input_api, output_api, file_filter=ShouldCheck).RunChecks()
    226