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 try: 166 affected_files = self.input_api.change.AffectedFiles( 167 file_filter=self.file_filter, 168 include_deletes=False) 169 except: 170 affected_files = [] 171 172 affected_js_files = filter(lambda f: f.LocalPath().endswith('.js'), 173 affected_files) 174 for f in affected_js_files: 175 error_lines = [] 176 177 # Check for the following: 178 # * document.getElementById() 179 # * the 'const' keyword 180 # * Passing an empty array to 'chrome.send()' 181 for i, line in enumerate(f.NewContents(), start=1): 182 error_lines += filter(None, [ 183 self.ChromeSendCheck(i, line), 184 self.ConstCheck(i, line), 185 self.GetElementByIdCheck(i, line), 186 ]) 187 188 # Use closure_linter to check for several different errors 189 import gflags as flags 190 flags.FLAGS.strict = True 191 error_handler = ErrorHandlerImpl(self.input_api.re) 192 js_checker = checker.JavaScriptStyleChecker(error_handler) 193 js_checker.Check(self.input_api.os_path.join( 194 self.input_api.change.RepositoryRoot(), 195 f.LocalPath())) 196 197 for error in error_handler.GetErrors(): 198 highlight = self.error_highlight( 199 error.token.start_index, error.token.length) 200 error_msg = ' line %d: E%04d: %s\n%s\n%s' % ( 201 error.token.line_number, 202 error.code, 203 error.message, 204 error.token.line.rstrip(), 205 highlight) 206 error_lines.append(error_msg) 207 208 if error_lines: 209 error_lines = [ 210 'Found JavaScript style violations in %s:' % 211 f.LocalPath()] + error_lines 212 results.append(self._makeErrorOrWarning( 213 '\n'.join(error_lines), f.AbsoluteLocalPath())) 214 215 if results: 216 results.append(self.output_api.PresubmitNotifyResult( 217 'See the JavaScript style guide at ' 218 'http://www.chromium.org/developers/web-development-style-guide' 219 '#TOC-JavaScript and if you have any feedback about the JavaScript ' 220 'PRESUBMIT check, contact tbreisacher@chromium.org')) 221 222 return results 223