Home | History | Annotate | Download | only in web_dev_style
      1 # Copyright (c) 2012 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 """Presubmit script for Chromium JS resources.
      6 
      7 See chrome/browser/PRESUBMIT.py
      8 """
      9 
     10 import regex_check
     11 
     12 
     13 class JSChecker(object):
     14   def __init__(self, input_api, output_api, file_filter=None):
     15     self.input_api = input_api
     16     self.output_api = output_api
     17     self.file_filter = file_filter
     18 
     19   def RegexCheck(self, line_number, line, regex, message):
     20     return regex_check.RegexCheck(
     21         self.input_api.re, line_number, line, regex, message)
     22 
     23   def ChromeSendCheck(self, i, line):
     24     """Checks for a particular misuse of 'chrome.send'."""
     25     return self.RegexCheck(i, line, r"chrome\.send\('[^']+'\s*(, \[\])\)",
     26         'Passing an empty array to chrome.send is unnecessary')
     27 
     28   def ConstCheck(self, i, line):
     29     """Check for use of the 'const' keyword."""
     30     if self.input_api.re.search(r'\*\s+@const', line):
     31       # Probably a JsDoc line
     32       return ''
     33 
     34     return self.RegexCheck(i, line, r'(?:^|\s|\()(const)\s',
     35         'Use /** @const */ var varName; instead of const varName;')
     36 
     37   def EndJsDocCommentCheck(self, i, line):
     38     msg = 'End JSDoc comments with */ instead of **/'
     39     def _check(regex):
     40       return self.RegexCheck(i, line, regex, msg)
     41     return _check(r'^\s*(\*\*/)\s*$') or _check(r'/\*\* @[a-zA-Z]+.* (\*\*/)')
     42 
     43   def GetElementByIdCheck(self, i, line):
     44     """Checks for use of 'document.getElementById' instead of '$'."""
     45     return self.RegexCheck(i, line, r"(document\.getElementById)\('",
     46         "Use $('id'), from chrome://resources/js/util.js, instead of "
     47         "document.getElementById('id')")
     48 
     49   def InheritDocCheck(self, i, line):
     50     """Checks for use of '@inheritDoc' instead of '@override'."""
     51     return self.RegexCheck(i, line, r"\* (@inheritDoc)",
     52         "@inheritDoc is deprecated, use @override instead")
     53 
     54   def WrapperTypeCheck(self, i, line):
     55     """Check for wrappers (new String()) instead of builtins (string)."""
     56     return self.RegexCheck(i, line,
     57         r"(?:/\*)?\*.*?@(?:param|return|type) ?"     # /** @param/@return/@type
     58         r"{[^}]*\b(String|Boolean|Number)\b[^}]*}",  # {(Boolean|Number|String)}
     59         "Don't use wrapper types (i.e. new String() or @type {String})")
     60 
     61   def VarNameCheck(self, i, line):
     62     """See the style guide. http://goo.gl/uKir6"""
     63     return self.RegexCheck(i, line,
     64         r"var (?!g_\w+)([a-z]*[_$][\w_$]*)(?<! \$)",
     65         "Please use var namesLikeThis <http://goo.gl/uKir6>")
     66 
     67   def _GetErrorHighlight(self, start, length):
     68     """Takes a start position and a length, and produces a row of '^'s to
     69        highlight the corresponding part of a string.
     70     """
     71     return start * ' ' + length * '^'
     72 
     73   def _MakeErrorOrWarning(self, error_text, filename):
     74     """Takes a few lines of text indicating a style violation and turns it into
     75        a PresubmitError (if |filename| is in a directory where we've already
     76        taken out all the style guide violations) or a PresubmitPromptWarning
     77        (if it's in a directory where we haven't done that yet).
     78     """
     79     # TODO(tbreisacher): Once we've cleaned up the style nits in all of
     80     # resources/ we can get rid of this function.
     81     path = self.input_api.os_path
     82     resources = path.join(self.input_api.PresubmitLocalPath(), 'resources')
     83     dirs = (
     84         path.join(resources, 'bookmark_manager'),
     85         path.join(resources, 'extensions'),
     86         path.join(resources, 'file_manager'),
     87         path.join(resources, 'help'),
     88         path.join(resources, 'history'),
     89         path.join(resources, 'memory_internals'),
     90         path.join(resources, 'net_export'),
     91         path.join(resources, 'net_internals'),
     92         path.join(resources, 'network_action_predictor'),
     93         path.join(resources, 'ntp4'),
     94         path.join(resources, 'options'),
     95         path.join(resources, 'password_manager_internals'),
     96         path.join(resources, 'print_preview'),
     97         path.join(resources, 'profiler'),
     98         path.join(resources, 'sync_promo'),
     99         path.join(resources, 'tracing'),
    100         path.join(resources, 'uber'),
    101     )
    102     if filename.startswith(dirs):
    103       return self.output_api.PresubmitError(error_text)
    104     else:
    105       return self.output_api.PresubmitPromptWarning(error_text)
    106 
    107   def ClosureLint(self, file_to_lint, source=None):
    108     """Lints |file_to_lint| and returns the errors."""
    109 
    110     import sys
    111     import warnings
    112     old_path = sys.path
    113     old_filters = warnings.filters
    114 
    115     try:
    116       closure_linter_path = self.input_api.os_path.join(
    117           self.input_api.change.RepositoryRoot(),
    118           "third_party",
    119           "closure_linter")
    120       gflags_path = self.input_api.os_path.join(
    121           self.input_api.change.RepositoryRoot(),
    122           "third_party",
    123           "python_gflags")
    124 
    125       sys.path.insert(0, closure_linter_path)
    126       sys.path.insert(0, gflags_path)
    127 
    128       warnings.filterwarnings('ignore', category=DeprecationWarning)
    129 
    130       from closure_linter import errors, runner
    131       from closure_linter.common import errorhandler
    132 
    133     finally:
    134       sys.path = old_path
    135       warnings.filters = old_filters
    136 
    137     class ErrorHandlerImpl(errorhandler.ErrorHandler):
    138       """Filters out errors that don't apply to Chromium JavaScript code."""
    139 
    140       def __init__(self, re):
    141         self._errors = []
    142         self.re = re
    143 
    144       def HandleFile(self, filename, first_token):
    145         self._filename = filename
    146 
    147       def HandleError(self, error):
    148         if (self._valid(error)):
    149           error.filename = self._filename
    150           self._errors.append(error)
    151 
    152       def GetErrors(self):
    153         return self._errors
    154 
    155       def HasErrors(self):
    156         return bool(self._errors)
    157 
    158       def _valid(self, error):
    159         """Check whether an error is valid. Most errors are valid, with a few
    160            exceptions which are listed here.
    161         """
    162 
    163         is_grit_statement = bool(
    164             self.re.search("</?(include|if)", error.token.line))
    165 
    166         # Ignore missing spaces before "(" until Promise#catch issue is solved.
    167         # http://crbug.com/338301
    168         if (error.code == errors.MISSING_SPACE and error.token.string == '(' and
    169            'catch(' in error.token.line):
    170           return False
    171 
    172         # Ignore "}.bind(" errors. http://crbug.com/397697
    173         if (error.code == errors.MISSING_SEMICOLON_AFTER_FUNCTION and
    174             '}.bind(' in error.token.line):
    175           return False
    176 
    177         return not is_grit_statement and error.code not in [
    178             errors.COMMA_AT_END_OF_LITERAL,
    179             errors.JSDOC_ILLEGAL_QUESTION_WITH_PIPE,
    180             errors.LINE_TOO_LONG,
    181             errors.MISSING_JSDOC_TAG_THIS,
    182         ]
    183 
    184     error_handler = ErrorHandlerImpl(self.input_api.re)
    185     runner.Run(file_to_lint, error_handler, source=source)
    186     return error_handler.GetErrors()
    187 
    188   def RunChecks(self):
    189     """Check for violations of the Chromium JavaScript style guide. See
    190        http://chromium.org/developers/web-development-style-guide#TOC-JavaScript
    191     """
    192     results = []
    193 
    194     affected_files = self.input_api.change.AffectedFiles(
    195         file_filter=self.file_filter,
    196         include_deletes=False)
    197     affected_js_files = filter(lambda f: f.LocalPath().endswith('.js'),
    198                                affected_files)
    199     for f in affected_js_files:
    200       error_lines = []
    201 
    202       # Check for the following:
    203       # * document.getElementById()
    204       # * the 'const' keyword
    205       # * Passing an empty array to 'chrome.send()'
    206       for i, line in enumerate(f.NewContents(), start=1):
    207         error_lines += filter(None, [
    208             self.ChromeSendCheck(i, line),
    209             self.ConstCheck(i, line),
    210             self.GetElementByIdCheck(i, line),
    211             self.InheritDocCheck(i, line),
    212             self.WrapperTypeCheck(i, line),
    213             self.VarNameCheck(i, line),
    214         ])
    215 
    216       # Use closure linter to check for several different errors.
    217       lint_errors = self.ClosureLint(self.input_api.os_path.join(
    218           self.input_api.change.RepositoryRoot(), f.LocalPath()))
    219 
    220       for error in lint_errors:
    221         highlight = self._GetErrorHighlight(
    222             error.token.start_index, error.token.length)
    223         error_msg = '  line %d: E%04d: %s\n%s\n%s' % (
    224             error.token.line_number,
    225             error.code,
    226             error.message,
    227             error.token.line.rstrip(),
    228             highlight)
    229         error_lines.append(error_msg)
    230 
    231       if error_lines:
    232         error_lines = [
    233             'Found JavaScript style violations in %s:' %
    234             f.LocalPath()] + error_lines
    235         results.append(self._MakeErrorOrWarning(
    236             '\n'.join(error_lines), f.AbsoluteLocalPath()))
    237 
    238     if results:
    239       results.append(self.output_api.PresubmitNotifyResult(
    240           'See the JavaScript style guide at '
    241           'http://www.chromium.org/developers/web-development-style-guide'
    242           '#TOC-JavaScript and if you have any feedback about the JavaScript '
    243           'PRESUBMIT check, contact tbreisacher (at] chromium.org or '
    244           'dbeam (at] chromium.org'))
    245 
    246     return results
    247