15c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)# Copyright (c) 2012 The Chromium Authors. All rights reserved.
2926b001d589ce2f10facb93dd4b87578ea35a855Torne (Richard Coles)# Use of this source code is governed by a BSD-style license that can be
35c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)# found in the LICENSE file.
45c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)
55c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)"""Presubmit script for Chromium JS resources.
65c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)
75c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)See chrome/browser/PRESUBMIT.py
85c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)"""
95c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)
105c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)import regex_check
115c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)
125c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)
135c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)class JSChecker(object):
145c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)  def __init__(self, input_api, output_api, file_filter=None):
155c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)    self.input_api = input_api
165c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)    self.output_api = output_api
175c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)    self.file_filter = file_filter
185c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)
195c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)  def RegexCheck(self, line_number, line, regex, message):
205c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)    return regex_check.RegexCheck(
215c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)        self.input_api.re, line_number, line, regex, message)
225c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)
235c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)  def ChromeSendCheck(self, i, line):
245c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)    """Checks for a particular misuse of 'chrome.send'."""
255c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)    return self.RegexCheck(i, line, r"chrome\.send\('[^']+'\s*(, \[\])\)",
2653e740f4a82e17f3ae59772501622dc354e42336Torne (Richard Coles)        'Passing an empty array to chrome.send is unnecessary')
2753e740f4a82e17f3ae59772501622dc354e42336Torne (Richard Coles)
285c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)  def ConstCheck(self, i, line):
29197021e6b966cfb06891637935ef33fff06433d1Ben Murdoch    """Check for use of the 'const' keyword."""
3053e740f4a82e17f3ae59772501622dc354e42336Torne (Richard Coles)    if self.input_api.re.search(r'\*\s+@const', line):
3110f88d5669dbd969c059d61ba09fa37dd72ac559Ben Murdoch      # Probably a JsDoc line
32c0e19a689c8ac22cdc96b291a8d33a5d3b0b34a4Torne (Richard Coles)      return ''
335c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)
345c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)    return self.RegexCheck(i, line, r'(?:^|\s|\()(const)\s',
355c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)        'Use /** @const */ var varName; instead of const varName;')
3609380295ba73501a205346becac22c6978e4671dTorne (Richard Coles)
375c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)  def EndJsDocCommentCheck(self, i, line):
3809380295ba73501a205346becac22c6978e4671dTorne (Richard Coles)    msg = 'End JSDoc comments with */ instead of **/'
3953e740f4a82e17f3ae59772501622dc354e42336Torne (Richard Coles)    def _check(regex):
4009380295ba73501a205346becac22c6978e4671dTorne (Richard Coles)      return self.RegexCheck(i, line, regex, msg)
4153e740f4a82e17f3ae59772501622dc354e42336Torne (Richard Coles)    return _check(r'^\s*(\*\*/)\s*$') or _check(r'/\*\* @[a-zA-Z]+.* (\*\*/)')
4209380295ba73501a205346becac22c6978e4671dTorne (Richard Coles)
43926b001d589ce2f10facb93dd4b87578ea35a855Torne (Richard Coles)  def GetElementByIdCheck(self, i, line):
4453e740f4a82e17f3ae59772501622dc354e42336Torne (Richard Coles)    """Checks for use of 'document.getElementById' instead of '$'."""
4553e740f4a82e17f3ae59772501622dc354e42336Torne (Richard Coles)    return self.RegexCheck(i, line, r"(document\.getElementById)\('",
4653e740f4a82e17f3ae59772501622dc354e42336Torne (Richard Coles)        "Use $('id'), from chrome://resources/js/util.js, instead of "
475c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)        "document.getElementById('id')")
485c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)
4909380295ba73501a205346becac22c6978e4671dTorne (Richard Coles)  def InheritDocCheck(self, i, line):
505c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)    """Checks for use of '@inheritDoc' instead of '@override'."""
5109380295ba73501a205346becac22c6978e4671dTorne (Richard Coles)    return self.RegexCheck(i, line, r"\* (@inheritDoc)",
525c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)        "@inheritDoc is deprecated, use @override instead")
535c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)
545c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)  def WrapperTypeCheck(self, i, line):
555c87bf8b86a7c82ef50fb7a89697d8e02e2553beTorne (Richard Coles)    """Check for wrappers (new String()) instead of builtins (string)."""
5653e740f4a82e17f3ae59772501622dc354e42336Torne (Richard Coles)    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@chromium.org or '
244          'dbeam@chromium.org'))
245
246    return results
247