util: Enable DRAM sweep to print power and efficiency
[gem5.git] / util / style.py
index 4615739411c1161701e3d2e7b87a08bcfdcb57e0..bf73bc425936519b5c431a40ab486ed31159e2bd 100644 (file)
@@ -1,6 +1,18 @@
 #! /usr/bin/env python
+# Copyright (c) 2014 ARM Limited
+# All rights reserved
+#
+# The license below extends only to copyright in the software and shall
+# not be construed as granting a license to any other intellectual
+# property including but not limited to intellectual property relating
+# to a hardware implementation of the functionality of the software
+# licensed hereunder.  You may use the software subject to the license
+# terms below provided that you ensure that this notice is replicated
+# unmodified and in its entirety in all distributions of the software,
+# modified or unmodified, in source code or in binary form.
+#
 # Copyright (c) 2006 The Regents of The University of Michigan
-# Copyright (c) 2007 The Hewlett-Packard Development Company
+# Copyright (c) 2007,2011 The Hewlett-Packard Development Company
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
 #
 # Authors: Nathan Binkert
 
-import re
+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, commands
+
+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) [(]')
 
-lang_types = { 'c'   : "C",
-               'h'   : "C",
-               'cc'  : "C++",
-               'hh'  : "C++",
-               'cxx' : "C++",
-               'hxx' : "C++",
-               'cpp' : "C++",
-               'hpp' : "C++",
-               'C'   : "C++",
-               'H'   : "C++",
-               'i'   : "swig",
-               'py'  : "python",
-               's'   : "asm",
-               'S'   : "asm",
-               'isa' : "isa" }
-def file_type(filename):
-    extension = filename.split('.')
-    extension = len(extension) > 1 and extension[-1]
-    return lang_types.get(extension, None)
-
-whitespace_types = ('C', 'C++', 'swig', 'python', 'asm', 'isa')
-def whitespace_file(filename):
-    if file_type(filename) in whitespace_types:
-        return True
+format_types = set(('C', 'C++'))
 
-    if filename.startswith("SCons"):
-        return True
 
-    return False
+def re_ignore(expr):
+    """Helper function to create regular expression ignore file
+    matcher functions"""
 
-format_types = ( 'C', 'C++' )
-def format_file(filename):
-    if file_type(filename) in format_types:
-        return True
+    rex = re.compile(expr)
+    def match_re(fname):
+        return rex.match(fname)
+    return match_re
+
+# This list contains a list of functions that are called to determine
+# if a file should be excluded from the style matching rules or
+# not. The functions are called with the file name relative to the
+# repository root (without a leading slash) as their argument. A file
+# is excluded if any function in the list returns true.
+style_ignores = [
+    # Ignore external projects as they are unlikely to follow the gem5
+    # coding convention.
+    re_ignore("^ext/"),
+]
+
+def check_ignores(fname):
+    """Check if a file name matches any of the ignore rules"""
+
+    for rule in style_ignores:
+        if rule(fname):
+            return True
 
     return False
 
-def checkwhite_line(line):
-    match = lead.search(line)
-    if match and match.group(1).find('\t') != -1:
-        return False
 
-    match = trail.search(line)
-    if match:
+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):
+        self.verbose = verbose
+
+    def prompt(self, prompt, results, default):
+        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):
+        self.ui = ui
+        self.repo = repo
+
+    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):
+        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):
+        filename = self.repo.wjoin(filename)
+
+        # We never want to handle symlinks, so always skip them: If the location
+        # pointed to is a directory, skip it. If the location is a file inside
+        # the gem5 directory, it will be checked as a file, so symlink can be
+        # skipped. If the location is a file outside gem5, we don't want to
+        # check it anyway.
+        if os.path.islink(filename):
+            return True
+        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
 
-    return True
 
-def checkwhite(filename):
-    if not whitespace_file(filename):
-        return
+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
 
-    try:
-        f = file(filename, 'r+')
-    except OSError, msg:
-        print 'could not open file %s: %s' % (filename, msg)
-        return
+        match = trail.search(line)
+        if match:
+            return False
 
-    for num,line in enumerate(f):
-        if not checkwhite_line(line):
-            yield line,num + 1
+        return True
 
-def fixwhite_line(line, tabsize):
-    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
+    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
+            line = newline
 
-    return line.rstrip() + '\n'
+        return line.rstrip() + '\n'
 
-def fixwhite(filename, tabsize, fixonly=None):
-    if not whitespace_file(filename):
-        return
+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()
 
-    try:
-        f = file(filename, 'r+')
-    except OSError, msg:
-        print 'could not open file %s: %s' % (filename, msg)
-        return
+    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
 
-    lines = list(f)
+        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
 
-    f.seek(0)
-    f.truncate()
+        return 0
 
-    for i,line in enumerate(lines):
-        if fixonly is None or i in fixonly:
-            line = fixwhite_line(line, tabsize)
+    def fix(self, filename, regions=all_regions):
+        f = self.open(filename, 'r+')
 
-        print >>f, line,
+        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')
@@ -174,7 +352,8 @@ class ValidationStats(object):
                self.trailwhite or self.badcontrol or self.cret
 
 def validate(filename, stats, verbose, exit_code):
-    if not format_file(filename):
+    lang = lang_type(filename)
+    if lang not in format_types:
         return
 
     def msg(lineno, line, message):
@@ -186,13 +365,6 @@ def validate(filename, stats, verbose, exit_code):
         if exit_code is not None:
             sys.exit(exit_code)
 
-    cpp = filename.endswith('.cc') or filename.endswith('.hh')
-    py = filename.endswith('.py')
-
-    if py + cpp != 1:
-        raise AttributeError, \
-              "I don't know how to deal with the file %s" % filename
-
     try:
         f = file(filename, 'r')
     except OSError:
@@ -237,7 +409,7 @@ def validate(filename, stats, verbose, exit_code):
             bad()
 
         # for c++, exactly one space betwen if/while/for and (
-        if cpp:
+        if lang == 'C++':
             match = any_control.search(line)
             if match and not good_control.search(line):
                 stats.badcontrol += 1
@@ -245,140 +417,146 @@ def validate(filename, stats, verbose, exit_code):
                     msg(i, line, 'improper spacing after %s' % match.group(1))
                 bad()
 
-def modified_lines(old_data, new_data, max_lines):
-    from itertools import count
-    from mercurial import bdiff, mdiff
 
-    modified = set()
-    counter = count()
-    for pbeg, pend, fbeg, fend in bdiff.blocks(old_data, new_data):
-        for i in counter:
-            if i < fbeg:
-                modified.add(i)
-            elif i + 1 >= fend:
-                break
-            elif i > max_lines:
-                break
-    return modified
-
-def do_check_whitespace(ui, repo, *files, **args):
-    """check files for proper m5 style guidelines"""
-    from mercurial import mdiff, util
-
-    if files:
-        files = frozenset(files)
-
-    def skip(name):
-        return files and name in files
-
-    def prompt(name, fixonly=None):
-        if args.get('auto', False):
-            result = 'f'
-        else:
-            result = ui.prompt("(a)bort, (i)gnore, or (f)ix?", "^[aif]$", "a")
+def _modified_regions(repo, patterns, **kwargs):
+    opt_all = kwargs.get('all', False)
+    opt_no_ignore = kwargs.get('no_ignore', False)
+
+    # Import the match (repository file name matching helper)
+    # function. Different versions of Mercurial keep it in different
+    # modules and implement them differently.
+    try:
+        from mercurial import scmutil
+        m = scmutil.match(repo[None], patterns, kwargs)
+    except ImportError:
+        from mercurial import cmdutil
+        m = cmdutil.match(repo, patterns, kwargs)
+
+    modified, added, removed, deleted, unknown, ignore, clean = \
+        repo.status(match=m, clean=opt_all)
+
+    if not opt_all:
+        try:
+            wctx = repo.workingctx()
+        except:
+            from mercurial import context
+            wctx = context.workingctx(repo)
+
+        files = [ (fn, all_regions) for fn in added ] + \
+            [ (fn,  modregions(wctx, fn)) for fn in modified ]
+    else:
+        files = [ (fn, all_regions) for fn in added + modified + clean ]
+
+    for fname, mod_regions in files:
+        if opt_no_ignore or not check_ignores(fname):
+            yield fname, mod_regions
+
+
+def do_check_style(hgui, repo, *pats, **opts):
+    """check files for proper m5 style guidelines
+
+    Without an argument, checks all modified and added files for gem5
+    coding style violations. A list of files can be specified to limit
+    the checker to a subset of the repository. The style rules are
+    normally applied on a diff of the repository state (i.e., added
+    files are checked in their entirety while only modifications of
+    modified files are checked).
+
+    The --all option can be specified to include clean files and check
+    modified files in their entirety.
+    """
+    opt_fix_all = opts.get('fix_all', False)
+    if not opt_fix_all:
+        opt_fix_white = opts.get('fix_white', False)
+        opt_fix_include = opts.get('fix_include', False)
+    else:
+        opt_fix_white = True
+        opt_fix_include = True
+
+    ui = MercurialUI(hgui, verbose=hgui.verbose)
+
+    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 == 'i':
-            pass
         elif result == 'f':
-            fixwhite(repo.wjoin(name), args['tabsize'], fixonly)
-        else:
-            raise util.Abort(_("Invalid response: '%s'") % result)
+            func(name, regions)
 
         return False
 
-    modified, added, removed, deleted, unknown, ignore, clean = repo.status()
+    def no_prompt(name, func, regions=all_regions):
+        func(name, regions)
+        return False
 
-    for fname in added:
-        if skip(fname):
-            continue
+    prompt_white = prompt if not opt_fix_white else no_prompt
+    prompt_include = prompt if not opt_fix_include else no_prompt
 
-        ok = True
-        for line,num in checkwhite(repo.wjoin(fname)):
-            ui.write("invalid whitespace in %s:%d\n" % (fname, num))
-            if ui.verbose:
-                ui.write(">>%s<<\n" % line[-1])
-            ok = False
+    whitespace = Whitespace(ui, repo)
+    sorted_includes = SortedIncludes(ui, repo)
+    for fname, mod_regions in _modified_regions(repo, pats, **opts):
+        if whitespace.apply(fname, prompt_white, mod_regions):
+            return True
 
-        if not ok:
-            if prompt(fname):
-                return True
+        if sorted_includes.apply(fname, prompt_include, mod_regions):
+            return True
 
-    try:
-        wctx = repo.workingctx()
-    except:
-        from mercurial import context
-        wctx = context.workingctx(repo)
-
-    for fname in modified:
-        if skip(fname):
-            continue
-
-        if not whitespace_file(fname):
-            continue
-
-        fctx = wctx.filectx(fname)
-        pctx = fctx.parents()
-
-        file_data = fctx.data()
-        lines = mdiff.splitnewlines(file_data)
-        if len(pctx) in (1, 2):
-            mod_lines = modified_lines(pctx[0].data(), file_data, len(lines))
-            if len(pctx) == 2:
-                m2 = modified_lines(pctx[1].data(), file_data, len(lines))
-                mod_lines = mod_lines & m2 # only the lines that are new in both
-        else:
-            mod_lines = xrange(0, len(lines))
+    return False
 
-        fixonly = set()
-        for i,line in enumerate(lines):
-            if i not in mod_lines:
-                continue
+def do_check_format(hgui, repo, *pats, **opts):
+    """check files for gem5 code formatting violations
 
-            if checkwhite_line(line):
-                continue
+    Without an argument, checks all modified and added files for gem5
+    code formatting violations. A list of files can be specified to
+    limit the checker to a subset of the repository. The style rules
+    are normally applied on a diff of the repository state (i.e.,
+    added files are checked in their entirety while only modifications
+    of modified files are checked).
 
-            ui.write("invalid whitespace: %s:%d\n" % (fname, i+1))
-            if ui.verbose:
-                ui.write(">>%s<<\n" % line[:-1])
-            fixonly.add(i)
+    The --all option can be specified to include clean files and check
+    modified files in their entirety.
+    """
+    ui = MercurialUI(hgui, hgui.verbose)
 
-        if fixonly:
-            if prompt(fname, fixonly):
+    verbose = 0
+    for fname, mod_regions in _modified_regions(repo, pats, **opts):
+        stats = ValidationStats()
+        validate(joinpath(repo.root, fname), stats, verbose, None)
+        if stats:
+            print "%s:" % fname
+            stats.dump()
+            result = ui.prompt("invalid formatting\n(i)gnore or (a)bort?",
+                               'ai', 'a')
+            if result == 'a':
                 return True
 
-def check_whitespace(ui, repo, hooktype, node, parent1, parent2, **kwargs):
-    if hooktype != 'pretxncommit':
-        raise AttributeError, \
-              "This hook is only meant for pretxncommit, not %s" % hooktype
-
-    args = { 'tabsize' : 8 }
-    do_check_whitespace(ui, repo, **args)
+    return False
 
-def check_format(ui, repo, hooktype, node, parent1, parent2, **kwargs):
-    if hooktype != 'pretxncommit':
+def check_hook(hooktype):
+    if hooktype not in ('pretxncommit', 'pre-qrefresh'):
         raise AttributeError, \
-              "This hook is only meant for pretxncommit, not %s" % hooktype
+              "This hook is not meant for %s" % hooktype
 
-    modified, added, removed, deleted, unknown, ignore, clean = repo.status()
+def check_style(ui, repo, hooktype, **kwargs):
+    check_hook(hooktype)
+    args = {}
 
-    verbose = 0
-    stats = ValidationStats()
-    for f in modified + added:
-        validate(f, stats, verbose, None)
-
-    if stats:
-        stats.dump()
-        result = ui.prompt("invalid formatting\n(i)gnore or (a)bort?",
-                           "^[ia]$", "a")
-        if result.startswith('i'):
-            pass
-        elif result.startswith('a'):
-            return True
-        else:
-            raise util.Abort(_("Invalid response: '%s'") % result)
+    try:
+        return do_check_style(ui, repo, **args)
+    except Exception, e:
+        import traceback
+        traceback.print_exc()
+        return True
 
-    return False
+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 _
@@ -386,13 +564,26 @@ except ImportError:
     def _(arg):
         return arg
 
+_common_region_options = [
+    ('a', 'all', False,
+     _("include clean files and unmodified parts of modified files")),
+    ('', 'no-ignore', False, _("ignore the style ignore list")),
+    ]
+
 cmdtable = {
-    '^m5style' :
-    ( do_check_whitespace,
-      [ ('a', 'auto', False, _("automatically fix whitespace")),
-        ('t', 'tabsize', 8, _("Number of spaces TAB indents")) ],
-      _('hg m5check [-t <tabsize>] [FILE]...')),
+    '^m5style' : (
+        do_check_style, [
+            ('f', 'fix-all', False, _("automatically fix style issues")),
+            ('', 'fix-white', False, _("automatically fix white space issues")),
+            ('', 'fix-include', False, _("automatically fix include ordering")),
+            ] + _common_region_options +  commands.walkopts,
+        _('hg m5style [-a] [FILE]...')),
+    '^m5format' :
+    ( do_check_format, [
+            ] + _common_region_options + commands.walkopts,
+      _('hg m5format [FILE]...')),
 }
+
 if __name__ == '__main__':
     import getopt
 
@@ -421,7 +612,6 @@ if __name__ == '__main__':
 
     code = 1
     verbose = 1
-    tabsize = 8
     for opt,arg in opts:
         if opt == '-n':
             code = None