style.py revision 4745:33b409225928
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" } 54whitespace_types = ('C', 'C++', 'swig', 'python', 'asm', 'isa') 55format_types = ( 'C', 'C++' ) 56 57def file_type(filename): 58 extension = filename.split('.') 59 extension = len(extension) > 1 and extension[-1] 60 return lang_types.get(extension, None) 61 62def checkwhite_line(line): 63 match = lead.search(line) 64 if match and match.group(1).find('\t') != -1: 65 return False 66 67 match = trail.search(line) 68 if match: 69 return False 70 71 return True 72 73def checkwhite(filename): 74 if file_type(filename) not in whitespace_types: 75 return 76 77 try: 78 f = file(filename, 'r+') 79 except OSError, msg: 80 print 'could not open file %s: %s' % (filename, msg) 81 return 82 83 for num,line in enumerate(f): 84 if not checkwhite_line(line): 85 yield line,num + 1 86 87def fixwhite_line(line, tabsize): 88 if lead.search(line): 89 newline = '' 90 for i,c in enumerate(line): 91 if c == ' ': 92 newline += ' ' 93 elif c == '\t': 94 newline += ' ' * (tabsize - len(newline) % tabsize) 95 else: 96 newline += line[i:] 97 break 98 99 line = newline 100 101 return line.rstrip() + '\n' 102 103def fixwhite(filename, tabsize, fixonly=None): 104 if file_type(filename) not in whitespace_types: 105 return 106 107 try: 108 f = file(filename, 'r+') 109 except OSError, msg: 110 print 'could not open file %s: %s' % (filename, msg) 111 return 112 113 lines = list(f) 114 115 f.seek(0) 116 f.truncate() 117 118 for i,line in enumerate(lines): 119 if fixonly is None or i in fixonly: 120 line = fixwhite_line(line, tabsize) 121 122 print >>f, line, 123 124def linelen(line): 125 tabs = line.count('\t') 126 if not tabs: 127 return len(line) 128 129 count = 0 130 for c in line: 131 if c == '\t': 132 count += tabsize - count % tabsize 133 else: 134 count += 1 135 136 return count 137 138class ValidationStats(object): 139 def __init__(self): 140 self.toolong = 0 141 self.toolong80 = 0 142 self.leadtabs = 0 143 self.trailwhite = 0 144 self.badcontrol = 0 145 self.cret = 0 146 147 def dump(self): 148 print '''\ 149%d violations of lines over 79 chars. %d of which are 80 chars exactly. 150%d cases of whitespace at the end of a line. 151%d cases of tabs to indent. 152%d bad parens after if/while/for. 153%d carriage returns found. 154''' % (self.toolong, self.toolong80, self.trailwhite, self.leadtabs, 155 self.badcontrol, self.cret) 156 157 def __nonzero__(self): 158 return self.toolong or self.toolong80 or self.leadtabs or \ 159 self.trailwhite or self.badcontrol or self.cret 160 161def validate(filename, stats, verbose, exit_code): 162 if file_type(filename) not in format_types: 163 return 164 165 def msg(lineno, line, message): 166 print '%s:%d>' % (filename, lineno + 1), message 167 if verbose > 2: 168 print line 169 170 def bad(): 171 if exit_code is not None: 172 sys.exit(exit_code) 173 174 cpp = filename.endswith('.cc') or filename.endswith('.hh') 175 py = filename.endswith('.py') 176 177 if py + cpp != 1: 178 raise AttributeError, \ 179 "I don't know how to deal with the file %s" % filename 180 181 try: 182 f = file(filename, 'r') 183 except OSError: 184 if verbose > 0: 185 print 'could not open file %s' % filename 186 bad() 187 return 188 189 for i,line in enumerate(f): 190 line = line.rstrip('\n') 191 192 # no carriage returns 193 if line.find('\r') != -1: 194 self.cret += 1 195 if verbose > 1: 196 msg(i, line, 'carriage return found') 197 bad() 198 199 # lines max out at 79 chars 200 llen = linelen(line) 201 if llen > 79: 202 stats.toolong += 1 203 if llen == 80: 204 stats.toolong80 += 1 205 if verbose > 1: 206 msg(i, line, 'line too long (%d chars)' % llen) 207 bad() 208 209 # no tabs used to indent 210 match = lead.search(line) 211 if match and match.group(1).find('\t') != -1: 212 stats.leadtabs += 1 213 if verbose > 1: 214 msg(i, line, 'using tabs to indent') 215 bad() 216 217 # no trailing whitespace 218 if trail.search(line): 219 stats.trailwhite +=1 220 if verbose > 1: 221 msg(i, line, 'trailing whitespace') 222 bad() 223 224 # for c++, exactly one space betwen if/while/for and ( 225 if cpp: 226 match = any_control.search(line) 227 if match and not good_control.search(line): 228 stats.badcontrol += 1 229 if verbose > 1: 230 msg(i, line, 'improper spacing after %s' % match.group(1)) 231 bad() 232 233def modified_lines(old_data, new_data): 234 from itertools import count 235 from mercurial import bdiff, mdiff 236 237 modified = set() 238 counter = count() 239 for pbeg, pend, fbeg, fend in bdiff.blocks(old_data, new_data): 240 for i in counter: 241 if i < fbeg: 242 modified.add(i) 243 elif i + 1 >= fend: 244 break 245 return modified 246 247def check_whitespace(ui, repo, hooktype, node, parent1, parent2): 248 from mercurial import mdiff 249 250 if hooktype != 'pretxncommit': 251 raise AttributeError, \ 252 "This hook is only meant for pretxncommit, not %s" % hooktype 253 254 tabsize = 8 255 verbose = ui.configbool('style', 'verbose', False) 256 def prompt(name, fixonly=None): 257 result = ui.prompt("(a)bort, (i)gnore, or (f)ix?", "^[aif]$", "a") 258 if result == 'a': 259 return True 260 elif result == 'i': 261 pass 262 elif result == 'f': 263 fixwhite(name, tabsize, fixonly) 264 else: 265 raise RepoError, "Invalid response: '%s'" % result 266 267 return False 268 269 modified, added, removed, deleted, unknown, ignore, clean = repo.status() 270 271 for fname in added: 272 ok = True 273 for line,num in checkwhite(fname): 274 ui.write("invalid whitespace in %s:%d\n" % (fname, num)) 275 if verbose: 276 ui.write(">>%s<<\n" % line[-1]) 277 ok = False 278 279 if not ok: 280 if prompt(fname): 281 return True 282 283 wctx = repo.workingctx() 284 for fname in modified: 285 fctx = wctx.filectx(fname) 286 pctx = fctx.parents() 287 assert len(pctx) in (1, 2) 288 289 file_data = fctx.data() 290 mod_lines = modified_lines(pctx[0].data(), file_data) 291 if len(pctx) == 2: 292 m2 = modified_lines(pctx[1].data(), file_data) 293 mod_lines = mod_lines & m2 # only the lines that are new in both 294 295 fixonly = set() 296 for i,line in enumerate(mdiff.splitnewlines(file_data)): 297 if i not in mod_lines: 298 continue 299 300 if checkwhite_line(line): 301 continue 302 303 ui.write("invalid whitespace: %s:%d\n" % (fname, i+1)) 304 if verbose: 305 ui.write(">>%s<<\n" % line[:-1]) 306 fixonly.add(i) 307 308 if fixonly: 309 if prompt(fname, fixonly): 310 return True 311 312def check_format(ui, repo, hooktype, node, parent1, parent2): 313 if hooktype != 'pretxncommit': 314 raise AttributeError, \ 315 "This hook is only meant for pretxncommit, not %s" % hooktype 316 317 modified, added, removed, deleted, unknown, ignore, clean = repo.status() 318 319 verbose = 0 320 stats = ValidationStats() 321 for f in modified + added: 322 validate(f, stats, verbose, None) 323 324 if stats: 325 stats.dump() 326 result = ui.prompt("invalid formatting\n(i)gnore or (a)bort?", 327 "^[ia]$", "a") 328 if result.startswith('i'): 329 pass 330 elif result.startswith('a'): 331 return True 332 else: 333 raise RepoError, "Invalid response: '%s'" % result 334 335 return False 336 337if __name__ == '__main__': 338 import getopt 339 340 progname = sys.argv[0] 341 if len(sys.argv) < 2: 342 sys.exit('usage: %s <command> [<command args>]' % progname) 343 344 fixwhite_usage = '%s fixwhite [-t <tabsize> ] <path> [...] \n' % progname 345 chkformat_usage = '%s chkformat <path> [...] \n' % progname 346 chkwhite_usage = '%s chkwhite <path> [...] \n' % progname 347 348 command = sys.argv[1] 349 if command == 'fixwhite': 350 flags = 't:' 351 usage = fixwhite_usage 352 elif command == 'chkwhite': 353 flags = 'nv' 354 usage = chkwhite_usage 355 elif command == 'chkformat': 356 flags = 'nv' 357 usage = chkformat_usage 358 else: 359 sys.exit(fixwhite_usage + chkwhite_usage + chkformat_usage) 360 361 opts, args = getopt.getopt(sys.argv[2:], flags) 362 363 code = 1 364 verbose = 1 365 tabsize = 8 366 for opt,arg in opts: 367 if opt == '-n': 368 code = None 369 if opt == '-t': 370 tabsize = int(arg) 371 if opt == '-v': 372 verbose += 1 373 374 if command == 'fixwhite': 375 for filename in args: 376 fixwhite(filename, tabsize) 377 elif command == 'chkwhite': 378 for filename in args: 379 line = checkwhite(filename) 380 if line: 381 print 'invalid whitespace at %s:%d' % (filename, line) 382 elif command == 'chkformat': 383 stats = ValidationStats() 384 for filename in files: 385 validate(filename, stats=stats, verbose=verbose, exit_code=code) 386 387 if verbose > 0: 388 stats.dump() 389 else: 390 sys.exit("command '%s' not found" % command) 391