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