1 # Copyright (c) 2014 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 import os 6 import re 7 import sys 8 import warnings 9 10 from py_vulcanize import strip_js_comments 11 12 from catapult_build import parse_html 13 14 15 class JSChecker(object): 16 17 def __init__(self, input_api, output_api, file_filter=None): 18 self.input_api = input_api 19 self.output_api = output_api 20 if file_filter: 21 self.file_filter = file_filter 22 else: 23 self.file_filter = lambda x: True 24 25 def RegexCheck(self, line_number, line, regex, message): 26 """Searches for |regex| in |line| to check for a style violation. 27 28 The |regex| must have exactly one capturing group so that the relevant 29 part of |line| can be highlighted. If more groups are needed, use 30 "(?:...)" to make a non-capturing group. Sample message: 31 32 Returns a message like the one below if the regex matches. 33 line 6: Use var instead of const. 34 const foo = bar(); 35 ^^^^^ 36 """ 37 match = re.search(regex, line) 38 if match: 39 assert len(match.groups()) == 1 40 start = match.start(1) 41 length = match.end(1) - start 42 return ' line %d: %s\n%s\n%s' % ( 43 line_number, 44 message, 45 line, 46 _ErrorHighlight(start, length)) 47 return '' 48 49 def ConstCheck(self, i, line): 50 """Checks for use of the 'const' keyword.""" 51 if re.search(r'\*\s+@const', line): 52 # Probably a JsDoc line. 53 return '' 54 55 return self.RegexCheck( 56 i, line, r'(?:^|\s|\()(const)\s', 'Use var instead of const.') 57 58 def RunChecks(self): 59 """Checks for violations of the Chromium JavaScript style guide. 60 61 See: 62 http://chromium.org/developers/web-development-style-guide#TOC-JavaScript 63 """ 64 old_path = sys.path 65 old_filters = warnings.filters 66 67 try: 68 base_path = os.path.abspath(os.path.join( 69 os.path.dirname(__file__), '..')) 70 closure_linter_path = os.path.join( 71 base_path, 'third_party', 'closure_linter') 72 gflags_path = os.path.join( 73 base_path, 'third_party', 'python_gflags') 74 sys.path.insert(0, closure_linter_path) 75 sys.path.insert(0, gflags_path) 76 77 warnings.filterwarnings('ignore', category=DeprecationWarning) 78 79 from closure_linter import runner, errors 80 from closure_linter.common import errorhandler 81 82 finally: 83 sys.path = old_path 84 warnings.filters = old_filters 85 86 class ErrorHandlerImpl(errorhandler.ErrorHandler): 87 """Filters out errors that don't apply to Chromium JavaScript code.""" 88 89 def __init__(self): 90 super(ErrorHandlerImpl, self).__init__() 91 self._errors = [] 92 self._filename = None 93 94 def HandleFile(self, filename, _): 95 self._filename = filename 96 97 def HandleError(self, error): 98 if self._Valid(error): 99 error.filename = self._filename 100 self._errors.append(error) 101 102 def GetErrors(self): 103 return self._errors 104 105 def HasErrors(self): 106 return bool(self._errors) 107 108 def _Valid(self, error): 109 """Checks whether an error is valid. 110 111 Most errors are valid, with a few exceptions which are listed here. 112 """ 113 if re.search('</?(include|if)', error.token.line): 114 return False # GRIT statement. 115 116 if (error.code == errors.MISSING_SEMICOLON and 117 error.token.string == 'of'): 118 return False # ES6 for...of statement. 119 120 return error.code not in [ 121 errors.JSDOC_ILLEGAL_QUESTION_WITH_PIPE, 122 errors.MISSING_JSDOC_TAG_THIS, 123 errors.MISSING_MEMBER_DOCUMENTATION, 124 ] 125 126 results = [] 127 128 affected_files = self.input_api.AffectedFiles( 129 file_filter=self.file_filter, 130 include_deletes=False) 131 132 def ShouldCheck(f): 133 if f.LocalPath().endswith('.js'): 134 return True 135 if f.LocalPath().endswith('.html'): 136 return True 137 return False 138 139 affected_js_files = filter(ShouldCheck, affected_files) 140 for f in affected_js_files: 141 error_lines = [] 142 143 contents = list(f.NewContents()) 144 error_lines += CheckStrictMode( 145 '\n'.join(contents), 146 is_html_file=f.LocalPath().endswith('.html')) 147 148 for i, line in enumerate(contents, start=1): 149 error_lines += filter(None, [self.ConstCheck(i, line)]) 150 151 # Use closure_linter to check for several different errors. 152 import gflags as flags 153 flags.FLAGS.strict = True 154 error_handler = ErrorHandlerImpl() 155 runner.Run(f.AbsoluteLocalPath(), error_handler) 156 157 for error in error_handler.GetErrors(): 158 highlight = _ErrorHighlight( 159 error.token.start_index, error.token.length) 160 error_msg = ' line %d: E%04d: %s\n%s\n%s' % ( 161 error.token.line_number, 162 error.code, 163 error.message, 164 error.token.line.rstrip(), 165 highlight) 166 error_lines.append(error_msg) 167 168 if error_lines: 169 error_lines = [ 170 'Found JavaScript style violations in %s:' % 171 f.LocalPath()] + error_lines 172 results.append( 173 _MakeErrorOrWarning(self.output_api, '\n'.join(error_lines))) 174 175 return results 176 177 178 def _ErrorHighlight(start, length): 179 """Produces a row of '^'s to underline part of a string.""" 180 return start * ' ' + length * '^' 181 182 183 def _MakeErrorOrWarning(output_api, error_text): 184 return output_api.PresubmitError(error_text) 185 186 187 def CheckStrictMode(contents, is_html_file=False): 188 statements_to_check = [] 189 if is_html_file: 190 statements_to_check.extend(_FirstStatementsInScriptElements(contents)) 191 else: 192 statements_to_check.append(_FirstStatement(contents)) 193 error_lines = [] 194 for s in statements_to_check: 195 if s != "'use strict'": 196 error_lines.append('Expected "\'use strict\'" as first statement, ' 197 'but found "%s" instead.' % s) 198 return error_lines 199 200 201 def _FirstStatementsInScriptElements(contents): 202 """Returns a list of first statements found in each <script> element.""" 203 soup = parse_html.BeautifulSoup(contents) 204 script_elements = soup.find_all('script', src=None) 205 return [_FirstStatement(e.get_text()) for e in script_elements] 206 207 208 def _FirstStatement(contents): 209 """Extracts the first statement in some JS source code.""" 210 stripped_contents = strip_js_comments.StripJSComments(contents).strip() 211 matches = re.match('^(.*?);', stripped_contents, re.DOTALL) 212 if not matches: 213 return '' 214 return matches.group(1).strip() 215 216 217 def RunChecks(input_api, output_api, excluded_paths=None): 218 219 def ShouldCheck(affected_file): 220 if not excluded_paths: 221 return True 222 path = affected_file.LocalPath() 223 return not any(re.match(pattern, path) for pattern in excluded_paths) 224 225 return JSChecker(input_api, output_api, file_filter=ShouldCheck).RunChecks() 226