# Copyright (c) 2014 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. import os import re import sys import warnings from py_vulcanize import strip_js_comments from catapult_build import parse_html class JSChecker(object): def __init__(self, input_api, output_api, file_filter=None): self.input_api = input_api self.output_api = output_api if file_filter: self.file_filter = file_filter else: self.file_filter = lambda x: True def RegexCheck(self, line_number, line, regex, message): """Searches for |regex| in |line| to check for a style violation. The |regex| must have exactly one capturing group so that the relevant part of |line| can be highlighted. If more groups are needed, use "(?:...)" to make a non-capturing group. Sample message: Returns a message like the one below if the regex matches. line 6: Use var instead of const. const foo = bar(); ^^^^^ """ match = re.search(regex, line) if match: assert len(match.groups()) == 1 start = match.start(1) length = match.end(1) - start return ' line %d: %s\n%s\n%s' % ( line_number, message, line, _ErrorHighlight(start, length)) return '' def ConstCheck(self, i, line): """Checks for use of the 'const' keyword.""" if re.search(r'\*\s+@const', line): # Probably a JsDoc line. return '' return self.RegexCheck( i, line, r'(?:^|\s|\()(const)\s', 'Use var instead of const.') def RunChecks(self): """Checks for violations of the Chromium JavaScript style guide. See: http://chromium.org/developers/web-development-style-guide#TOC-JavaScript """ old_path = sys.path old_filters = warnings.filters try: base_path = os.path.abspath(os.path.join( os.path.dirname(__file__), '..')) closure_linter_path = os.path.join( base_path, 'third_party', 'closure_linter') gflags_path = os.path.join( base_path, '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 runner, errors 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): super(ErrorHandlerImpl, self).__init__() self._errors = [] self._filename = None def HandleFile(self, filename, _): 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): """Checks whether an error is valid. Most errors are valid, with a few exceptions which are listed here. """ if re.search('</?(include|if)', error.token.line): return False # GRIT statement. if (error.code == errors.MISSING_SEMICOLON and error.token.string == 'of'): return False # ES6 for...of statement. return error.code not in [ errors.JSDOC_ILLEGAL_QUESTION_WITH_PIPE, errors.MISSING_JSDOC_TAG_THIS, errors.MISSING_MEMBER_DOCUMENTATION, ] results = [] affected_files = self.input_api.AffectedFiles( file_filter=self.file_filter, include_deletes=False) def ShouldCheck(f): if f.LocalPath().endswith('.js'): return True if f.LocalPath().endswith('.html'): return True return False affected_js_files = filter(ShouldCheck, affected_files) for f in affected_js_files: error_lines = [] contents = list(f.NewContents()) error_lines += CheckStrictMode( '\n'.join(contents), is_html_file=f.LocalPath().endswith('.html')) for i, line in enumerate(contents, start=1): error_lines += filter(None, [self.ConstCheck(i, line)]) # Use closure_linter to check for several different errors. import gflags as flags flags.FLAGS.strict = True error_handler = ErrorHandlerImpl() runner.Run(f.AbsoluteLocalPath(), error_handler) for error in error_handler.GetErrors(): highlight = _ErrorHighlight( 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( _MakeErrorOrWarning(self.output_api, '\n'.join(error_lines))) return results def _ErrorHighlight(start, length): """Produces a row of '^'s to underline part of a string.""" return start * ' ' + length * '^' def _MakeErrorOrWarning(output_api, error_text): return output_api.PresubmitError(error_text) def CheckStrictMode(contents, is_html_file=False): statements_to_check = [] if is_html_file: statements_to_check.extend(_FirstStatementsInScriptElements(contents)) else: statements_to_check.append(_FirstStatement(contents)) error_lines = [] for s in statements_to_check: if s != "'use strict'": error_lines.append('Expected "\'use strict\'" as first statement, ' 'but found "%s" instead.' % s) return error_lines def _FirstStatementsInScriptElements(contents): """Returns a list of first statements found in each <script> element.""" soup = parse_html.BeautifulSoup(contents) script_elements = soup.find_all('script', src=None) return [_FirstStatement(e.get_text()) for e in script_elements] def _FirstStatement(contents): """Extracts the first statement in some JS source code.""" stripped_contents = strip_js_comments.StripJSComments(contents).strip() matches = re.match('^(.*?);', stripped_contents, re.DOTALL) if not matches: return '' return matches.group(1).strip() def RunChecks(input_api, output_api, excluded_paths=None): def ShouldCheck(affected_file): if not excluded_paths: return True path = affected_file.LocalPath() return not any(re.match(pattern, path) for pattern in excluded_paths) return JSChecker(input_api, output_api, file_filter=ShouldCheck).RunChecks()