2 # Copyright (c) 2007 The Regents of The University of Michigan
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are
7 # met: redistributions of source code must retain the above copyright
8 # notice, this list of conditions and the following disclaimer;
9 # redistributions in binary form must reproduce the above copyright
10 # notice, this list of conditions and the following disclaimer in the
11 # documentation and/or other materials provided with the distribution;
12 # neither the name of the copyright holders nor the names of its
13 # contributors may be used to endorse or promote products derived from
14 # this software without specific prior written permission.
16 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 # Authors: Nathan Binkert
34 lead
= re
.compile(r
'^([ \t]+)')
35 trail
= re
.compile(r
'([ \t]+)$')
36 any_control
= re
.compile(r
'\b(if|while|for)[ \t]*[(]')
37 good_control
= re
.compile(r
'\b(if|while|for) [(]')
39 lang_types
= { 'c' : "C",
54 def file_type(filename
):
55 extension
= filename
.split('.')
56 extension
= len(extension
) > 1 and extension
[-1]
57 return lang_types
.get(extension
, None)
59 whitespace_types
= ('C', 'C++', 'swig', 'python', 'asm', 'isa')
60 def whitespace_file(filename
):
61 if file_type(filename
) in whitespace_types
:
64 if filename
.startswith("SCons"):
69 format_types
= ( 'C', 'C++' )
70 def format_file(filename
):
71 if file_type(filename
) in format_types
:
76 def checkwhite_line(line
):
77 match
= lead
.search(line
)
78 if match
and match
.group(1).find('\t') != -1:
81 match
= trail
.search(line
)
87 def checkwhite(filename
):
88 if not whitespace_file(filename
):
92 f
= file(filename
, 'r+')
94 print 'could not open file %s: %s' % (filename
, msg
)
97 for num
,line
in enumerate(f
):
98 if not checkwhite_line(line
):
101 def fixwhite_line(line
, tabsize
):
102 if lead
.search(line
):
104 for i
,c
in enumerate(line
):
108 newline
+= ' ' * (tabsize
- len(newline
) % tabsize
)
115 return line
.rstrip() + '\n'
117 def fixwhite(filename
, tabsize
, fixonly
=None):
118 if not whitespace_file(filename
):
122 f
= file(filename
, 'r+')
124 print 'could not open file %s: %s' % (filename
, msg
)
132 for i
,line
in enumerate(lines
):
133 if fixonly
is None or i
in fixonly
:
134 line
= fixwhite_line(line
, tabsize
)
139 tabs
= line
.count('\t')
146 count
+= tabsize
- count
% tabsize
152 class ValidationStats(object):
163 %d violations of lines over 79 chars. %d of which are 80 chars exactly.
164 %d cases of whitespace at the end of a line.
165 %d cases of tabs to indent.
166 %d bad parens after if/while/for.
167 %d carriage returns found.
168 ''' % (self
.toolong
, self
.toolong80
, self
.trailwhite
, self
.leadtabs
,
169 self
.badcontrol
, self
.cret
)
171 def __nonzero__(self
):
172 return self
.toolong
or self
.toolong80
or self
.leadtabs
or \
173 self
.trailwhite
or self
.badcontrol
or self
.cret
175 def validate(filename
, stats
, verbose
, exit_code
):
176 if not format_file(filename
):
179 def msg(lineno
, line
, message
):
180 print '%s:%d>' % (filename
, lineno
+ 1), message
185 if exit_code
is not None:
188 cpp
= filename
.endswith('.cc') or filename
.endswith('.hh')
189 py
= filename
.endswith('.py')
192 raise AttributeError, \
193 "I don't know how to deal with the file %s" % filename
196 f
= file(filename
, 'r')
199 print 'could not open file %s' % filename
203 for i
,line
in enumerate(f
):
204 line
= line
.rstrip('\n')
206 # no carriage returns
207 if line
.find('\r') != -1:
210 msg(i
, line
, 'carriage return found')
213 # lines max out at 79 chars
220 msg(i
, line
, 'line too long (%d chars)' % llen
)
223 # no tabs used to indent
224 match
= lead
.search(line
)
225 if match
and match
.group(1).find('\t') != -1:
228 msg(i
, line
, 'using tabs to indent')
231 # no trailing whitespace
232 if trail
.search(line
):
235 msg(i
, line
, 'trailing whitespace')
238 # for c++, exactly one space betwen if/while/for and (
240 match
= any_control
.search(line
)
241 if match
and not good_control
.search(line
):
242 stats
.badcontrol
+= 1
244 msg(i
, line
, 'improper spacing after %s' % match
.group(1))
247 def modified_lines(old_data
, new_data
, max_lines
):
248 from itertools
import count
249 from mercurial
import bdiff
, mdiff
253 for pbeg
, pend
, fbeg
, fend
in bdiff
.blocks(old_data
, new_data
):
263 def check_whitespace(ui
, repo
, hooktype
, node
, parent1
, parent2
):
264 from mercurial
import mdiff
266 if hooktype
!= 'pretxncommit':
267 raise AttributeError, \
268 "This hook is only meant for pretxncommit, not %s" % hooktype
271 verbose
= ui
.configbool('style', 'verbose', False)
272 def prompt(name
, fixonly
=None):
273 result
= ui
.prompt("(a)bort, (i)gnore, or (f)ix?", "^[aif]$", "a")
279 fixwhite(repo
.wjoin(name
), tabsize
, fixonly
)
281 raise RepoError
, "Invalid response: '%s'" % result
285 modified
, added
, removed
, deleted
, unknown
, ignore
, clean
= repo
.status()
289 for line
,num
in checkwhite(fname
):
290 ui
.write("invalid whitespace in %s:%d\n" % (fname
, num
))
292 ui
.write(">>%s<<\n" % line
[-1])
299 wctx
= repo
.workingctx()
300 for fname
in modified
:
301 fctx
= wctx
.filectx(fname
)
302 pctx
= fctx
.parents()
303 assert len(pctx
) in (1, 2)
305 file_data
= fctx
.data()
306 lines
= mdiff
.splitnewlines(file_data
)
307 mod_lines
= modified_lines(pctx
[0].data(), file_data
, len(lines
))
309 m2
= modified_lines(pctx
[1].data(), file_data
, len(lines
))
310 mod_lines
= mod_lines
& m2
# only the lines that are new in both
313 for i
,line
in enumerate(lines
):
314 if i
not in mod_lines
:
317 if checkwhite_line(line
):
320 ui
.write("invalid whitespace: %s:%d\n" % (fname
, i
+1))
322 ui
.write(">>%s<<\n" % line
[:-1])
326 if prompt(fname
, fixonly
):
329 def check_format(ui
, repo
, hooktype
, node
, parent1
, parent2
):
330 if hooktype
!= 'pretxncommit':
331 raise AttributeError, \
332 "This hook is only meant for pretxncommit, not %s" % hooktype
334 modified
, added
, removed
, deleted
, unknown
, ignore
, clean
= repo
.status()
337 stats
= ValidationStats()
338 for f
in modified
+ added
:
339 validate(f
, stats
, verbose
, None)
343 result
= ui
.prompt("invalid formatting\n(i)gnore or (a)bort?",
345 if result
.startswith('i'):
347 elif result
.startswith('a'):
350 raise RepoError
, "Invalid response: '%s'" % result
354 if __name__
== '__main__':
357 progname
= sys
.argv
[0]
358 if len(sys
.argv
) < 2:
359 sys
.exit('usage: %s <command> [<command args>]' % progname
)
361 fixwhite_usage
= '%s fixwhite [-t <tabsize> ] <path> [...] \n' % progname
362 chkformat_usage
= '%s chkformat <path> [...] \n' % progname
363 chkwhite_usage
= '%s chkwhite <path> [...] \n' % progname
365 command
= sys
.argv
[1]
366 if command
== 'fixwhite':
368 usage
= fixwhite_usage
369 elif command
== 'chkwhite':
371 usage
= chkwhite_usage
372 elif command
== 'chkformat':
374 usage
= chkformat_usage
376 sys
.exit(fixwhite_usage
+ chkwhite_usage
+ chkformat_usage
)
378 opts
, args
= getopt
.getopt(sys
.argv
[2:], flags
)
391 if command
== 'fixwhite':
392 for filename
in args
:
393 fixwhite(filename
, tabsize
)
394 elif command
== 'chkwhite':
395 for filename
in args
:
396 for line
,num
in checkwhite(filename
):
397 print 'invalid whitespace: %s:%d' % (filename
, num
)
399 print '>>%s<<' % line
[:-1]
400 elif command
== 'chkformat':
401 stats
= ValidationStats()
402 for filename
in files
:
403 validate(filename
, stats
=stats
, verbose
=verbose
, exit_code
=code
)
408 sys
.exit("command '%s' not found" % command
)