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