style.py revision 5747:ffded3077c63
1#! /usr/bin/env python 2# Copyright (c) 2006 The Regents of The University of Michigan 3# Copyright (c) 2007 The Hewlett-Packard Development Company 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions are 8# met: redistributions of source code must retain the above copyright 9# notice, this list of conditions and the following disclaimer; 10# redistributions in binary form must reproduce the above copyright 11# notice, this list of conditions and the following disclaimer in the 12# documentation and/or other materials provided with the distribution; 13# neither the name of the copyright holders nor the names of its 14# contributors may be used to endorse or promote products derived from 15# this software without specific prior written permission. 16# 17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28# 29# Authors: Nathan Binkert 30 31import re 32import os 33import sys 34 35lead = re.compile(r'^([ \t]+)') 36trail = re.compile(r'([ \t]+)$') 37any_control = re.compile(r'\b(if|while|for)[ \t]*[(]') 38good_control = re.compile(r'\b(if|while|for) [(]') 39 40lang_types = { 'c' : "C", 41 'h' : "C", 42 'cc' : "C++", 43 'hh' : "C++", 44 'cxx' : "C++", 45 'hxx' : "C++", 46 'cpp' : "C++", 47 'hpp' : "C++", 48 'C' : "C++", 49 'H' : "C++", 50 'i' : "swig", 51 'py' : "python", 52 's' : "asm", 53 'S' : "asm", 54 'isa' : "isa" } 55def file_type(filename): 56 extension = filename.split('.') 57 extension = len(extension) > 1 and extension[-1] 58 return lang_types.get(extension, None) 59 60whitespace_types = ('C', 'C++', 'swig', 'python', 'asm', 'isa') 61def whitespace_file(filename): 62 if file_type(filename) in whitespace_types: 63 return True 64 65 if filename.startswith("SCons"): 66 return True 67 68 return False 69 70format_types = ( 'C', 'C++' ) 71def format_file(filename): 72 if file_type(filename) in format_types: 73 return True 74 75 return False 76 77def checkwhite_line(line): 78 match = lead.search(line) 79 if match and match.group(1).find('\t') != -1: 80 return False 81 82 match = trail.search(line) 83 if match: 84 return False 85 86 return True 87 88def checkwhite(filename): 89 if not whitespace_file(filename): 90 return 91 92 try: 93 f = file(filename, 'r+') 94 except OSError, msg: 95 print 'could not open file %s: %s' % (filename, msg) 96 return 97 98 for num,line in enumerate(f): 99 if not checkwhite_line(line): 100 yield line,num + 1 101 102def fixwhite_line(line, tabsize): 103 if lead.search(line): 104 newline = '' 105 for i,c in enumerate(line): 106 if c == ' ': 107 newline += ' ' 108 elif c == '\t': 109 newline += ' ' * (tabsize - len(newline) % tabsize) 110 else: 111 newline += line[i:] 112 break 113 114 line = newline 115 116 return line.rstrip() + '\n' 117 118def fixwhite(filename, tabsize, fixonly=None): 119 if not whitespace_file(filename): 120 return 121 122 try: 123 f = file(filename, 'r+') 124 except OSError, msg: 125 print 'could not open file %s: %s' % (filename, msg) 126 return 127 128 lines = list(f) 129 130 f.seek(0) 131 f.truncate() 132 133 for i,line in enumerate(lines): 134 if fixonly is None or i in fixonly: 135 line = fixwhite_line(line, tabsize) 136 137 print >>f, line, 138 139def linelen(line): 140 tabs = line.count('\t') 141 if not tabs: 142 return len(line) 143 144 count = 0 145 for c in line: 146 if c == '\t': 147 count += tabsize - count % tabsize 148 else: 149 count += 1 150 151 return count 152 153class ValidationStats(object): 154 def __init__(self): 155 self.toolong = 0 156 self.toolong80 = 0 157 self.leadtabs = 0 158 self.trailwhite = 0 159 self.badcontrol = 0 160 self.cret = 0 161 162 def dump(self): 163 print '''\ 164%d violations of lines over 79 chars. %d of which are 80 chars exactly. 165%d cases of whitespace at the end of a line. 166%d cases of tabs to indent. 167%d bad parens after if/while/for. 168%d carriage returns found. 169''' % (self.toolong, self.toolong80, self.trailwhite, self.leadtabs, 170 self.badcontrol, self.cret) 171 172 def __nonzero__(self): 173 return self.toolong or self.toolong80 or self.leadtabs or \ 174 self.trailwhite or self.badcontrol or self.cret 175 176def validate(filename, stats, verbose, exit_code): 177 if not format_file(filename): 178 return 179 180 def msg(lineno, line, message): 181 print '%s:%d>' % (filename, lineno + 1), message 182 if verbose > 2: 183 print line 184 185 def bad(): 186 if exit_code is not None: 187 sys.exit(exit_code) 188 189 cpp = filename.endswith('.cc') or filename.endswith('.hh') 190 py = filename.endswith('.py') 191 192 if py + cpp != 1: 193 raise AttributeError, \ 194 "I don't know how to deal with the file %s" % filename 195 196 try: 197 f = file(filename, 'r') 198 except OSError: 199 if verbose > 0: 200 print 'could not open file %s' % filename 201 bad() 202 return 203 204 for i,line in enumerate(f): 205 line = line.rstrip('\n') 206 207 # no carriage returns 208 if line.find('\r') != -1: 209 self.cret += 1 210 if verbose > 1: 211 msg(i, line, 'carriage return found') 212 bad() 213 214 # lines max out at 79 chars 215 llen = linelen(line) 216 if llen > 79: 217 stats.toolong += 1 218 if llen == 80: 219 stats.toolong80 += 1 220 if verbose > 1: 221 msg(i, line, 'line too long (%d chars)' % llen) 222 bad() 223 224 # no tabs used to indent 225 match = lead.search(line) 226 if match and match.group(1).find('\t') != -1: 227 stats.leadtabs += 1 228 if verbose > 1: 229 msg(i, line, 'using tabs to indent') 230 bad() 231 232 # no trailing whitespace 233 if trail.search(line): 234 stats.trailwhite +=1 235 if verbose > 1: 236 msg(i, line, 'trailing whitespace') 237 bad() 238 239 # for c++, exactly one space betwen if/while/for and ( 240 if cpp: 241 match = any_control.search(line) 242 if match and not good_control.search(line): 243 stats.badcontrol += 1 244 if verbose > 1: 245 msg(i, line, 'improper spacing after %s' % match.group(1)) 246 bad() 247 248def modified_lines(old_data, new_data, max_lines): 249 from itertools import count 250 from mercurial import bdiff, mdiff 251 252 modified = set() 253 counter = count() 254 for pbeg, pend, fbeg, fend in bdiff.blocks(old_data, new_data): 255 for i in counter: 256 if i < fbeg: 257 modified.add(i) 258 elif i + 1 >= fend: 259 break 260 elif i > max_lines: 261 break 262 return modified 263 264def do_check_whitespace(ui, repo, *files, **args): 265 """check files for proper m5 style guidelines""" 266 from mercurial import mdiff, util 267 268 if files: 269 files = frozenset(files) 270 271 def skip(name): 272 return files and name in files 273 274 def prompt(name, fixonly=None): 275 if args.get('auto', False): 276 result = 'f' 277 else: 278 result = ui.prompt("(a)bort, (i)gnore, or (f)ix?", "^[aif]$", "a") 279 if result == 'a': 280 return True 281 elif result == 'i': 282 pass 283 elif result == 'f': 284 fixwhite(repo.wjoin(name), args['tabsize'], fixonly) 285 else: 286 raise util.Abort(_("Invalid response: '%s'") % result) 287 288 return False 289 290 modified, added, removed, deleted, unknown, ignore, clean = repo.status() 291 292 for fname in added: 293 if skip(fname): 294 continue 295 296 ok = True 297 for line,num in checkwhite(repo.wjoin(fname)): 298 ui.write("invalid whitespace in %s:%d\n" % (fname, num)) 299 if ui.verbose: 300 ui.write(">>%s<<\n" % line[-1]) 301 ok = False 302 303 if not ok: 304 if prompt(fname): 305 return True 306 307 wctx = repo.workingctx() 308 for fname in modified: 309 if skip(fname): 310 continue 311 312 if not whitespace_file(fname): 313 continue 314 315 fctx = wctx.filectx(fname) 316 pctx = fctx.parents() 317 318 file_data = fctx.data() 319 lines = mdiff.splitnewlines(file_data) 320 if len(pctx) in (1, 2): 321 mod_lines = modified_lines(pctx[0].data(), file_data, len(lines)) 322 if len(pctx) == 2: 323 m2 = modified_lines(pctx[1].data(), file_data, len(lines)) 324 mod_lines = mod_lines & m2 # only the lines that are new in both 325 else: 326 mod_lines = xrange(0, len(lines)) 327 328 fixonly = set() 329 for i,line in enumerate(lines): 330 if i not in mod_lines: 331 continue 332 333 if checkwhite_line(line): 334 continue 335 336 ui.write("invalid whitespace: %s:%d\n" % (fname, i+1)) 337 if ui.verbose: 338 ui.write(">>%s<<\n" % line[:-1]) 339 fixonly.add(i) 340 341 if fixonly: 342 if prompt(fname, fixonly): 343 return True 344 345def check_whitespace(ui, repo, hooktype, node, parent1, parent2): 346 if hooktype != 'pretxncommit': 347 raise AttributeError, \ 348 "This hook is only meant for pretxncommit, not %s" % hooktype 349 350 args = { 'tabsize' : 8 } 351 do_check_whitespace(ui, repo, **args) 352 353def check_format(ui, repo, hooktype, node, parent1, parent2): 354 if hooktype != 'pretxncommit': 355 raise AttributeError, \ 356 "This hook is only meant for pretxncommit, not %s" % hooktype 357 358 modified, added, removed, deleted, unknown, ignore, clean = repo.status() 359 360 verbose = 0 361 stats = ValidationStats() 362 for f in modified + added: 363 validate(f, stats, verbose, None) 364 365 if stats: 366 stats.dump() 367 result = ui.prompt("invalid formatting\n(i)gnore or (a)bort?", 368 "^[ia]$", "a") 369 if result.startswith('i'): 370 pass 371 elif result.startswith('a'): 372 return True 373 else: 374 raise util.Abort(_("Invalid response: '%s'") % result) 375 376 return False 377 378try: 379 from mercurial.i18n import _ 380except ImportError: 381 def _(arg): 382 return arg 383 384cmdtable = { 385 '^m5style' : 386 ( do_check_whitespace, 387 [ ('a', 'auto', False, _("automatically fix whitespace")), 388 ('t', 'tabsize', 8, _("Number of spaces TAB indents")) ], 389 _('hg m5check [-t <tabsize>] [FILE]...')), 390} 391if __name__ == '__main__': 392 import getopt 393 394 progname = sys.argv[0] 395 if len(sys.argv) < 2: 396 sys.exit('usage: %s <command> [<command args>]' % progname) 397 398 fixwhite_usage = '%s fixwhite [-t <tabsize> ] <path> [...] \n' % progname 399 chkformat_usage = '%s chkformat <path> [...] \n' % progname 400 chkwhite_usage = '%s chkwhite <path> [...] \n' % progname 401 402 command = sys.argv[1] 403 if command == 'fixwhite': 404 flags = 't:' 405 usage = fixwhite_usage 406 elif command == 'chkwhite': 407 flags = 'nv' 408 usage = chkwhite_usage 409 elif command == 'chkformat': 410 flags = 'nv' 411 usage = chkformat_usage 412 else: 413 sys.exit(fixwhite_usage + chkwhite_usage + chkformat_usage) 414 415 opts, args = getopt.getopt(sys.argv[2:], flags) 416 417 code = 1 418 verbose = 1 419 tabsize = 8 420 for opt,arg in opts: 421 if opt == '-n': 422 code = None 423 if opt == '-t': 424 tabsize = int(arg) 425 if opt == '-v': 426 verbose += 1 427 428 if command == 'fixwhite': 429 for filename in args: 430 fixwhite(filename, tabsize) 431 elif command == 'chkwhite': 432 for filename in args: 433 for line,num in checkwhite(filename): 434 print 'invalid whitespace: %s:%d' % (filename, num) 435 if verbose: 436 print '>>%s<<' % line[:-1] 437 elif command == 'chkformat': 438 stats = ValidationStats() 439 for filename in args: 440 validate(filename, stats=stats, verbose=verbose, exit_code=code) 441 442 if verbose > 0: 443 stats.dump() 444 else: 445 sys.exit("command '%s' not found" % command) 446