##// END OF EJS Templates
check-code: add --blame switch
Matt Mackall -
r11604:c5d40818 default
parent child Browse files
Show More
@@ -1,239 +1,263
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # check-code - a style and portability checker for Mercurial
3 # check-code - a style and portability checker for Mercurial
4 #
4 #
5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 import re, glob
10 import re, glob, os
11 import optparse
11 import optparse
12
12
13 def repquote(m):
13 def repquote(m):
14 t = re.sub(r"\w", "x", m.group('text'))
14 t = re.sub(r"\w", "x", m.group('text'))
15 t = re.sub(r"[^\sx]", "o", t)
15 t = re.sub(r"[^\sx]", "o", t)
16 return m.group('quote') + t + m.group('quote')
16 return m.group('quote') + t + m.group('quote')
17
17
18 def reppython(m):
18 def reppython(m):
19 comment = m.group('comment')
19 comment = m.group('comment')
20 if comment:
20 if comment:
21 return "#" * len(comment)
21 return "#" * len(comment)
22 return repquote(m)
22 return repquote(m)
23
23
24 def repcomment(m):
24 def repcomment(m):
25 return m.group(1) + "#" * len(m.group(2))
25 return m.group(1) + "#" * len(m.group(2))
26
26
27 def repccomment(m):
27 def repccomment(m):
28 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
28 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
29 return m.group(1) + t + "*/"
29 return m.group(1) + t + "*/"
30
30
31 def repcallspaces(m):
31 def repcallspaces(m):
32 t = re.sub(r"\n\s+", "\n", m.group(2))
32 t = re.sub(r"\n\s+", "\n", m.group(2))
33 return m.group(1) + t
33 return m.group(1) + t
34
34
35 def repinclude(m):
35 def repinclude(m):
36 return m.group(1) + "<foo>"
36 return m.group(1) + "<foo>"
37
37
38 def rephere(m):
38 def rephere(m):
39 t = re.sub(r"\S", "x", m.group(2))
39 t = re.sub(r"\S", "x", m.group(2))
40 return m.group(1) + t
40 return m.group(1) + t
41
41
42
42
43 testpats = [
43 testpats = [
44 (r'(pushd|popd)', "don't use 'pushd' or 'popd', use 'cd'"),
44 (r'(pushd|popd)', "don't use 'pushd' or 'popd', use 'cd'"),
45 (r'\W\$?\(\([^\)]*\)\)', "don't use (()) or $(()), use 'expr'"),
45 (r'\W\$?\(\([^\)]*\)\)', "don't use (()) or $(()), use 'expr'"),
46 (r'^function', "don't use 'function', use old style"),
46 (r'^function', "don't use 'function', use old style"),
47 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
47 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
48 (r'echo.*\\n', "don't use 'echo \\n', use printf"),
48 (r'echo.*\\n', "don't use 'echo \\n', use printf"),
49 (r'^diff.*-\w*N', "don't use 'diff -N'"),
49 (r'^diff.*-\w*N', "don't use 'diff -N'"),
50 (r'(^| )wc[^|]*$', "filter wc output"),
50 (r'(^| )wc[^|]*$', "filter wc output"),
51 (r'head -c', "don't use 'head -c', use 'dd'"),
51 (r'head -c', "don't use 'head -c', use 'dd'"),
52 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
52 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
53 (r'printf.*\\\d\d\d', "don't use 'printf \NNN', use Python"),
53 (r'printf.*\\\d\d\d', "don't use 'printf \NNN', use Python"),
54 (r'printf.*\\x', "don't use printf \\x, use Python"),
54 (r'printf.*\\x', "don't use printf \\x, use Python"),
55 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
55 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
56 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
56 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
57 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
57 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
58 "use egrep for extended grep syntax"),
58 "use egrep for extended grep syntax"),
59 (r'/bin/', "don't use explicit paths for tools"),
59 (r'/bin/', "don't use explicit paths for tools"),
60 (r'\$PWD', "don't use $PWD, use `pwd`"),
60 (r'\$PWD', "don't use $PWD, use `pwd`"),
61 (r'[^\n]\Z', "no trailing newline"),
61 (r'[^\n]\Z', "no trailing newline"),
62 (r'export.*=', "don't export and assign at once"),
62 (r'export.*=', "don't export and assign at once"),
63 ('^([^"\']|("[^"]*")|(\'[^\']*\'))*\\^', "^ must be quoted"),
63 ('^([^"\']|("[^"]*")|(\'[^\']*\'))*\\^', "^ must be quoted"),
64 (r'^source\b', "don't use 'source', use '.'"),
64 (r'^source\b', "don't use 'source', use '.'"),
65 ]
65 ]
66
66
67 testfilters = [
67 testfilters = [
68 (r"( *)(#([^\n]*\S)?)", repcomment),
68 (r"( *)(#([^\n]*\S)?)", repcomment),
69 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
69 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
70 ]
70 ]
71
71
72 pypats = [
72 pypats = [
73 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
73 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
74 "tuple parameter unpacking not available in Python 3+"),
74 "tuple parameter unpacking not available in Python 3+"),
75 (r'lambda\s*\(.*,.*\)',
75 (r'lambda\s*\(.*,.*\)',
76 "tuple parameter unpacking not available in Python 3+"),
76 "tuple parameter unpacking not available in Python 3+"),
77 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
77 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
78 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
78 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
79 (r'^\s*\t', "don't use tabs"),
79 (r'^\s*\t', "don't use tabs"),
80 (r'\S;\s*\n', "semicolon"),
80 (r'\S;\s*\n', "semicolon"),
81 (r'\w,\w', "missing whitespace after ,"),
81 (r'\w,\w', "missing whitespace after ,"),
82 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
82 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
83 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
83 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
84 (r'.{85}', "line too long"),
84 (r'.{85}', "line too long"),
85 (r'[^\n]\Z', "no trailing newline"),
85 (r'[^\n]\Z', "no trailing newline"),
86 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
86 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
87 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
87 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
88 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
88 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
89 "linebreak after :"),
89 "linebreak after :"),
90 (r'class\s[^(]:', "old-style class, use class foo(object)"),
90 (r'class\s[^(]:', "old-style class, use class foo(object)"),
91 (r'^\s+del\(', "del isn't a function"),
91 (r'^\s+del\(', "del isn't a function"),
92 (r'^\s+except\(', "except isn't a function"),
92 (r'^\s+except\(', "except isn't a function"),
93 (r',]', "unneeded trailing ',' in list"),
93 (r',]', "unneeded trailing ',' in list"),
94 # (r'class\s[A-Z][^\(]*\((?!Exception)',
94 # (r'class\s[A-Z][^\(]*\((?!Exception)',
95 # "don't capitalize non-exception classes"),
95 # "don't capitalize non-exception classes"),
96 # (r'in range\(', "use xrange"),
96 # (r'in range\(', "use xrange"),
97 # (r'^\s*print\s+', "avoid using print in core and extensions"),
97 # (r'^\s*print\s+', "avoid using print in core and extensions"),
98 (r'[\x80-\xff]', "non-ASCII character literal"),
98 (r'[\x80-\xff]', "non-ASCII character literal"),
99 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
99 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
100 (r'^\s*with\s+', "with not available in Python 2.4"),
100 (r'^\s*with\s+', "with not available in Python 2.4"),
101 (r'(?<!def)\s+(any|all|format)\(',
101 (r'(?<!def)\s+(any|all|format)\(',
102 "any/all/format not available in Python 2.4"),
102 "any/all/format not available in Python 2.4"),
103 (r'(?<!def)\s+(callable)\(',
103 (r'(?<!def)\s+(callable)\(',
104 "callable not available in Python 3, use hasattr(f, '__call__')"),
104 "callable not available in Python 3, use hasattr(f, '__call__')"),
105 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
105 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
106 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
106 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
107 # (r'\s\s=', "gratuitous whitespace before ="),
107 # (r'\s\s=', "gratuitous whitespace before ="),
108 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
108 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
109 "missing whitespace around operator"),
109 "missing whitespace around operator"),
110 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s',
110 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s',
111 "missing whitespace around operator"),
111 "missing whitespace around operator"),
112 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
112 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
113 "missing whitespace around operator"),
113 "missing whitespace around operator"),
114 (r'[^+=*!<>&| -](\s=|=\s)[^= ]',
114 (r'[^+=*!<>&| -](\s=|=\s)[^= ]',
115 "wrong whitespace around ="),
115 "wrong whitespace around ="),
116 (r'raise Exception', "don't raise generic exceptions"),
116 (r'raise Exception', "don't raise generic exceptions"),
117 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
117 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
118 "warning: unwrapped ui message"),
118 "warning: unwrapped ui message"),
119 ]
119 ]
120
120
121 pyfilters = [
121 pyfilters = [
122 (r"""(?msx)(?P<comment>\#.*?$)|
122 (r"""(?msx)(?P<comment>\#.*?$)|
123 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
123 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
124 (?P<text>(([^\\]|\\.)*?))
124 (?P<text>(([^\\]|\\.)*?))
125 (?P=quote))""", reppython),
125 (?P=quote))""", reppython),
126 ]
126 ]
127
127
128 cpats = [
128 cpats = [
129 (r'//', "don't use //-style comments"),
129 (r'//', "don't use //-style comments"),
130 (r'^ ', "don't use spaces to indent"),
130 (r'^ ', "don't use spaces to indent"),
131 (r'\S\t', "don't use tabs except for indent"),
131 (r'\S\t', "don't use tabs except for indent"),
132 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
132 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
133 (r'.{85}', "line too long"),
133 (r'.{85}', "line too long"),
134 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
134 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
135 (r'return\(', "return is not a function"),
135 (r'return\(', "return is not a function"),
136 (r' ;', "no space before ;"),
136 (r' ;', "no space before ;"),
137 (r'\w+\* \w+', "use int *foo, not int* foo"),
137 (r'\w+\* \w+', "use int *foo, not int* foo"),
138 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
138 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
139 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
139 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
140 (r'\w,\w', "missing whitespace after ,"),
140 (r'\w,\w', "missing whitespace after ,"),
141 (r'\w[+/*]\w', "missing whitespace in expression"),
141 (r'\w[+/*]\w', "missing whitespace in expression"),
142 (r'^#\s+\w', "use #foo, not # foo"),
142 (r'^#\s+\w', "use #foo, not # foo"),
143 (r'[^\n]\Z', "no trailing newline"),
143 (r'[^\n]\Z', "no trailing newline"),
144 ]
144 ]
145
145
146 cfilters = [
146 cfilters = [
147 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
147 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
148 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
148 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
149 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
149 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
150 (r'(\()([^)]+\))', repcallspaces),
150 (r'(\()([^)]+\))', repcallspaces),
151 ]
151 ]
152
152
153 checks = [
153 checks = [
154 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
154 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
155 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
155 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
156 ('c', r'.*\.c$', cfilters, cpats),
156 ('c', r'.*\.c$', cfilters, cpats),
157 ]
157 ]
158
158
159 class norepeatlogger(object):
159 class norepeatlogger(object):
160 def __init__(self):
160 def __init__(self):
161 self._lastseen = None
161 self._lastseen = None
162
162
163 def log(self, fname, lineno, line, msg):
163 def log(self, fname, lineno, line, msg, blame):
164 """print error related a to given line of a given file.
164 """print error related a to given line of a given file.
165
165
166 The faulty line will also be printed but only once in the case
166 The faulty line will also be printed but only once in the case
167 of multiple errors.
167 of multiple errors.
168
168
169 :fname: filename
169 :fname: filename
170 :lineno: line number
170 :lineno: line number
171 :line: actual content of the line
171 :line: actual content of the line
172 :msg: error message
172 :msg: error message
173 """
173 """
174 msgid = fname, lineno, line
174 msgid = fname, lineno, line
175 if msgid != self._lastseen:
175 if msgid != self._lastseen:
176 print "%s:%d:" % (fname, lineno)
176 if blame:
177 print "%s:%d (%s):" % (fname, lineno, blame)
178 else:
179 print "%s:%d:" % (fname, lineno)
177 print " > %s" % line
180 print " > %s" % line
178 self._lastseen = msgid
181 self._lastseen = msgid
179 print " " + msg
182 print " " + msg
180
183
181 _defaultlogger = norepeatlogger()
184 _defaultlogger = norepeatlogger()
182
185
183 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False):
186 def getblame(f):
187 lines = []
188 for l in os.popen('hg annotate -un %s' % f):
189 start, line = l.split(':', 1)
190 user, rev = start.split()
191 lines.append((line[1:-1], user, rev))
192 return lines
193
194 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
195 blame=False):
184 """checks style and portability of a given file
196 """checks style and portability of a given file
185
197
186 :f: filepath
198 :f: filepath
187 :logfunc: function used to report error
199 :logfunc: function used to report error
188 logfunc(filename, linenumber, linecontent, errormessage)
200 logfunc(filename, linenumber, linecontent, errormessage)
189 :maxerr: number of error to display before arborting.
201 :maxerr: number of error to display before arborting.
190 Set to None (default) to report all errors
202 Set to None (default) to report all errors
191
203
192 return True if no error is found, False otherwise.
204 return True if no error is found, False otherwise.
193 """
205 """
206 blamecache = None
194 result = True
207 result = True
195 for name, match, filters, pats in checks:
208 for name, match, filters, pats in checks:
196 fc = 0
209 fc = 0
197 if not re.match(match, f):
210 if not re.match(match, f):
198 continue
211 continue
199 pre = post = open(f).read()
212 pre = post = open(f).read()
200 if "no-" + "check-code" in pre:
213 if "no-" + "check-code" in pre:
201 break
214 break
202 for p, r in filters:
215 for p, r in filters:
203 post = re.sub(p, r, post)
216 post = re.sub(p, r, post)
204 # print post # uncomment to show filtered version
217 # print post # uncomment to show filtered version
205 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
218 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
206 for n, l in z:
219 for n, l in z:
207 if "check-code" + "-ignore" in l[0]:
220 if "check-code" + "-ignore" in l[0]:
208 continue
221 continue
209 for p, msg in pats:
222 for p, msg in pats:
210 if not warnings and msg.startswith("warning"):
223 if not warnings and msg.startswith("warning"):
211 continue
224 continue
212 if re.search(p, l[1]):
225 if re.search(p, l[1]):
213 logfunc(f, n + 1, l[0], msg)
226 bd = ""
227 if blame:
228 bd = 'working directory'
229 if not blamecache:
230 blamecache = getblame(f)
231 if n < len(blamecache):
232 bl, bu, br = blamecache[n]
233 if bl == l[0]:
234 bd = '%s@%s' % (bu, br)
235 logfunc(f, n + 1, l[0], msg, bd)
214 fc += 1
236 fc += 1
215 result = False
237 result = False
216 if maxerr is not None and fc >= maxerr:
238 if maxerr is not None and fc >= maxerr:
217 print " (too many errors, giving up)"
239 print " (too many errors, giving up)"
218 break
240 break
219 break
241 break
220 return result
242 return result
221
243
222
223 if __name__ == "__main__":
244 if __name__ == "__main__":
224 parser = optparse.OptionParser("%prog [options] [files]")
245 parser = optparse.OptionParser("%prog [options] [files]")
225 parser.add_option("-w", "--warnings", action="store_true",
246 parser.add_option("-w", "--warnings", action="store_true",
226 help="include warning-level checks")
247 help="include warning-level checks")
227 parser.add_option("-p", "--per-file", type="int",
248 parser.add_option("-p", "--per-file", type="int",
228 help="max warnings per file")
249 help="max warnings per file")
250 parser.add_option("-b", "--blame", action="store_true",
251 help="use annotate to generate blame info")
229
252
230 parser.set_defaults(per_file=15, warnings=False)
253 parser.set_defaults(per_file=15, warnings=False, blame=False)
231 (options, args) = parser.parse_args()
254 (options, args) = parser.parse_args()
232
255
233 if len(args) == 0:
256 if len(args) == 0:
234 check = glob.glob("*")
257 check = glob.glob("*")
235 else:
258 else:
236 check = args
259 check = args
237
260
238 for f in check:
261 for f in check:
239 checkfile(f, maxerr=options.per_file, warnings=options.warnings)
262 checkfile(f, maxerr=options.per_file, warnings=options.warnings,
263 blame=options.blame)
General Comments 0
You need to be logged in to leave comments. Login now