##// END OF EJS Templates
check-code: fix class style checking (with tests)...
Thomas Arendsen Hein -
r14763:b071cd58 stable
parent child Browse files
Show More
@@ -1,381 +1,383
1 1 #!/usr/bin/env python
2 2 #
3 3 # check-code - a style and portability checker for Mercurial
4 4 #
5 5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 import re, glob, os, sys
11 11 import keyword
12 12 import optparse
13 13
14 14 def repquote(m):
15 15 t = re.sub(r"\w", "x", m.group('text'))
16 16 t = re.sub(r"[^\sx]", "o", t)
17 17 return m.group('quote') + t + m.group('quote')
18 18
19 19 def reppython(m):
20 20 comment = m.group('comment')
21 21 if comment:
22 22 return "#" * len(comment)
23 23 return repquote(m)
24 24
25 25 def repcomment(m):
26 26 return m.group(1) + "#" * len(m.group(2))
27 27
28 28 def repccomment(m):
29 29 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
30 30 return m.group(1) + t + "*/"
31 31
32 32 def repcallspaces(m):
33 33 t = re.sub(r"\n\s+", "\n", m.group(2))
34 34 return m.group(1) + t
35 35
36 36 def repinclude(m):
37 37 return m.group(1) + "<foo>"
38 38
39 39 def rephere(m):
40 40 t = re.sub(r"\S", "x", m.group(2))
41 41 return m.group(1) + t
42 42
43 43
44 44 testpats = [
45 45 [
46 46 (r'(pushd|popd)', "don't use 'pushd' or 'popd', use 'cd'"),
47 47 (r'\W\$?\(\([^\)]*\)\)', "don't use (()) or $(()), use 'expr'"),
48 48 (r'^function', "don't use 'function', use old style"),
49 49 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
50 50 (r'echo.*\\n', "don't use 'echo \\n', use printf"),
51 51 (r'echo -n', "don't use 'echo -n', use printf"),
52 52 (r'^diff.*-\w*N', "don't use 'diff -N'"),
53 53 (r'(^| )wc[^|]*$', "filter wc output"),
54 54 (r'head -c', "don't use 'head -c', use 'dd'"),
55 55 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
56 56 (r'printf.*\\\d\d\d', "don't use 'printf \NNN', use Python"),
57 57 (r'printf.*\\x', "don't use printf \\x, use Python"),
58 58 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
59 59 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
60 60 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
61 61 "use egrep for extended grep syntax"),
62 62 (r'/bin/', "don't use explicit paths for tools"),
63 63 (r'\$PWD', "don't use $PWD, use `pwd`"),
64 64 (r'[^\n]\Z', "no trailing newline"),
65 65 (r'export.*=', "don't export and assign at once"),
66 66 ('^([^"\']|("[^"]*")|(\'[^\']*\'))*\\^', "^ must be quoted"),
67 67 (r'^source\b', "don't use 'source', use '.'"),
68 68 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
69 69 (r'ls\s+[^|-]+\s+-', "options to 'ls' must come before filenames"),
70 70 (r'[^>]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
71 71 ],
72 72 # warnings
73 73 []
74 74 ]
75 75
76 76 testfilters = [
77 77 (r"( *)(#([^\n]*\S)?)", repcomment),
78 78 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
79 79 ]
80 80
81 81 uprefix = r"^ \$ "
82 82 uprefixc = r"^ > "
83 83 utestpats = [
84 84 [
85 85 (r'^(\S| $ ).*(\S\s+|^\s+)\n', "trailing whitespace on non-output"),
86 86 (uprefix + r'.*\|\s*sed', "use regex test output patterns instead of sed"),
87 87 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
88 88 (uprefix + r'.*\$\?', "explicit exit code checks unnecessary"),
89 89 (uprefix + r'.*\|\| echo.*(fail|error)',
90 90 "explicit exit code checks unnecessary"),
91 91 (uprefix + r'set -e', "don't use set -e"),
92 92 (uprefixc + r'( *)\t', "don't use tabs to indent"),
93 93 ],
94 94 # warnings
95 95 []
96 96 ]
97 97
98 98 for i in [0, 1]:
99 99 for p, m in testpats[i]:
100 100 if p.startswith('^'):
101 101 p = uprefix + p[1:]
102 102 else:
103 103 p = uprefix + p
104 104 utestpats[i].append((p, m))
105 105
106 106 utestfilters = [
107 107 (r"( *)(#([^\n]*\S)?)", repcomment),
108 108 ]
109 109
110 110 pypats = [
111 111 [
112 112 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
113 113 "tuple parameter unpacking not available in Python 3+"),
114 114 (r'lambda\s*\(.*,.*\)',
115 115 "tuple parameter unpacking not available in Python 3+"),
116 116 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
117 117 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
118 118 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
119 119 (r'^\s*\t', "don't use tabs"),
120 120 (r'\S;\s*\n', "semicolon"),
121 121 (r'\w,\w', "missing whitespace after ,"),
122 122 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
123 123 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
124 124 (r'.{85}', "line too long"),
125 125 (r'[^\n]\Z', "no trailing newline"),
126 126 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
127 127 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
128 128 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
129 129 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
130 130 "linebreak after :"),
131 (r'class\s[^(]:', "old-style class, use class foo(object)"),
131 (r'class\s[^( ]+:', "old-style class, use class foo(object)"),
132 (r'class\s[^( ]+\(\):',
133 "class foo() not available in Python 2.4, use class foo(object)"),
132 134 (r'\b(%s)\(' % '|'.join(keyword.kwlist),
133 135 "Python keyword is not a function"),
134 136 (r',]', "unneeded trailing ',' in list"),
135 137 # (r'class\s[A-Z][^\(]*\((?!Exception)',
136 138 # "don't capitalize non-exception classes"),
137 139 # (r'in range\(', "use xrange"),
138 140 # (r'^\s*print\s+', "avoid using print in core and extensions"),
139 141 (r'[\x80-\xff]', "non-ASCII character literal"),
140 142 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
141 143 (r'^\s*with\s+', "with not available in Python 2.4"),
142 144 (r'\.isdisjoint\(', "set.isdisjoint not available in Python 2.4"),
143 145 (r'^\s*except.* as .*:', "except as not available in Python 2.4"),
144 146 (r'^\s*os\.path\.relpath', "relpath not available in Python 2.4"),
145 147 (r'(?<!def)\s+(any|all|format)\(',
146 148 "any/all/format not available in Python 2.4"),
147 149 (r'(?<!def)\s+(callable)\(',
148 150 "callable not available in Python 3, use hasattr(f, '__call__')"),
149 151 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
150 152 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
151 153 "gratuitous whitespace after Python keyword"),
152 154 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
153 155 # (r'\s\s=', "gratuitous whitespace before ="),
154 156 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
155 157 "missing whitespace around operator"),
156 158 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s',
157 159 "missing whitespace around operator"),
158 160 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
159 161 "missing whitespace around operator"),
160 162 (r'[^+=*/!<>&| -](\s=|=\s)[^= ]',
161 163 "wrong whitespace around ="),
162 164 (r'raise Exception', "don't raise generic exceptions"),
163 165 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
164 166 (r' [=!]=\s+(True|False|None)',
165 167 "comparison with singleton, use 'is' or 'is not' instead"),
166 168 (r'^\s*(while|if) [01]:',
167 169 "use True/False for constant Boolean expression"),
168 170 (r'opener\([^)]*\).read\(',
169 171 "use opener.read() instead"),
170 172 (r'opener\([^)]*\).write\(',
171 173 "use opener.write() instead"),
172 174 (r'[\s\(](open|file)\([^)]*\)\.read\(',
173 175 "use util.readfile() instead"),
174 176 (r'[\s\(](open|file)\([^)]*\)\.write\(',
175 177 "use util.readfile() instead"),
176 178 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
177 179 "always assign an opened file to a variable, and close it afterwards"),
178 180 (r'[\s\(](open|file)\([^)]*\)\.',
179 181 "always assign an opened file to a variable, and close it afterwards"),
180 182 (r'(?i)descendent', "the proper spelling is descendAnt"),
181 183 (r'\.debug\(\_', "don't mark debug messages for translation"),
182 184 ],
183 185 # warnings
184 186 [
185 187 (r'.{81}', "warning: line over 80 characters"),
186 188 (r'^\s*except:$', "warning: naked except clause"),
187 189 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
188 190 "warning: unwrapped ui message"),
189 191 ]
190 192 ]
191 193
192 194 pyfilters = [
193 195 (r"""(?msx)(?P<comment>\#.*?$)|
194 196 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
195 197 (?P<text>(([^\\]|\\.)*?))
196 198 (?P=quote))""", reppython),
197 199 ]
198 200
199 201 cpats = [
200 202 [
201 203 (r'//', "don't use //-style comments"),
202 204 (r'^ ', "don't use spaces to indent"),
203 205 (r'\S\t', "don't use tabs except for indent"),
204 206 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
205 207 (r'.{85}', "line too long"),
206 208 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
207 209 (r'return\(', "return is not a function"),
208 210 (r' ;', "no space before ;"),
209 211 (r'\w+\* \w+', "use int *foo, not int* foo"),
210 212 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
211 213 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
212 214 (r'\w,\w', "missing whitespace after ,"),
213 215 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
214 216 (r'^#\s+\w', "use #foo, not # foo"),
215 217 (r'[^\n]\Z', "no trailing newline"),
216 218 (r'^\s*#import\b', "use only #include in standard C code"),
217 219 ],
218 220 # warnings
219 221 []
220 222 ]
221 223
222 224 cfilters = [
223 225 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
224 226 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
225 227 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
226 228 (r'(\()([^)]+\))', repcallspaces),
227 229 ]
228 230
229 231 inutilpats = [
230 232 [
231 233 (r'\bui\.', "don't use ui in util"),
232 234 ],
233 235 # warnings
234 236 []
235 237 ]
236 238
237 239 inrevlogpats = [
238 240 [
239 241 (r'\brepo\.', "don't use repo in revlog"),
240 242 ],
241 243 # warnings
242 244 []
243 245 ]
244 246
245 247 checks = [
246 248 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
247 249 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
248 250 ('c', r'.*\.c$', cfilters, cpats),
249 251 ('unified test', r'.*\.t$', utestfilters, utestpats),
250 252 ('layering violation repo in revlog', r'mercurial/revlog\.py', pyfilters,
251 253 inrevlogpats),
252 254 ('layering violation ui in util', r'mercurial/util\.py', pyfilters,
253 255 inutilpats),
254 256 ]
255 257
256 258 class norepeatlogger(object):
257 259 def __init__(self):
258 260 self._lastseen = None
259 261
260 262 def log(self, fname, lineno, line, msg, blame):
261 263 """print error related a to given line of a given file.
262 264
263 265 The faulty line will also be printed but only once in the case
264 266 of multiple errors.
265 267
266 268 :fname: filename
267 269 :lineno: line number
268 270 :line: actual content of the line
269 271 :msg: error message
270 272 """
271 273 msgid = fname, lineno, line
272 274 if msgid != self._lastseen:
273 275 if blame:
274 276 print "%s:%d (%s):" % (fname, lineno, blame)
275 277 else:
276 278 print "%s:%d:" % (fname, lineno)
277 279 print " > %s" % line
278 280 self._lastseen = msgid
279 281 print " " + msg
280 282
281 283 _defaultlogger = norepeatlogger()
282 284
283 285 def getblame(f):
284 286 lines = []
285 287 for l in os.popen('hg annotate -un %s' % f):
286 288 start, line = l.split(':', 1)
287 289 user, rev = start.split()
288 290 lines.append((line[1:-1], user, rev))
289 291 return lines
290 292
291 293 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
292 294 blame=False, debug=False):
293 295 """checks style and portability of a given file
294 296
295 297 :f: filepath
296 298 :logfunc: function used to report error
297 299 logfunc(filename, linenumber, linecontent, errormessage)
298 300 :maxerr: number of error to display before arborting.
299 301 Set to None (default) to report all errors
300 302
301 303 return True if no error is found, False otherwise.
302 304 """
303 305 blamecache = None
304 306 result = True
305 307 for name, match, filters, pats in checks:
306 308 if debug:
307 309 print name, f
308 310 fc = 0
309 311 if not re.match(match, f):
310 312 if debug:
311 313 print "Skipping %s for %s it doesn't match %s" % (
312 314 name, match, f)
313 315 continue
314 316 fp = open(f)
315 317 pre = post = fp.read()
316 318 fp.close()
317 319 if "no-" + "check-code" in pre:
318 320 if debug:
319 321 print "Skipping %s for %s it has no- and check-code" % (
320 322 name, f)
321 323 break
322 324 for p, r in filters:
323 325 post = re.sub(p, r, post)
324 326 if warnings:
325 327 pats = pats[0] + pats[1]
326 328 else:
327 329 pats = pats[0]
328 330 # print post # uncomment to show filtered version
329 331 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
330 332 if debug:
331 333 print "Checking %s for %s" % (name, f)
332 334 for n, l in z:
333 335 if "check-code" + "-ignore" in l[0]:
334 336 if debug:
335 337 print "Skipping %s for %s:%s (check-code -ignore)" % (
336 338 name, f, n)
337 339 continue
338 340 for p, msg in pats:
339 341 if re.search(p, l[1]):
340 342 bd = ""
341 343 if blame:
342 344 bd = 'working directory'
343 345 if not blamecache:
344 346 blamecache = getblame(f)
345 347 if n < len(blamecache):
346 348 bl, bu, br = blamecache[n]
347 349 if bl == l[0]:
348 350 bd = '%s@%s' % (bu, br)
349 351 logfunc(f, n + 1, l[0], msg, bd)
350 352 fc += 1
351 353 result = False
352 354 if maxerr is not None and fc >= maxerr:
353 355 print " (too many errors, giving up)"
354 356 break
355 357 return result
356 358
357 359 if __name__ == "__main__":
358 360 parser = optparse.OptionParser("%prog [options] [files]")
359 361 parser.add_option("-w", "--warnings", action="store_true",
360 362 help="include warning-level checks")
361 363 parser.add_option("-p", "--per-file", type="int",
362 364 help="max warnings per file")
363 365 parser.add_option("-b", "--blame", action="store_true",
364 366 help="use annotate to generate blame info")
365 367 parser.add_option("", "--debug", action="store_true",
366 368 help="show debug information")
367 369
368 370 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False)
369 371 (options, args) = parser.parse_args()
370 372
371 373 if len(args) == 0:
372 374 check = glob.glob("*")
373 375 else:
374 376 check = args
375 377
376 378 for f in check:
377 379 ret = 0
378 380 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
379 381 blame=options.blame, debug=options.debug):
380 382 ret = 1
381 383 sys.exit(ret)
@@ -1,95 +1,114
1 1 $ cat > correct.py <<EOF
2 2 > def toto(arg1, arg2):
3 3 > del arg2
4 4 > return (5 + 6, 9)
5 5 > EOF
6 6 $ cat > wrong.py <<EOF
7 7 > def toto( arg1, arg2):
8 8 > del(arg2)
9 9 > return ( 5+6, 9)
10 10 > EOF
11 11 $ cat > quote.py <<EOF
12 12 > # let's use quote in comments
13 13 > (''' ( 4x5 )
14 14 > but """\\''' and finally''',
15 15 > """let's fool checkpatch""", '1+2',
16 16 > '"""', 42+1, """and
17 17 > ( 4-1 ) """, "( 1+1 )\" and ")
18 18 > a, '\\\\\\\\', "\\\\\\" x-2", "c-1"
19 19 > EOF
20 20 $ cat > non-py24.py <<EOF
21 21 > # Using builtins that does not exist in Python 2.4
22 22 > if any():
23 23 > x = all()
24 24 > y = format(x)
25 25 >
26 26 > # Do not complain about our own definition
27 27 > def any(x):
28 28 > pass
29 29 > EOF
30 $ cat > classstyle.py <<EOF
31 > class newstyle_class(object):
32 > pass
33 >
34 > class oldstyle_class:
35 > pass
36 >
37 > class empty():
38 > pass
39 >
40 > no_class = 1:
41 > pass
42 > EOF
30 43 $ check_code="$TESTDIR"/../contrib/check-code.py
31 $ "$check_code" ./wrong.py ./correct.py ./quote.py ./non-py24.py
44 $ "$check_code" ./wrong.py ./correct.py ./quote.py ./non-py24.py ./classstyle.py
32 45 ./wrong.py:1:
33 46 > def toto( arg1, arg2):
34 47 gratuitous whitespace in () or []
35 48 ./wrong.py:2:
36 49 > del(arg2)
37 50 Python keyword is not a function
38 51 ./wrong.py:3:
39 52 > return ( 5+6, 9)
40 53 missing whitespace in expression
41 54 gratuitous whitespace in () or []
42 55 ./quote.py:5:
43 56 > '"""', 42+1, """and
44 57 missing whitespace in expression
45 58 ./non-py24.py:2:
46 59 > if any():
47 60 any/all/format not available in Python 2.4
48 61 ./non-py24.py:3:
49 62 > x = all()
50 63 any/all/format not available in Python 2.4
51 64 ./non-py24.py:4:
52 65 > y = format(x)
53 66 any/all/format not available in Python 2.4
67 ./classstyle.py:4:
68 > class oldstyle_class:
69 old-style class, use class foo(object)
70 ./classstyle.py:7:
71 > class empty():
72 class foo() not available in Python 2.4, use class foo(object)
54 73 [1]
55 74
56 75 $ cat > is-op.py <<EOF
57 76 > # is-operator comparing number or string literal
58 77 > x = None
59 78 > y = x is 'foo'
60 79 > y = x is "foo"
61 80 > y = x is 5346
62 81 > y = x is -6
63 82 > y = x is not 'foo'
64 83 > y = x is not "foo"
65 84 > y = x is not 5346
66 85 > y = x is not -6
67 86 > EOF
68 87
69 88 $ "$check_code" ./is-op.py
70 89 ./is-op.py:3:
71 90 > y = x is 'foo'
72 91 object comparison with literal
73 92 ./is-op.py:4:
74 93 > y = x is "foo"
75 94 object comparison with literal
76 95 ./is-op.py:5:
77 96 > y = x is 5346
78 97 object comparison with literal
79 98 ./is-op.py:6:
80 99 > y = x is -6
81 100 object comparison with literal
82 101 ./is-op.py:7:
83 102 > y = x is not 'foo'
84 103 object comparison with literal
85 104 ./is-op.py:8:
86 105 > y = x is not "foo"
87 106 object comparison with literal
88 107 ./is-op.py:9:
89 108 > y = x is not 5346
90 109 object comparison with literal
91 110 ./is-op.py:10:
92 111 > y = x is not -6
93 112 object comparison with literal
94 113 [1]
95 114
General Comments 0
You need to be logged in to leave comments. Login now