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