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 re
      6 
      7 from node_runner import node_util
      8 from py_vulcanize import strip_js_comments
      9 
     10 from catapult_build import parse_html
     11 
     12 
     13 class JSChecker(object):
     14 
     15   def __init__(self, input_api, output_api, file_filter=None):
     16     self.input_api = input_api
     17     self.output_api = output_api
     18     if file_filter:
     19       self.file_filter = file_filter
     20     else:
     21       self.file_filter = lambda x: True
     22 
     23   def RegexCheck(self, line_number, line, regex, message):
     24     """Searches for |regex| in |line| to check for a style violation.
     25 
     26     The |regex| must have exactly one capturing group so that the relevant
     27     part of |line| can be highlighted. If more groups are needed, use
     28     "(?:...)" to make a non-capturing group. Sample message:
     29 
     30     Returns a message like the one below if the regex matches.
     31        line 6: Use var instead of const.
     32            const foo = bar();
     33            ^^^^^
     34     """
     35     match = re.search(regex, line)
     36     if match:
     37       assert len(match.groups()) == 1
     38       start = match.start(1)
     39       length = match.end(1) - start
     40       return '  line %d: %s\n%s\n%s' % (
     41           line_number,
     42           message,
     43           line,
     44           _ErrorHighlight(start, length))
     45     return ''
     46 
     47   def ConstCheck(self, i, line):
     48     """Checks for use of the 'const' keyword."""
     49     if re.search(r'\*\s+@const', line):
     50       # Probably a JsDoc line.
     51       return ''
     52 
     53     return self.RegexCheck(
     54         i, line, r'(?:^|\s|\()(const)\s', 'Use var instead of const.')
     55 
     56   def RunChecks(self):
     57     """Checks for violations of the Chromium JavaScript style guide.
     58 
     59     See:
     60     http://chromium.org/developers/web-development-style-guide#TOC-JavaScript
     61     """
     62     results = []
     63 
     64     affected_files = self.input_api.AffectedFiles(
     65         file_filter=self.file_filter,
     66         include_deletes=False)
     67 
     68     def ShouldCheck(f):
     69       if f.LocalPath().endswith('.js'):
     70         return True
     71       if f.LocalPath().endswith('.html'):
     72         return True
     73       return False
     74 
     75     affected_js_files = filter(ShouldCheck, affected_files)
     76     error_lines = []
     77     for f in affected_js_files:
     78       contents = list(f.NewContents())
     79       error_lines += CheckStrictMode(
     80           '\n'.join(contents),
     81           is_html_file=f.LocalPath().endswith('.html'))
     82 
     83       for i, line in enumerate(contents, start=1):
     84         error_lines += filter(None, [self.ConstCheck(i, line)])
     85 
     86     if affected_js_files:
     87       eslint_output = node_util.RunEslint(
     88           [f.AbsoluteLocalPath() for f in affected_js_files]).rstrip()
     89 
     90       if eslint_output:
     91         error_lines.append('\neslint found lint errors:')
     92         error_lines.append(eslint_output)
     93 
     94     if error_lines:
     95       error_lines.insert(0, 'Found JavaScript style violations:')
     96       results.append(
     97           _MakeErrorOrWarning(self.output_api, '\n'.join(error_lines)))
     98 
     99     return results
    100 
    101 
    102 def _ErrorHighlight(start, length):
    103   """Produces a row of '^'s to underline part of a string."""
    104   return start * ' ' + length * '^'
    105 
    106 
    107 def _MakeErrorOrWarning(output_api, error_text):
    108   return output_api.PresubmitError(error_text)
    109 
    110 
    111 def CheckStrictMode(contents, is_html_file=False):
    112   statements_to_check = []
    113   if is_html_file:
    114     statements_to_check.extend(_FirstStatementsInScriptElements(contents))
    115   else:
    116     statements_to_check.append(_FirstStatement(contents))
    117   error_lines = []
    118   for s in statements_to_check:
    119     if s != "'use strict'":
    120       error_lines.append('Expected "\'use strict\'" as first statement, '
    121                          'but found "%s" instead.' % s)
    122   return error_lines
    123 
    124 
    125 def _FirstStatementsInScriptElements(contents):
    126   """Returns a list of first statements found in each <script> element."""
    127   soup = parse_html.BeautifulSoup(contents)
    128   script_elements = soup.find_all('script', src=None)
    129   return [_FirstStatement(e.get_text()) for e in script_elements]
    130 
    131 
    132 def _FirstStatement(contents):
    133   """Extracts the first statement in some JS source code."""
    134   stripped_contents = strip_js_comments.StripJSComments(contents).strip()
    135   matches = re.match('^(.*?);', stripped_contents, re.DOTALL)
    136   if not matches:
    137     return ''
    138   return matches.group(1).strip()
    139 
    140 
    141 def RunChecks(input_api, output_api, excluded_paths=None):
    142 
    143   def ShouldCheck(affected_file):
    144     if not excluded_paths:
    145       return True
    146     path = affected_file.LocalPath()
    147     return not any(re.match(pattern, path) for pattern in excluded_paths)
    148 
    149   return JSChecker(input_api, output_api, file_filter=ShouldCheck).RunChecks()
    150