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