xref: /freebsd/crypto/krb5/src/util/cstyle.py (revision 7f2fe78b9dd5f51c821d771b63d2e096f6fd49e9)
1# Copyright (C) 2012 by the Massachusetts Institute of Technology.
2# All rights reserved.
3#
4# Export of this software from the United States of America may
5#   require a specific license from the United States Government.
6#   It is the responsibility of any person or organization contemplating
7#   export to obtain such a license before exporting.
8#
9# WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
10# distribute this software and its documentation for any purpose and
11# without fee is hereby granted, provided that the above copyright
12# notice appear in all copies and that both that copyright notice and
13# this permission notice appear in supporting documentation, and that
14# the name of M.I.T. not be used in advertising or publicity pertaining
15# to distribution of the software without specific, written prior
16# permission.  Furthermore if you modify this software you must label
17# your software as modified software and not distribute it in such a
18# fashion that it might be confused with the original M.I.T. software.
19# M.I.T. makes no representations about the suitability of
20# this software for any purpose.  It is provided "as is" without express
21# or implied warranty.
22
23# This program attempts to detect MIT krb5 coding style violations
24# attributable to the changes a series of git commits.  It can be run
25# from anywhere within a git working tree.
26
27import getopt
28import os
29import re
30import sys
31from subprocess import Popen, PIPE, call
32
33def usage():
34    u = ['Usage: cstyle [-w] [rev|rev1..rev2]',
35         '',
36         'By default, checks working tree against HEAD, or checks changes in',
37         'HEAD if the working tree is clean.  With a revision option, checks',
38         'changes in rev or the series rev1..rev2.  With the -w option,',
39         'checks working tree against rev (defaults to HEAD).']
40    sys.stderr.write('\n'.join(u) + '\n')
41    sys.exit(1)
42
43
44# Run a command and return a list of its output lines.
45def run(args):
46    # subprocess.check_output would be ideal here, but requires Python 2.7.
47    p = Popen(args, stdout=PIPE, stderr=PIPE, universal_newlines=True)
48    out, err = p.communicate()
49    if p.returncode != 0:
50        sys.stderr.write('Failed command: ' + ' '.join(args) + '\n')
51        if err != '':
52            sys.stderr.write('stderr:\n' + err)
53        sys.stderr.write('Unexpected command failure, exiting\n')
54        sys.exit(1)
55    return out.splitlines()
56
57
58# Find the top level of the git working tree, or None if we're not in
59# one.
60def find_toplevel():
61    # git doesn't seem to have a way to do this, so we search by hand.
62    dir = os.getcwd()
63    while True:
64        if os.path.exists(os.path.join(dir, '.git')):
65            break
66        parent = os.path.dirname(dir)
67        if (parent == dir):
68            return None
69        dir = parent
70    return dir
71
72
73# Check for style issues in a file within rev (or within the current
74# checkout if rev is None).  Report only problems on line numbers in
75# new_lines.
76line_re = re.compile(r'^\s*(\d+)  (.*)$')
77def check_file(filename, rev, new_lines):
78    # Process only C source files under src.
79    root, ext = os.path.splitext(filename)
80    if not filename.startswith('src/') or ext not in ('.c', '.h', '.hin'):
81        return
82    dispname = filename[4:]
83
84    if rev is None:
85        p1 = Popen(['cat', filename], stdout=PIPE)
86    else:
87        p1 = Popen(['git', 'show', rev + ':' + filename], stdout=PIPE)
88    p2 = Popen([sys.executable, 'src/util/cstyle-file.py'], stdin=p1.stdout,
89               stdout=PIPE, universal_newlines=True)
90    p1.stdout.close()
91    out, err = p2.communicate()
92    if p2.returncode != 0:
93        sys.exit(1)
94
95    first = True
96    for line in out.splitlines():
97        m = line_re.match(line)
98        if int(m.group(1)) in new_lines:
99            if first:
100                print('  ' + dispname + ':')
101                first = False
102            print('    ' + line)
103
104
105# Determine the lines of each file modified by diff (a sequence of
106# strings) and check for style violations in those lines.  rev
107# indicates the version in which the new contents of each file can be
108# found, or is None if the current contents are in the working copy.
109chunk_header_re = re.compile(r'^@@ -\d+(,(\d+))? \+(\d+)(,(\d+))? @@')
110def check_diff(diff, rev):
111    old_count, new_count, lineno = 0, 0, 0
112    filename = None
113    for line in diff:
114        if not line or line.startswith('\\ No newline'):
115            continue
116        if old_count > 0 or new_count > 0:
117            # We're in a chunk.
118            if line[0] == '+':
119                new_lines.append(lineno)
120            if line[0] in ('+', ' '):
121                new_count = new_count - 1
122                lineno = lineno + 1
123            if line[0] in ('-', ' '):
124                old_count = old_count - 1
125        elif line.startswith('+++ b/'):
126            # We're starting a new file.  Check the last one.
127            if filename:
128                check_file(filename, rev, new_lines)
129            filename = line[6:]
130            new_lines = []
131        else:
132            m = chunk_header_re.match(line)
133            if m:
134                old_count = int(m.group(2) or '1')
135                lineno = int(m.group(3))
136                new_count = int(m.group(5) or '1')
137
138    # Check the last file in the diff.
139    if filename:
140        check_file(filename, rev, new_lines)
141
142
143# Check a sequence of revisions for style issues.
144def check_series(revlist):
145    for rev in revlist:
146        sys.stdout.flush()
147        call(['git', 'show', '-s', '--oneline', rev])
148        diff = run(['git', 'diff-tree', '--no-commit-id', '--root', '-M',
149                    '--cc', rev])
150        check_diff(diff, rev)
151
152
153# Parse arguments.
154try:
155    opts, args = getopt.getopt(sys.argv[1:], 'w')
156except getopt.GetoptError as err:
157    print(str(err))
158    usage()
159if len(args) > 1:
160    usage()
161
162# Change to the top level of the working tree so we easily run the file
163# checker and refer to working tree files.
164toplevel = find_toplevel()
165if toplevel is None:
166    sys.stderr.write('%s must be run within a git working tree')
167os.chdir(toplevel)
168
169if ('-w', '') in opts:
170    # Check the working tree against a base revision.
171    arg = 'HEAD'
172    if args:
173        arg = args[0]
174    check_diff(run(['git', 'diff', arg]), None)
175elif args:
176    # Check the differences in a rev or a series of revs.
177    if '..' in args[0]:
178        check_series(run(['git', 'rev-list', '--reverse', args[0]]))
179    else:
180        check_series([args[0]])
181else:
182    # No options or arguments.  Check the differences against HEAD, or
183    # the differences in HEAD if the working tree is clean.
184    diff = run(['git', 'diff', 'HEAD'])
185    if diff:
186        check_diff(diff, None)
187    else:
188        check_series(['HEAD'])
189