blob: 4fae90d033ee1edfef954a4b65be55c53f6f0225 [file] [log] [blame]
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Presubmit script for Chromium JS resources.
See chrome/browser/PRESUBMIT.py
"""
import regex_check
class JSChecker(object):
def __init__(self, input_api, output_api, file_filter=None):
self.input_api = input_api
self.output_api = output_api
self.file_filter = file_filter
def RegexCheck(self, line_number, line, regex, message):
return regex_check.RegexCheck(
self.input_api.re, line_number, line, regex, message)
def ChromeSendCheck(self, i, line):
"""Checks for a particular misuse of 'chrome.send'."""
return self.RegexCheck(i, line, r"chrome\.send\('[^']+'\s*(, \[\])\)",
'Passing an empty array to chrome.send is unnecessary')
def ConstCheck(self, i, line):
"""Check for use of the 'const' keyword."""
if self.input_api.re.search(r'\*\s+@const', line):
# Probably a JsDoc line
return ''
return self.RegexCheck(i, line, r'(?:^|\s|\()(const)\s',
'Use /** @const */ var varName; instead of const varName;')
def EndJsDocCommentCheck(self, i, line):
msg = 'End JSDoc comments with */ instead of **/'
def _check(regex):
return self.RegexCheck(i, line, regex, msg)
return _check(r'^\s*(\*\*/)\s*$') or _check(r'/\*\* @[a-zA-Z]+.* (\*\*/)')
def GetElementByIdCheck(self, i, line):
"""Checks for use of 'document.getElementById' instead of '$'."""
return self.RegexCheck(i, line, r"(document\.getElementById)\('",
"Use $('id'), from chrome://resources/js/util.js, instead of "
"document.getElementById('id')")
def InheritDocCheck(self, i, line):
"""Checks for use of '@inheritDoc' instead of '@override'."""
return self.RegexCheck(i, line, r"\* (@inheritDoc)",
"@inheritDoc is deprecated, use @override instead")
def WrapperTypeCheck(self, i, line):
"""Check for wrappers (new String()) instead of builtins (string)."""
return self.RegexCheck(i, line,
r"(?:/\*)?\*.*?@(?:param|return|type) ?" # /** @param/@return/@type
r"{[^}]*\b(String|Boolean|Number)\b[^}]*}", # {(Boolean|Number|String)}
"Don't use wrapper types (i.e. new String() or @type {String})")
def VarNameCheck(self, i, line):
"""See the style guide. http://goo.gl/uKir6"""
return self.RegexCheck(i, line,
r"var (?!g_\w+)([a-z]*[_$][\w_$]*)(?<! \$)",
"Please use var namesLikeThis <http://goo.gl/uKir6>")
def _GetErrorHighlight(self, start, length):
"""Takes a start position and a length, and produces a row of '^'s to
highlight the corresponding part of a string.
"""
return start * ' ' + length * '^'
def _MakeErrorOrWarning(self, error_text, filename):
"""Takes a few lines of text indicating a style violation and turns it into
a PresubmitError (if |filename| is in a directory where we've already
taken out all the style guide violations) or a PresubmitPromptWarning
(if it's in a directory where we haven't done that yet).
"""
# TODO(tbreisacher): Once we've cleaned up the style nits in all of
# resources/ we can get rid of this function.
path = self.input_api.os_path
resources = path.join(self.input_api.PresubmitLocalPath(), 'resources')
dirs = (
path.join(resources, 'bookmark_manager'),
path.join(resources, 'extensions'),
path.join(resources, 'file_manager'),
path.join(resources, 'help'),
path.join(resources, 'history'),
path.join(resources, 'memory_internals'),
path.join(resources, 'net_export'),
path.join(resources, 'net_internals'),
path.join(resources, 'network_action_predictor'),
path.join(resources, 'ntp4'),
path.join(resources, 'options'),
path.join(resources, 'password_manager_internals'),
path.join(resources, 'print_preview'),
path.join(resources, 'profiler'),
path.join(resources, 'sync_promo'),
path.join(resources, 'tracing'),
path.join(resources, 'uber'),
)
if filename.startswith(dirs):
return self.output_api.PresubmitError(error_text)
else:
return self.output_api.PresubmitPromptWarning(error_text)
def ClosureLint(self, file_to_lint, source=None):
"""Lints |file_to_lint| and returns the errors."""
import sys
import warnings
old_path = sys.path
old_filters = warnings.filters
try:
closure_linter_path = self.input_api.os_path.join(
self.input_api.change.RepositoryRoot(),
"third_party",
"closure_linter")
gflags_path = self.input_api.os_path.join(
self.input_api.change.RepositoryRoot(),
"third_party",
"python_gflags")
sys.path.insert(0, closure_linter_path)
sys.path.insert(0, gflags_path)
warnings.filterwarnings('ignore', category=DeprecationWarning)
from closure_linter import errors, runner
from closure_linter.common import errorhandler
finally:
sys.path = old_path
warnings.filters = old_filters
class ErrorHandlerImpl(errorhandler.ErrorHandler):
"""Filters out errors that don't apply to Chromium JavaScript code."""
def __init__(self, re):
self._errors = []
self.re = re
def HandleFile(self, filename, first_token):
self._filename = filename
def HandleError(self, error):
if (self._valid(error)):
error.filename = self._filename
self._errors.append(error)
def GetErrors(self):
return self._errors
def HasErrors(self):
return bool(self._errors)
def _valid(self, error):
"""Check whether an error is valid. Most errors are valid, with a few
exceptions which are listed here.
"""
is_grit_statement = bool(
self.re.search("</?(include|if)", error.token.line))
# Ignore missing spaces before "(" until Promise#catch issue is solved.
# http://crbug.com/338301
if (error.code == errors.MISSING_SPACE and error.token.string == '(' and
'catch(' in error.token.line):
return False
# Ignore "}.bind(" errors. http://crbug.com/397697
if (error.code == errors.MISSING_SEMICOLON_AFTER_FUNCTION and
'}.bind(' in error.token.line):
return False
return not is_grit_statement and error.code not in [
errors.COMMA_AT_END_OF_LITERAL,
errors.JSDOC_ILLEGAL_QUESTION_WITH_PIPE,
errors.LINE_TOO_LONG,
errors.MISSING_JSDOC_TAG_THIS,
]
error_handler = ErrorHandlerImpl(self.input_api.re)
runner.Run(file_to_lint, error_handler, source=source)
return error_handler.GetErrors()
def RunChecks(self):
"""Check for violations of the Chromium JavaScript style guide. See
http://chromium.org/developers/web-development-style-guide#TOC-JavaScript
"""
results = []
affected_files = self.input_api.change.AffectedFiles(
file_filter=self.file_filter,
include_deletes=False)
affected_js_files = filter(lambda f: f.LocalPath().endswith('.js'),
affected_files)
for f in affected_js_files:
error_lines = []
# Check for the following:
# * document.getElementById()
# * the 'const' keyword
# * Passing an empty array to 'chrome.send()'
for i, line in enumerate(f.NewContents(), start=1):
error_lines += filter(None, [
self.ChromeSendCheck(i, line),
self.ConstCheck(i, line),
self.GetElementByIdCheck(i, line),
self.InheritDocCheck(i, line),
self.WrapperTypeCheck(i, line),
self.VarNameCheck(i, line),
])
# Use closure linter to check for several different errors.
lint_errors = self.ClosureLint(self.input_api.os_path.join(
self.input_api.change.RepositoryRoot(), f.LocalPath()))
for error in lint_errors:
highlight = self._GetErrorHighlight(
error.token.start_index, error.token.length)
error_msg = ' line %d: E%04d: %s\n%s\n%s' % (
error.token.line_number,
error.code,
error.message,
error.token.line.rstrip(),
highlight)
error_lines.append(error_msg)
if error_lines:
error_lines = [
'Found JavaScript style violations in %s:' %
f.LocalPath()] + error_lines
results.append(self._MakeErrorOrWarning(
'\n'.join(error_lines), f.AbsoluteLocalPath()))
if results:
results.append(self.output_api.PresubmitNotifyResult(
'See the JavaScript style guide at '
'http://www.chromium.org/developers/web-development-style-guide'
'#TOC-JavaScript and if you have any feedback about the JavaScript '
'PRESUBMIT check, contact tbreisacher@chromium.org or '
'dbeam@chromium.org'))
return results