style.py revision 4678:fd95d7ddd1ee
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): 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) 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 check_whitespace(ui, repo, hooktype, node, parent1, parent2): 234 from mercurial import bdiff, mdiff, util 235 if hooktype != 'pretxncommit': 236 raise AttributeError, \ 237 "This hook is only meant for pretxncommit, not %s" % hooktype 238 239 tabsize = 8 240 verbose = ui.configbool('style', 'verbose', False) 241 def prompt(name, fixonly=None): 242 result = ui.prompt("(a)bort, (i)gnore, or (f)ix?", "^[aif]$", "a") 243 if result == 'a': 244 return True 245 elif result == 'i': 246 pass 247 elif result == 'f': 248 fixwhite(name, tabsize, fixonly) 249 else: 250 raise RepoError, "Invalid response: '%s'" % result 251 252 return False 253 254 modified, added, removed, deleted, unknown, ignore, clean = repo.status() 255 256 for fname in added: 257 ok = True 258 for line,num in checkwhite(fname): 259 ui.write("invalid whitespace in %s:%d\n" % (fname, num)) 260 if verbose: 261 ui.write(">>%s<<\n" % line[-1]) 262 ok = False 263 264 if not ok: 265 if prompt(fname): 266 return True 267 268 wctx = repo.workingctx() 269 for fname in modified: 270 fctx = wctx.filectx(fname) 271 pctx = fctx.parents() 272 assert len(pctx) == 1 273 274 pdata = pctx[0].data() 275 fdata = fctx.data() 276 277 fixonly = set() 278 lines = enumerate(mdiff.splitnewlines(fdata)) 279 for pbeg, pend, fbeg, fend in bdiff.blocks(pdata, fdata): 280 for i, line in lines: 281 if i < fbeg: 282 if checkwhite_line(line): 283 continue 284 285 ui.write("invalid whitespace: %s:%d\n" % (fname, i+1)) 286 if verbose: 287 ui.write(">>%s<<\n" % line[:-1]) 288 fixonly.add(i) 289 elif i + 1 >= fend: 290 break 291 292 if fixonly: 293 if prompt(fname, fixonly): 294 return True 295 296def check_format(ui, repo, hooktype, node, parent1, parent2): 297 if hooktype != 'pretxncommit': 298 raise AttributeError, \ 299 "This hook is only meant for pretxncommit, not %s" % hooktype 300 301 modified, added, removed, deleted, unknown, ignore, clean = repo.status() 302 303 verbose = 0 304 stats = ValidationStats() 305 for f in modified + added: 306 validate(f, stats, verbose, None) 307 308 if stats: 309 stats.dump() 310 result = ui.prompt("invalid formatting\n(i)gnore or (a)bort?", 311 "^[ia]$", "a") 312 if result.startswith('i'): 313 pass 314 elif result.startswith('a'): 315 return True 316 else: 317 raise RepoError, "Invalid response: '%s'" % result 318 319 return False 320 321if __name__ == '__main__': 322 import getopt 323 324 progname = sys.argv[0] 325 if len(sys.argv) < 2: 326 sys.exit('usage: %s <command> [<command args>]' % progname) 327 328 fixwhite_usage = '%s fixwhite [-t <tabsize> ] <path> [...] \n' % progname 329 chkformat_usage = '%s chkformat <path> [...] \n' % progname 330 chkwhite_usage = '%s chkwhite <path> [...] \n' % progname 331 332 command = sys.argv[1] 333 if command == 'fixwhite': 334 flags = 't:' 335 usage = fixwhite_usage 336 elif command == 'chkwhite': 337 flags = 'nv' 338 usage = chkwhite_usage 339 elif command == 'chkformat': 340 flags = 'nv' 341 usage = chkformat_usage 342 else: 343 sys.exit(fixwhite_usage + chkwhite_usage + chkformat_usage) 344 345 opts, args = getopt.getopt(sys.argv[2:], flags) 346 347 code = 1 348 verbose = 1 349 tabsize = 8 350 for opt,arg in opts: 351 if opt == '-n': 352 code = None 353 if opt == '-t': 354 tabsize = int(arg) 355 if opt == '-v': 356 verbose += 1 357 358 if command == 'fixwhite': 359 for filename in args: 360 fixwhite(filename, tabsize) 361 elif command == 'chkwhite': 362 for filename in args: 363 line = checkwhite(filename) 364 if line: 365 print 'invalid whitespace at %s:%d' % (filename, line) 366 elif command == 'chkformat': 367 stats = ValidationStats() 368 for filename in files: 369 validate(filename, stats=stats, verbose=verbose, exit_code=code) 370 371 if verbose > 0: 372 stats.dump() 373 else: 374 sys.exit("command '%s' not found" % command) 375