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