1# Copyright (c) 2012 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"""Presubmit script for Chromium JS resources. 6 7See chrome/browser/resources/PRESUBMIT.py 8""" 9 10class JSChecker(object): 11 def __init__(self, input_api, output_api, file_filter=None): 12 self.input_api = input_api 13 self.output_api = output_api 14 self.file_filter = file_filter 15 16 def RegexCheck(self, line_number, line, regex, message): 17 """Searches for |regex| in |line| to check for a particular style 18 violation, returning a message like the one below if the regex matches. 19 The |regex| must have exactly one capturing group so that the relevant 20 part of |line| can be highlighted. If more groups are needed, use 21 "(?:...)" to make a non-capturing group. Sample message: 22 23 line 6: Use var instead of const. 24 const foo = bar(); 25 ^^^^^ 26 """ 27 match = self.input_api.re.search(regex, line) 28 if match: 29 assert len(match.groups()) == 1 30 start = match.start(1) 31 length = match.end(1) - start 32 return ' line %d: %s\n%s\n%s' % ( 33 line_number, 34 message, 35 line, 36 self.error_highlight(start, length)) 37 return '' 38 39 def ChromeSendCheck(self, i, line): 40 """Checks for a particular misuse of 'chrome.send'.""" 41 return self.RegexCheck(i, line, r"chrome\.send\('[^']+'\s*(, \[\])\)", 42 'Passing an empty array to chrome.send is unnecessary.') 43 44 def ConstCheck(self, i, line): 45 """Check for use of the 'const' keyword.""" 46 if self.input_api.re.search(r'\*\s+@const', line): 47 # Probably a JsDoc line 48 return '' 49 50 return self.RegexCheck(i, line, r'(?:^|\s|\()(const)\s', 51 'Use var instead of const.') 52 53 def GetElementByIdCheck(self, i, line): 54 """Checks for use of 'document.getElementById' instead of '$'.""" 55 return self.RegexCheck(i, line, r"(document\.getElementById)\('", 56 "Use $('id'), from chrome://resources/js/util.js, instead of " 57 "document.getElementById('id'))") 58 59 def error_highlight(self, start, length): 60 """Takes a start position and a length, and produces a row of '^'s to 61 highlight the corresponding part of a string. 62 """ 63 return start * ' ' + length * '^' 64 65 def _makeErrorOrWarning(self, error_text, filename): 66 """Takes a few lines of text indicating a style violation and turns it into 67 a PresubmitError (if |filename| is in a directory where we've already 68 taken out all the style guide violations) or a PresubmitPromptWarning 69 (if it's in a directory where we haven't done that yet). 70 """ 71 # TODO(tbreisacher): Once we've cleaned up the style nits in all of 72 # resources/ we can get rid of this function. 73 path = self.input_api.os_path 74 resources = self.input_api.PresubmitLocalPath() 75 dirs = ( 76 path.join(resources, 'extensions'), 77 path.join(resources, 'help'), 78 path.join(resources, 'history'), 79 path.join(resources, 'net_internals'), 80 path.join(resources, 'network_action_predictor'), 81 path.join(resources, 'ntp4'), 82 path.join(resources, 'options'), 83 path.join(resources, 'print_preview'), 84 path.join(resources, 'profiler'), 85 path.join(resources, 'sync_promo'), 86 path.join(resources, 'tracing'), 87 path.join(resources, 'uber'), 88 ) 89 if filename.startswith(dirs): 90 return self.output_api.PresubmitError(error_text) 91 else: 92 return self.output_api.PresubmitPromptWarning(error_text) 93 94 def RunChecks(self): 95 """Check for violations of the Chromium JavaScript style guide. See 96 http://chromium.org/developers/web-development-style-guide#TOC-JavaScript 97 """ 98 99 import sys 100 import warnings 101 old_path = sys.path 102 old_filters = warnings.filters 103 104 try: 105 closure_linter_path = self.input_api.os_path.join( 106 self.input_api.change.RepositoryRoot(), 107 "third_party", 108 "closure_linter") 109 gflags_path = self.input_api.os_path.join( 110 self.input_api.change.RepositoryRoot(), 111 "third_party", 112 "python_gflags") 113 114 sys.path.insert(0, closure_linter_path) 115 sys.path.insert(0, gflags_path) 116 117 warnings.filterwarnings('ignore', category=DeprecationWarning) 118 119 from closure_linter import checker, errors 120 from closure_linter.common import errorhandler 121 122 finally: 123 sys.path = old_path 124 warnings.filters = old_filters 125 126 class ErrorHandlerImpl(errorhandler.ErrorHandler): 127 """Filters out errors that don't apply to Chromium JavaScript code.""" 128 129 def __init__(self, re): 130 self._errors = [] 131 self.re = re 132 133 def HandleFile(self, filename, first_token): 134 self._filename = filename 135 136 def HandleError(self, error): 137 if (self._valid(error)): 138 error.filename = self._filename 139 self._errors.append(error) 140 141 def GetErrors(self): 142 return self._errors 143 144 def HasErrors(self): 145 return bool(self._errors) 146 147 def _valid(self, error): 148 """Check whether an error is valid. Most errors are valid, with a few 149 exceptions which are listed here. 150 """ 151 152 is_grit_statement = bool( 153 self.re.search("</?(include|if)", error.token.line)) 154 155 return not is_grit_statement and error.code not in [ 156 errors.COMMA_AT_END_OF_LITERAL, 157 errors.JSDOC_ILLEGAL_QUESTION_WITH_PIPE, 158 errors.JSDOC_TAG_DESCRIPTION_ENDS_WITH_INVALID_CHARACTER, 159 errors.LINE_TOO_LONG, 160 errors.MISSING_JSDOC_TAG_THIS, 161 ] 162 163 results = [] 164 165 affected_files = self.input_api.change.AffectedFiles( 166 file_filter=self.file_filter, 167 include_deletes=False) 168 affected_js_files = filter(lambda f: f.LocalPath().endswith('.js'), 169 affected_files) 170 for f in affected_js_files: 171 error_lines = [] 172 173 # Check for the following: 174 # * document.getElementById() 175 # * the 'const' keyword 176 # * Passing an empty array to 'chrome.send()' 177 for i, line in enumerate(f.NewContents(), start=1): 178 error_lines += filter(None, [ 179 self.ChromeSendCheck(i, line), 180 self.ConstCheck(i, line), 181 self.GetElementByIdCheck(i, line), 182 ]) 183 184 # Use closure_linter to check for several different errors 185 error_handler = ErrorHandlerImpl(self.input_api.re) 186 js_checker = checker.JavaScriptStyleChecker(error_handler) 187 js_checker.Check(self.input_api.os_path.join( 188 self.input_api.change.RepositoryRoot(), 189 f.LocalPath())) 190 191 for error in error_handler.GetErrors(): 192 highlight = self.error_highlight( 193 error.token.start_index, error.token.length) 194 error_msg = ' line %d: E%04d: %s\n%s\n%s' % ( 195 error.token.line_number, 196 error.code, 197 error.message, 198 error.token.line.rstrip(), 199 highlight) 200 error_lines.append(error_msg) 201 202 if error_lines: 203 error_lines = [ 204 'Found JavaScript style violations in %s:' % 205 f.LocalPath()] + error_lines 206 results.append(self._makeErrorOrWarning( 207 '\n'.join(error_lines), f.AbsoluteLocalPath())) 208 209 if results: 210 results.append(self.output_api.PresubmitNotifyResult( 211 'See the JavaScript style guide at ' 212 'http://www.chromium.org/developers/web-development-style-guide' 213 '#TOC-JavaScript and if you have any feedback about the JavaScript ' 214 'PRESUBMIT check, contact tbreisacher@chromium.org')) 215 216 return results 217