style.py revision 8228:59d3bfa85f16
1#! /usr/bin/env python 2# Copyright (c) 2006 The Regents of The University of Michigan 3# Copyright (c) 2007,2011 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 heapq 32import os 33import re 34import sys 35 36from os.path import dirname, join as joinpath 37from itertools import count 38from mercurial import bdiff, mdiff 39 40current_dir = dirname(__file__) 41sys.path.insert(0, current_dir) 42sys.path.insert(1, joinpath(dirname(current_dir), 'src', 'python')) 43 44from m5.util import neg_inf, pos_inf, Region, Regions 45import sort_includes 46from file_types import lang_type 47 48all_regions = Region(neg_inf, pos_inf) 49 50tabsize = 8 51lead = re.compile(r'^([ \t]+)') 52trail = re.compile(r'([ \t]+)$') 53any_control = re.compile(r'\b(if|while|for)[ \t]*[(]') 54good_control = re.compile(r'\b(if|while|for) [(]') 55 56format_types = set(('C', 'C++')) 57 58def modified_regions(old_data, new_data): 59 regions = Regions() 60 beg = None 61 for pbeg, pend, fbeg, fend in bdiff.blocks(old_data, new_data): 62 if beg is not None and beg != fbeg: 63 regions.append(beg, fbeg) 64 beg = fend 65 return regions 66 67def modregions(wctx, fname): 68 fctx = wctx.filectx(fname) 69 pctx = fctx.parents() 70 71 file_data = fctx.data() 72 lines = mdiff.splitnewlines(file_data) 73 if len(pctx) in (1, 2): 74 mod_regions = modified_regions(pctx[0].data(), file_data) 75 if len(pctx) == 2: 76 m2 = modified_regions(pctx[1].data(), file_data) 77 # only the lines that are new in both 78 mod_regions &= m2 79 else: 80 mod_regions = Regions() 81 mod_regions.add(0, len(lines)) 82 83 return mod_regions 84 85class UserInterface(object): 86 def __init__(self, verbose=False, auto=False): 87 self.auto = auto 88 self.verbose = verbose 89 90 def prompt(self, prompt, results, default): 91 if self.auto: 92 return self.auto 93 94 while True: 95 result = self.do_prompt(prompt, results, default) 96 if result in results: 97 return result 98 99class MercurialUI(UserInterface): 100 def __init__(self, ui, *args, **kwargs): 101 super(MercurialUI, self).__init__(*args, **kwargs) 102 self.ui = ui 103 104 def do_prompt(self, prompt, results, default): 105 return self.ui.prompt(prompt, default=default) 106 107 def write(self, string): 108 self.ui.write(string) 109 110class StdioUI(UserInterface): 111 def do_prompt(self, prompt, results, default): 112 return raw_input(prompt) or default 113 114 def write(self, string): 115 sys.stdout.write(string) 116 117class Region(object): 118 def __init__(self, asdf): 119 self.regions = Foo 120 121class Verifier(object): 122 def __init__(self, ui, repo=None): 123 self.ui = ui 124 self.repo = repo 125 if repo is None: 126 self.wctx = None 127 128 def __getattr__(self, attr): 129 if attr in ('prompt', 'write'): 130 return getattr(self.ui, attr) 131 132 if attr == 'wctx': 133 try: 134 wctx = repo.workingctx() 135 except: 136 from mercurial import context 137 wctx = context.workingctx(repo) 138 self.wctx = wctx 139 return wctx 140 141 raise AttributeError 142 143 def open(self, filename, mode): 144 if self.repo: 145 filename = self.repo.wjoin(filename) 146 147 try: 148 f = file(filename, mode) 149 except OSError, msg: 150 print 'could not open file %s: %s' % (filename, msg) 151 return None 152 153 return f 154 155 def skip(self, filename): 156 return lang_type(filename) not in self.languages 157 158 def check(self, filename, regions=all_regions): 159 f = self.open(filename, 'r') 160 161 errors = 0 162 for num,line in enumerate(f): 163 if num not in regions: 164 continue 165 if not self.check_line(line): 166 self.write("invalid %s in %s:%d\n" % \ 167 (self.test_name, filename, num + 1)) 168 if self.ui.verbose: 169 self.write(">>%s<<\n" % line[-1]) 170 errors += 1 171 return errors 172 173 def fix(self, filename, regions=all_regions): 174 f = self.open(filename, 'r+') 175 176 lines = list(f) 177 178 f.seek(0) 179 f.truncate() 180 181 for i,line in enumerate(lines): 182 if i in regions: 183 line = self.fix_line(line) 184 185 f.write(line) 186 f.close() 187 188 def apply(self, filename, prompt, regions=all_regions): 189 if not self.skip(filename): 190 errors = self.check(filename, regions) 191 if errors: 192 if prompt(filename, self.fix, regions): 193 return True 194 return False 195 196 197class Whitespace(Verifier): 198 languages = set(('C', 'C++', 'swig', 'python', 'asm', 'isa', 'scons')) 199 test_name = 'whitespace' 200 def check_line(self, line): 201 match = lead.search(line) 202 if match and match.group(1).find('\t') != -1: 203 return False 204 205 match = trail.search(line) 206 if match: 207 return False 208 209 return True 210 211 def fix_line(self, line): 212 if lead.search(line): 213 newline = '' 214 for i,c in enumerate(line): 215 if c == ' ': 216 newline += ' ' 217 elif c == '\t': 218 newline += ' ' * (tabsize - len(newline) % tabsize) 219 else: 220 newline += line[i:] 221 break 222 223 line = newline 224 225 return line.rstrip() + '\n' 226 227class SortedIncludes(Verifier): 228 languages = sort_includes.default_languages 229 def __init__(self, *args, **kwargs): 230 super(SortedIncludes, self).__init__(*args, **kwargs) 231 self.sort_includes = sort_includes.SortIncludes() 232 233 def check(self, filename, regions=all_regions): 234 f = self.open(filename, 'r') 235 236 lines = [ l.rstrip('\n') for l in f.xreadlines() ] 237 old = ''.join(line + '\n' for line in lines) 238 f.close() 239 240 language = lang_type(filename, lines[0]) 241 sort_lines = list(self.sort_includes(lines, filename, language)) 242 new = ''.join(line + '\n' for line in sort_lines) 243 244 mod = modified_regions(old, new) 245 modified = mod & regions 246 print mod, regions, modified 247 248 if modified: 249 self.write("invalid sorting of includes\n") 250 if self.ui.verbose: 251 for start, end in modified.regions: 252 self.write("bad region [%d, %d)\n" % (start, end)) 253 return 1 254 255 return 0 256 257 def fix(self, filename, regions=all_regions): 258 f = self.open(filename, 'r+') 259 260 old = f.readlines() 261 lines = [ l.rstrip('\n') for l in old ] 262 language = lang_type(filename, lines[0]) 263 sort_lines = list(self.sort_includes(lines, filename, language)) 264 new = ''.join(line + '\n' for line in sort_lines) 265 266 f.seek(0) 267 f.truncate() 268 269 for i,line in enumerate(sort_lines): 270 f.write(line) 271 f.write('\n') 272 f.close() 273 274def linelen(line): 275 tabs = line.count('\t') 276 if not tabs: 277 return len(line) 278 279 count = 0 280 for c in line: 281 if c == '\t': 282 count += tabsize - count % tabsize 283 else: 284 count += 1 285 286 return count 287 288class ValidationStats(object): 289 def __init__(self): 290 self.toolong = 0 291 self.toolong80 = 0 292 self.leadtabs = 0 293 self.trailwhite = 0 294 self.badcontrol = 0 295 self.cret = 0 296 297 def dump(self): 298 print '''\ 299%d violations of lines over 79 chars. %d of which are 80 chars exactly. 300%d cases of whitespace at the end of a line. 301%d cases of tabs to indent. 302%d bad parens after if/while/for. 303%d carriage returns found. 304''' % (self.toolong, self.toolong80, self.trailwhite, self.leadtabs, 305 self.badcontrol, self.cret) 306 307 def __nonzero__(self): 308 return self.toolong or self.toolong80 or self.leadtabs or \ 309 self.trailwhite or self.badcontrol or self.cret 310 311def validate(filename, stats, verbose, exit_code): 312 if lang_type(filename) not in format_types: 313 return 314 315 def msg(lineno, line, message): 316 print '%s:%d>' % (filename, lineno + 1), message 317 if verbose > 2: 318 print line 319 320 def bad(): 321 if exit_code is not None: 322 sys.exit(exit_code) 323 324 try: 325 f = file(filename, 'r') 326 except OSError: 327 if verbose > 0: 328 print 'could not open file %s' % filename 329 bad() 330 return 331 332 for i,line in enumerate(f): 333 line = line.rstrip('\n') 334 335 # no carriage returns 336 if line.find('\r') != -1: 337 self.cret += 1 338 if verbose > 1: 339 msg(i, line, 'carriage return found') 340 bad() 341 342 # lines max out at 79 chars 343 llen = linelen(line) 344 if llen > 79: 345 stats.toolong += 1 346 if llen == 80: 347 stats.toolong80 += 1 348 if verbose > 1: 349 msg(i, line, 'line too long (%d chars)' % llen) 350 bad() 351 352 # no tabs used to indent 353 match = lead.search(line) 354 if match and match.group(1).find('\t') != -1: 355 stats.leadtabs += 1 356 if verbose > 1: 357 msg(i, line, 'using tabs to indent') 358 bad() 359 360 # no trailing whitespace 361 if trail.search(line): 362 stats.trailwhite +=1 363 if verbose > 1: 364 msg(i, line, 'trailing whitespace') 365 bad() 366 367 # for c++, exactly one space betwen if/while/for and ( 368 if cpp: 369 match = any_control.search(line) 370 if match and not good_control.search(line): 371 stats.badcontrol += 1 372 if verbose > 1: 373 msg(i, line, 'improper spacing after %s' % match.group(1)) 374 bad() 375 376def do_check_style(hgui, repo, *files, **args): 377 """check files for proper m5 style guidelines""" 378 from mercurial import mdiff, util 379 380 auto = args.get('auto', False) 381 if auto: 382 auto = 'f' 383 ui = MercurialUI(hgui, hgui.verbose, auto) 384 385 if files: 386 files = frozenset(files) 387 388 def skip(name): 389 return files and name in files 390 391 def prompt(name, func, regions=all_regions): 392 result = ui.prompt("(a)bort, (i)gnore, or (f)ix?", 'aif', 'a') 393 if result == 'a': 394 return True 395 elif result == 'f': 396 func(repo.wjoin(name), regions) 397 398 return False 399 400 modified, added, removed, deleted, unknown, ignore, clean = repo.status() 401 402 whitespace = Whitespace(ui) 403 sorted_includes = SortedIncludes(ui) 404 for fname in added: 405 if skip(fname): 406 continue 407 408 if whitespace.apply(fname, prompt): 409 return True 410 411 if sorted_includes.apply(fname, prompt): 412 return True 413 414 try: 415 wctx = repo.workingctx() 416 except: 417 from mercurial import context 418 wctx = context.workingctx(repo) 419 420 for fname in modified: 421 if skip(fname): 422 continue 423 424 regions = modregions(wctx, fname) 425 426 if whitespace.apply(fname, prompt, regions): 427 return True 428 429 if sorted_includes.apply(fname, prompt, regions): 430 return True 431 432 return False 433 434def do_check_format(hgui, repo, **args): 435 ui = MercurialUI(hgui, hgui.verbose, auto) 436 437 modified, added, removed, deleted, unknown, ignore, clean = repo.status() 438 439 verbose = 0 440 stats = ValidationStats() 441 for f in modified + added: 442 validate(f, stats, verbose, None) 443 444 if stats: 445 stats.dump() 446 result = ui.prompt("invalid formatting\n(i)gnore or (a)bort?", 447 'ai', 'a') 448 if result == 'a': 449 return True 450 451 return False 452 453def check_hook(hooktype): 454 if hooktype not in ('pretxncommit', 'pre-qrefresh'): 455 raise AttributeError, \ 456 "This hook is not meant for %s" % hooktype 457 458def check_style(ui, repo, hooktype, **kwargs): 459 check_hook(hooktype) 460 args = {} 461 462 try: 463 return do_check_style(ui, repo, **args) 464 except Exception, e: 465 import traceback 466 traceback.print_exc() 467 return True 468 469def check_format(ui, repo, hooktype, **kwargs): 470 check_hook(hooktype) 471 args = {} 472 473 try: 474 return do_check_format(ui, repo, **args) 475 except Exception, e: 476 import traceback 477 traceback.print_exc() 478 return True 479 480try: 481 from mercurial.i18n import _ 482except ImportError: 483 def _(arg): 484 return arg 485 486cmdtable = { 487 '^m5style' : 488 ( do_check_style, 489 [ ('a', 'auto', False, _("automatically fix whitespace")) ], 490 _('hg m5style [-a] [FILE]...')), 491 '^m5format' : 492 ( do_check_format, 493 [ ], 494 _('hg m5format [FILE]...')), 495} 496 497if __name__ == '__main__': 498 import getopt 499 500 progname = sys.argv[0] 501 if len(sys.argv) < 2: 502 sys.exit('usage: %s <command> [<command args>]' % progname) 503 504 fixwhite_usage = '%s fixwhite [-t <tabsize> ] <path> [...] \n' % progname 505 chkformat_usage = '%s chkformat <path> [...] \n' % progname 506 chkwhite_usage = '%s chkwhite <path> [...] \n' % progname 507 508 command = sys.argv[1] 509 if command == 'fixwhite': 510 flags = 't:' 511 usage = fixwhite_usage 512 elif command == 'chkwhite': 513 flags = 'nv' 514 usage = chkwhite_usage 515 elif command == 'chkformat': 516 flags = 'nv' 517 usage = chkformat_usage 518 else: 519 sys.exit(fixwhite_usage + chkwhite_usage + chkformat_usage) 520 521 opts, args = getopt.getopt(sys.argv[2:], flags) 522 523 code = 1 524 verbose = 1 525 for opt,arg in opts: 526 if opt == '-n': 527 code = None 528 if opt == '-t': 529 tabsize = int(arg) 530 if opt == '-v': 531 verbose += 1 532 533 if command == 'fixwhite': 534 for filename in args: 535 fixwhite(filename, tabsize) 536 elif command == 'chkwhite': 537 for filename in args: 538 for line,num in checkwhite(filename): 539 print 'invalid whitespace: %s:%d' % (filename, num) 540 if verbose: 541 print '>>%s<<' % line[:-1] 542 elif command == 'chkformat': 543 stats = ValidationStats() 544 for filename in args: 545 validate(filename, stats=stats, verbose=verbose, exit_code=code) 546 547 if verbose > 0: 548 stats.dump() 549 else: 550 sys.exit("command '%s' not found" % command) 551