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