| #! /usr/bin/env python |
| # Copyright (c) 2006 The Regents of The University of Michigan |
| # Copyright (c) 2007,2011 The Hewlett-Packard Development Company |
| # All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer; |
| # redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution; |
| # neither the name of the copyright holders nor the names of its |
| # contributors may be used to endorse or promote products derived from |
| # this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| # |
| # Authors: Nathan Binkert |
| |
| import heapq |
| import os |
| import re |
| import sys |
| |
| from os.path import dirname, join as joinpath |
| from itertools import count |
| from mercurial import bdiff, mdiff |
| |
| current_dir = dirname(__file__) |
| sys.path.insert(0, current_dir) |
| sys.path.insert(1, joinpath(dirname(current_dir), 'src', 'python')) |
| |
| from m5.util import neg_inf, pos_inf, Region, Regions |
| import sort_includes |
| from file_types import lang_type |
| |
| all_regions = Regions(Region(neg_inf, pos_inf)) |
| |
| tabsize = 8 |
| lead = re.compile(r'^([ \t]+)') |
| trail = re.compile(r'([ \t]+)$') |
| any_control = re.compile(r'\b(if|while|for)[ \t]*[(]') |
| good_control = re.compile(r'\b(if|while|for) [(]') |
| |
| format_types = set(('C', 'C++')) |
| |
| def modified_regions(old_data, new_data): |
| regions = Regions() |
| beg = None |
| for pbeg, pend, fbeg, fend in bdiff.blocks(old_data, new_data): |
| if beg is not None and beg != fbeg: |
| regions.append(beg, fbeg) |
| beg = fend |
| return regions |
| |
| def modregions(wctx, fname): |
| fctx = wctx.filectx(fname) |
| pctx = fctx.parents() |
| |
| file_data = fctx.data() |
| lines = mdiff.splitnewlines(file_data) |
| if len(pctx) in (1, 2): |
| mod_regions = modified_regions(pctx[0].data(), file_data) |
| if len(pctx) == 2: |
| m2 = modified_regions(pctx[1].data(), file_data) |
| # only the lines that are new in both |
| mod_regions &= m2 |
| else: |
| mod_regions = Regions() |
| mod_regions.append(0, len(lines)) |
| |
| return mod_regions |
| |
| class UserInterface(object): |
| def __init__(self, verbose=False, auto=False): |
| self.auto = auto |
| self.verbose = verbose |
| |
| def prompt(self, prompt, results, default): |
| if self.auto: |
| return self.auto |
| |
| while True: |
| result = self.do_prompt(prompt, results, default) |
| if result in results: |
| return result |
| |
| class MercurialUI(UserInterface): |
| def __init__(self, ui, *args, **kwargs): |
| super(MercurialUI, self).__init__(*args, **kwargs) |
| self.ui = ui |
| |
| def do_prompt(self, prompt, results, default): |
| return self.ui.prompt(prompt, default=default) |
| |
| def write(self, string): |
| self.ui.write(string) |
| |
| class StdioUI(UserInterface): |
| def do_prompt(self, prompt, results, default): |
| return raw_input(prompt) or default |
| |
| def write(self, string): |
| sys.stdout.write(string) |
| |
| class Verifier(object): |
| def __init__(self, ui, repo=None): |
| self.ui = ui |
| self.repo = repo |
| if repo is None: |
| self.wctx = None |
| |
| def __getattr__(self, attr): |
| if attr in ('prompt', 'write'): |
| return getattr(self.ui, attr) |
| |
| if attr == 'wctx': |
| try: |
| wctx = repo.workingctx() |
| except: |
| from mercurial import context |
| wctx = context.workingctx(repo) |
| self.wctx = wctx |
| return wctx |
| |
| raise AttributeError |
| |
| def open(self, filename, mode): |
| if self.repo: |
| filename = self.repo.wjoin(filename) |
| |
| try: |
| f = file(filename, mode) |
| except OSError, msg: |
| print 'could not open file %s: %s' % (filename, msg) |
| return None |
| |
| return f |
| |
| def skip(self, filename): |
| return lang_type(filename) not in self.languages |
| |
| def check(self, filename, regions=all_regions): |
| f = self.open(filename, 'r') |
| |
| errors = 0 |
| for num,line in enumerate(f): |
| if num not in regions: |
| continue |
| if not self.check_line(line): |
| self.write("invalid %s in %s:%d\n" % \ |
| (self.test_name, filename, num + 1)) |
| if self.ui.verbose: |
| self.write(">>%s<<\n" % line[-1]) |
| errors += 1 |
| return errors |
| |
| def fix(self, filename, regions=all_regions): |
| f = self.open(filename, 'r+') |
| |
| lines = list(f) |
| |
| f.seek(0) |
| f.truncate() |
| |
| for i,line in enumerate(lines): |
| if i in regions: |
| line = self.fix_line(line) |
| |
| f.write(line) |
| f.close() |
| |
| def apply(self, filename, prompt, regions=all_regions): |
| if not self.skip(filename): |
| errors = self.check(filename, regions) |
| if errors: |
| if prompt(filename, self.fix, regions): |
| return True |
| return False |
| |
| |
| class Whitespace(Verifier): |
| languages = set(('C', 'C++', 'swig', 'python', 'asm', 'isa', 'scons')) |
| test_name = 'whitespace' |
| def check_line(self, line): |
| match = lead.search(line) |
| if match and match.group(1).find('\t') != -1: |
| return False |
| |
| match = trail.search(line) |
| if match: |
| return False |
| |
| return True |
| |
| def fix_line(self, line): |
| if lead.search(line): |
| newline = '' |
| for i,c in enumerate(line): |
| if c == ' ': |
| newline += ' ' |
| elif c == '\t': |
| newline += ' ' * (tabsize - len(newline) % tabsize) |
| else: |
| newline += line[i:] |
| break |
| |
| line = newline |
| |
| return line.rstrip() + '\n' |
| |
| class SortedIncludes(Verifier): |
| languages = sort_includes.default_languages |
| def __init__(self, *args, **kwargs): |
| super(SortedIncludes, self).__init__(*args, **kwargs) |
| self.sort_includes = sort_includes.SortIncludes() |
| |
| def check(self, filename, regions=all_regions): |
| f = self.open(filename, 'r') |
| |
| lines = [ l.rstrip('\n') for l in f.xreadlines() ] |
| old = ''.join(line + '\n' for line in lines) |
| f.close() |
| |
| if len(lines) == 0: |
| return 0 |
| |
| language = lang_type(filename, lines[0]) |
| sort_lines = list(self.sort_includes(lines, filename, language)) |
| new = ''.join(line + '\n' for line in sort_lines) |
| |
| mod = modified_regions(old, new) |
| modified = mod & regions |
| |
| if modified: |
| self.write("invalid sorting of includes in %s\n" % (filename)) |
| if self.ui.verbose: |
| for start, end in modified.regions: |
| self.write("bad region [%d, %d)\n" % (start, end)) |
| return 1 |
| |
| return 0 |
| |
| def fix(self, filename, regions=all_regions): |
| f = self.open(filename, 'r+') |
| |
| old = f.readlines() |
| lines = [ l.rstrip('\n') for l in old ] |
| language = lang_type(filename, lines[0]) |
| sort_lines = list(self.sort_includes(lines, filename, language)) |
| new = ''.join(line + '\n' for line in sort_lines) |
| |
| f.seek(0) |
| f.truncate() |
| |
| for i,line in enumerate(sort_lines): |
| f.write(line) |
| f.write('\n') |
| f.close() |
| |
| def linelen(line): |
| tabs = line.count('\t') |
| if not tabs: |
| return len(line) |
| |
| count = 0 |
| for c in line: |
| if c == '\t': |
| count += tabsize - count % tabsize |
| else: |
| count += 1 |
| |
| return count |
| |
| class ValidationStats(object): |
| def __init__(self): |
| self.toolong = 0 |
| self.toolong80 = 0 |
| self.leadtabs = 0 |
| self.trailwhite = 0 |
| self.badcontrol = 0 |
| self.cret = 0 |
| |
| def dump(self): |
| print '''\ |
| %d violations of lines over 79 chars. %d of which are 80 chars exactly. |
| %d cases of whitespace at the end of a line. |
| %d cases of tabs to indent. |
| %d bad parens after if/while/for. |
| %d carriage returns found. |
| ''' % (self.toolong, self.toolong80, self.trailwhite, self.leadtabs, |
| self.badcontrol, self.cret) |
| |
| def __nonzero__(self): |
| return self.toolong or self.toolong80 or self.leadtabs or \ |
| self.trailwhite or self.badcontrol or self.cret |
| |
| def validate(filename, stats, verbose, exit_code): |
| if lang_type(filename) not in format_types: |
| return |
| |
| def msg(lineno, line, message): |
| print '%s:%d>' % (filename, lineno + 1), message |
| if verbose > 2: |
| print line |
| |
| def bad(): |
| if exit_code is not None: |
| sys.exit(exit_code) |
| |
| try: |
| f = file(filename, 'r') |
| except OSError: |
| if verbose > 0: |
| print 'could not open file %s' % filename |
| bad() |
| return |
| |
| for i,line in enumerate(f): |
| line = line.rstrip('\n') |
| |
| # no carriage returns |
| if line.find('\r') != -1: |
| self.cret += 1 |
| if verbose > 1: |
| msg(i, line, 'carriage return found') |
| bad() |
| |
| # lines max out at 79 chars |
| llen = linelen(line) |
| if llen > 79: |
| stats.toolong += 1 |
| if llen == 80: |
| stats.toolong80 += 1 |
| if verbose > 1: |
| msg(i, line, 'line too long (%d chars)' % llen) |
| bad() |
| |
| # no tabs used to indent |
| match = lead.search(line) |
| if match and match.group(1).find('\t') != -1: |
| stats.leadtabs += 1 |
| if verbose > 1: |
| msg(i, line, 'using tabs to indent') |
| bad() |
| |
| # no trailing whitespace |
| if trail.search(line): |
| stats.trailwhite +=1 |
| if verbose > 1: |
| msg(i, line, 'trailing whitespace') |
| bad() |
| |
| # for c++, exactly one space betwen if/while/for and ( |
| if cpp: |
| match = any_control.search(line) |
| if match and not good_control.search(line): |
| stats.badcontrol += 1 |
| if verbose > 1: |
| msg(i, line, 'improper spacing after %s' % match.group(1)) |
| bad() |
| |
| def do_check_style(hgui, repo, *files, **args): |
| """check files for proper m5 style guidelines""" |
| from mercurial import mdiff, util |
| |
| auto = args.get('auto', False) |
| if auto: |
| auto = 'f' |
| ui = MercurialUI(hgui, hgui.verbose, auto) |
| |
| if files: |
| files = frozenset(files) |
| |
| def skip(name): |
| return files and name in files |
| |
| def prompt(name, func, regions=all_regions): |
| result = ui.prompt("(a)bort, (i)gnore, or (f)ix?", 'aif', 'a') |
| if result == 'a': |
| return True |
| elif result == 'f': |
| func(repo.wjoin(name), regions) |
| |
| return False |
| |
| modified, added, removed, deleted, unknown, ignore, clean = repo.status() |
| |
| whitespace = Whitespace(ui) |
| sorted_includes = SortedIncludes(ui) |
| for fname in added: |
| if skip(fname): |
| continue |
| |
| fpath = joinpath(repo.root, fname) |
| |
| if whitespace.apply(fpath, prompt): |
| return True |
| |
| if sorted_includes.apply(fpath, prompt): |
| return True |
| |
| try: |
| wctx = repo.workingctx() |
| except: |
| from mercurial import context |
| wctx = context.workingctx(repo) |
| |
| for fname in modified: |
| if skip(fname): |
| continue |
| |
| fpath = joinpath(repo.root, fname) |
| regions = modregions(wctx, fname) |
| |
| if whitespace.apply(fpath, prompt, regions): |
| return True |
| |
| if sorted_includes.apply(fpath, prompt, regions): |
| return True |
| |
| return False |
| |
| def do_check_format(hgui, repo, **args): |
| ui = MercurialUI(hgui, hgui.verbose, auto) |
| |
| modified, added, removed, deleted, unknown, ignore, clean = repo.status() |
| |
| verbose = 0 |
| stats = ValidationStats() |
| for f in modified + added: |
| validate(joinpath(repo.root, f), stats, verbose, None) |
| |
| if stats: |
| stats.dump() |
| result = ui.prompt("invalid formatting\n(i)gnore or (a)bort?", |
| 'ai', 'a') |
| if result == 'a': |
| return True |
| |
| return False |
| |
| def check_hook(hooktype): |
| if hooktype not in ('pretxncommit', 'pre-qrefresh'): |
| raise AttributeError, \ |
| "This hook is not meant for %s" % hooktype |
| |
| def check_style(ui, repo, hooktype, **kwargs): |
| check_hook(hooktype) |
| args = {} |
| |
| try: |
| return do_check_style(ui, repo, **args) |
| except Exception, e: |
| import traceback |
| traceback.print_exc() |
| return True |
| |
| def check_format(ui, repo, hooktype, **kwargs): |
| check_hook(hooktype) |
| args = {} |
| |
| try: |
| return do_check_format(ui, repo, **args) |
| except Exception, e: |
| import traceback |
| traceback.print_exc() |
| return True |
| |
| try: |
| from mercurial.i18n import _ |
| except ImportError: |
| def _(arg): |
| return arg |
| |
| cmdtable = { |
| '^m5style' : |
| ( do_check_style, |
| [ ('a', 'auto', False, _("automatically fix whitespace")) ], |
| _('hg m5style [-a] [FILE]...')), |
| '^m5format' : |
| ( do_check_format, |
| [ ], |
| _('hg m5format [FILE]...')), |
| } |
| |
| if __name__ == '__main__': |
| import getopt |
| |
| progname = sys.argv[0] |
| if len(sys.argv) < 2: |
| sys.exit('usage: %s <command> [<command args>]' % progname) |
| |
| fixwhite_usage = '%s fixwhite [-t <tabsize> ] <path> [...] \n' % progname |
| chkformat_usage = '%s chkformat <path> [...] \n' % progname |
| chkwhite_usage = '%s chkwhite <path> [...] \n' % progname |
| |
| command = sys.argv[1] |
| if command == 'fixwhite': |
| flags = 't:' |
| usage = fixwhite_usage |
| elif command == 'chkwhite': |
| flags = 'nv' |
| usage = chkwhite_usage |
| elif command == 'chkformat': |
| flags = 'nv' |
| usage = chkformat_usage |
| else: |
| sys.exit(fixwhite_usage + chkwhite_usage + chkformat_usage) |
| |
| opts, args = getopt.getopt(sys.argv[2:], flags) |
| |
| code = 1 |
| verbose = 1 |
| for opt,arg in opts: |
| if opt == '-n': |
| code = None |
| if opt == '-t': |
| tabsize = int(arg) |
| if opt == '-v': |
| verbose += 1 |
| |
| if command == 'fixwhite': |
| for filename in args: |
| fixwhite(filename, tabsize) |
| elif command == 'chkwhite': |
| for filename in args: |
| for line,num in checkwhite(filename): |
| print 'invalid whitespace: %s:%d' % (filename, num) |
| if verbose: |
| print '>>%s<<' % line[:-1] |
| elif command == 'chkformat': |
| stats = ValidationStats() |
| for filename in args: |
| validate(filename, stats=stats, verbose=verbose, exit_code=code) |
| |
| if verbose > 0: |
| stats.dump() |
| else: |
| sys.exit("command '%s' not found" % command) |