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