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