# 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