##// END OF EJS Templates
check-code: add check for lock release order
Matt Mackall -
r19031:341083b0 default
parent child Browse files
Show More
@@ -1,475 +1,476 b''
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"[^\s\nx]", "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 l = len(comment.rstrip())
23 23 return "#" * l + comment[l:]
24 24 return repquote(m)
25 25
26 26 def repcomment(m):
27 27 return m.group(1) + "#" * len(m.group(2))
28 28
29 29 def repccomment(m):
30 30 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
31 31 return m.group(1) + t + "*/"
32 32
33 33 def repcallspaces(m):
34 34 t = re.sub(r"\n\s+", "\n", m.group(2))
35 35 return m.group(1) + t
36 36
37 37 def repinclude(m):
38 38 return m.group(1) + "<foo>"
39 39
40 40 def rephere(m):
41 41 t = re.sub(r"\S", "x", m.group(2))
42 42 return m.group(1) + t
43 43
44 44
45 45 testpats = [
46 46 [
47 47 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
48 48 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
49 49 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
50 50 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
51 51 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
52 52 (r'echo -n', "don't use 'echo -n', use printf"),
53 53 (r'(^| )wc[^|]*$\n(?!.*\(re\))', "filter wc output"),
54 54 (r'head -c', "don't use 'head -c', use 'dd'"),
55 55 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
56 56 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
57 57 (r'printf.*\\([1-9]|0\d)', "don't use 'printf \NNN', use Python"),
58 58 (r'printf.*\\x', "don't use printf \\x, use Python"),
59 59 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
60 60 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
61 61 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
62 62 "use egrep for extended grep syntax"),
63 63 (r'/bin/', "don't use explicit paths for tools"),
64 64 (r'[^\n]\Z', "no trailing newline"),
65 65 (r'export.*=', "don't export and assign at once"),
66 66 (r'^source\b', "don't use 'source', use '.'"),
67 67 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
68 68 (r'ls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
69 69 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
70 70 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
71 71 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
72 72 (r'^alias\b.*=', "don't use alias, use a function"),
73 73 (r'if\s*!', "don't use '!' to negate exit status"),
74 74 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
75 75 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
76 76 (r'^( *)\t', "don't use tabs to indent"),
77 77 ],
78 78 # warnings
79 79 [
80 80 (r'^function', "don't use 'function', use old style"),
81 81 (r'^diff.*-\w*N', "don't use 'diff -N'"),
82 82 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
83 83 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
84 84 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
85 85 ]
86 86 ]
87 87
88 88 testfilters = [
89 89 (r"( *)(#([^\n]*\S)?)", repcomment),
90 90 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
91 91 ]
92 92
93 93 winglobmsg = "use (glob) to match Windows paths too"
94 94 uprefix = r"^ \$ "
95 95 utestpats = [
96 96 [
97 97 (r'^(\S.*|| [$>] .*)[ \t]\n', "trailing whitespace on non-output"),
98 98 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
99 99 "use regex test output patterns instead of sed"),
100 100 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
101 101 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
102 102 (uprefix + r'.*\|\| echo.*(fail|error)',
103 103 "explicit exit code checks unnecessary"),
104 104 (uprefix + r'set -e', "don't use set -e"),
105 105 (uprefix + r'\s', "don't indent commands, use > for continued lines"),
106 106 (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg),
107 107 (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$',
108 108 winglobmsg),
109 109 (r'^ pulling from \$TESTTMP/.*[^)]$', winglobmsg, '\$TESTTMP/unix-repo$'),
110 110 ],
111 111 # warnings
112 112 [
113 113 (r'^ [^*?/\n]* \(glob\)$',
114 114 "warning: glob match with no glob character (?*/)"),
115 115 ]
116 116 ]
117 117
118 118 for i in [0, 1]:
119 119 for p, m in testpats[i]:
120 120 if p.startswith(r'^'):
121 121 p = r"^ [$>] (%s)" % p[1:]
122 122 else:
123 123 p = r"^ [$>] .*(%s)" % p
124 124 utestpats[i].append((p, m))
125 125
126 126 utestfilters = [
127 127 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
128 128 (r"( *)(#([^\n]*\S)?)", repcomment),
129 129 ]
130 130
131 131 pypats = [
132 132 [
133 133 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
134 134 "tuple parameter unpacking not available in Python 3+"),
135 135 (r'lambda\s*\(.*,.*\)',
136 136 "tuple parameter unpacking not available in Python 3+"),
137 137 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
138 138 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
139 139 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
140 140 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
141 141 (r'^\s*\t', "don't use tabs"),
142 142 (r'\S;\s*\n', "semicolon"),
143 143 (r'[^_]_\("[^"]+"\s*%', "don't use % inside _()"),
144 144 (r"[^_]_\('[^']+'\s*%", "don't use % inside _()"),
145 145 (r'(\w|\)),\w', "missing whitespace after ,"),
146 146 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
147 147 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
148 148 (r'(\s+)try:\n((?:\n|\1\s.*\n)+?)\1except.*?:\n'
149 149 r'((?:\n|\1\s.*\n)+?)\1finally:', 'no try/except/finally in Python 2.4'),
150 150 (r'(\s+)try:\n((?:\n|\1\s.*\n)*?)\1\s*yield\b.*?'
151 151 r'((?:\n|\1\s.*\n)+?)\1finally:',
152 152 'no yield inside try/finally in Python 2.4'),
153 153 (r'.{81}', "line too long"),
154 154 (r' x+[xo][\'"]\n\s+[\'"]x', 'string join across lines with no space'),
155 155 (r'[^\n]\Z', "no trailing newline"),
156 156 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
157 157 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
158 158 # "don't use underbars in identifiers"),
159 159 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
160 160 "don't use camelcase in identifiers"),
161 161 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
162 162 "linebreak after :"),
163 163 (r'class\s[^( \n]+:', "old-style class, use class foo(object)"),
164 164 (r'class\s[^( \n]+\(\):',
165 165 "class foo() not available in Python 2.4, use class foo(object)"),
166 166 (r'\b(%s)\(' % '|'.join(keyword.kwlist),
167 167 "Python keyword is not a function"),
168 168 (r',]', "unneeded trailing ',' in list"),
169 169 # (r'class\s[A-Z][^\(]*\((?!Exception)',
170 170 # "don't capitalize non-exception classes"),
171 171 # (r'in range\(', "use xrange"),
172 172 # (r'^\s*print\s+', "avoid using print in core and extensions"),
173 173 (r'[\x80-\xff]', "non-ASCII character literal"),
174 174 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
175 175 (r'^\s*with\s+', "with not available in Python 2.4"),
176 176 (r'\.isdisjoint\(', "set.isdisjoint not available in Python 2.4"),
177 177 (r'^\s*except.* as .*:', "except as not available in Python 2.4"),
178 178 (r'^\s*os\.path\.relpath', "relpath not available in Python 2.4"),
179 179 (r'(?<!def)\s+(any|all|format)\(',
180 180 "any/all/format not available in Python 2.4"),
181 181 (r'(?<!def)\s+(callable)\(',
182 182 "callable not available in Python 3, use getattr(f, '__call__', None)"),
183 183 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
184 184 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
185 185 "gratuitous whitespace after Python keyword"),
186 186 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
187 187 # (r'\s\s=', "gratuitous whitespace before ="),
188 188 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
189 189 "missing whitespace around operator"),
190 190 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
191 191 "missing whitespace around operator"),
192 192 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
193 193 "missing whitespace around operator"),
194 194 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
195 195 "wrong whitespace around ="),
196 196 (r'raise Exception', "don't raise generic exceptions"),
197 197 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
198 198 "don't use old-style two-argument raise, use Exception(message)"),
199 199 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
200 200 (r' [=!]=\s+(True|False|None)',
201 201 "comparison with singleton, use 'is' or 'is not' instead"),
202 202 (r'^\s*(while|if) [01]:',
203 203 "use True/False for constant Boolean expression"),
204 204 (r'(?:(?<!def)\s+|\()hasattr',
205 205 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
206 206 (r'opener\([^)]*\).read\(',
207 207 "use opener.read() instead"),
208 208 (r'BaseException', 'not in Python 2.4, use Exception'),
209 209 (r'os\.path\.relpath', 'os.path.relpath is not in Python 2.5'),
210 210 (r'opener\([^)]*\).write\(',
211 211 "use opener.write() instead"),
212 212 (r'[\s\(](open|file)\([^)]*\)\.read\(',
213 213 "use util.readfile() instead"),
214 214 (r'[\s\(](open|file)\([^)]*\)\.write\(',
215 215 "use util.readfile() instead"),
216 216 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
217 217 "always assign an opened file to a variable, and close it afterwards"),
218 218 (r'[\s\(](open|file)\([^)]*\)\.',
219 219 "always assign an opened file to a variable, and close it afterwards"),
220 220 (r'(?i)descendent', "the proper spelling is descendAnt"),
221 221 (r'\.debug\(\_', "don't mark debug messages for translation"),
222 222 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
223 223 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
224 224 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
225 225 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
226 226 "missing _() in ui message (use () to hide false-positives)"),
227 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
227 228 ],
228 229 # warnings
229 230 [
230 231 ]
231 232 ]
232 233
233 234 pyfilters = [
234 235 (r"""(?msx)(?P<comment>\#.*?$)|
235 236 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
236 237 (?P<text>(([^\\]|\\.)*?))
237 238 (?P=quote))""", reppython),
238 239 ]
239 240
240 241 txtfilters = []
241 242
242 243 txtpats = [
243 244 [
244 245 ('\s$', 'trailing whitespace'),
245 246 ],
246 247 []
247 248 ]
248 249
249 250 cpats = [
250 251 [
251 252 (r'//', "don't use //-style comments"),
252 253 (r'^ ', "don't use spaces to indent"),
253 254 (r'\S\t', "don't use tabs except for indent"),
254 255 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
255 256 (r'.{81}', "line too long"),
256 257 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
257 258 (r'return\(', "return is not a function"),
258 259 (r' ;', "no space before ;"),
259 260 (r'\w+\* \w+', "use int *foo, not int* foo"),
260 261 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
261 262 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
262 263 (r'\w,\w', "missing whitespace after ,"),
263 264 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
264 265 (r'^#\s+\w', "use #foo, not # foo"),
265 266 (r'[^\n]\Z', "no trailing newline"),
266 267 (r'^\s*#import\b', "use only #include in standard C code"),
267 268 ],
268 269 # warnings
269 270 []
270 271 ]
271 272
272 273 cfilters = [
273 274 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
274 275 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
275 276 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
276 277 (r'(\()([^)]+\))', repcallspaces),
277 278 ]
278 279
279 280 inutilpats = [
280 281 [
281 282 (r'\bui\.', "don't use ui in util"),
282 283 ],
283 284 # warnings
284 285 []
285 286 ]
286 287
287 288 inrevlogpats = [
288 289 [
289 290 (r'\brepo\.', "don't use repo in revlog"),
290 291 ],
291 292 # warnings
292 293 []
293 294 ]
294 295
295 296 checks = [
296 297 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
297 298 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
298 299 ('c', r'.*\.c$', cfilters, cpats),
299 300 ('unified test', r'.*\.t$', utestfilters, utestpats),
300 301 ('layering violation repo in revlog', r'mercurial/revlog\.py', pyfilters,
301 302 inrevlogpats),
302 303 ('layering violation ui in util', r'mercurial/util\.py', pyfilters,
303 304 inutilpats),
304 305 ('txt', r'.*\.txt$', txtfilters, txtpats),
305 306 ]
306 307
307 308 class norepeatlogger(object):
308 309 def __init__(self):
309 310 self._lastseen = None
310 311
311 312 def log(self, fname, lineno, line, msg, blame):
312 313 """print error related a to given line of a given file.
313 314
314 315 The faulty line will also be printed but only once in the case
315 316 of multiple errors.
316 317
317 318 :fname: filename
318 319 :lineno: line number
319 320 :line: actual content of the line
320 321 :msg: error message
321 322 """
322 323 msgid = fname, lineno, line
323 324 if msgid != self._lastseen:
324 325 if blame:
325 326 print "%s:%d (%s):" % (fname, lineno, blame)
326 327 else:
327 328 print "%s:%d:" % (fname, lineno)
328 329 print " > %s" % line
329 330 self._lastseen = msgid
330 331 print " " + msg
331 332
332 333 _defaultlogger = norepeatlogger()
333 334
334 335 def getblame(f):
335 336 lines = []
336 337 for l in os.popen('hg annotate -un %s' % f):
337 338 start, line = l.split(':', 1)
338 339 user, rev = start.split()
339 340 lines.append((line[1:-1], user, rev))
340 341 return lines
341 342
342 343 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
343 344 blame=False, debug=False, lineno=True):
344 345 """checks style and portability of a given file
345 346
346 347 :f: filepath
347 348 :logfunc: function used to report error
348 349 logfunc(filename, linenumber, linecontent, errormessage)
349 350 :maxerr: number of error to display before aborting.
350 351 Set to false (default) to report all errors
351 352
352 353 return True if no error is found, False otherwise.
353 354 """
354 355 blamecache = None
355 356 result = True
356 357 for name, match, filters, pats in checks:
357 358 if debug:
358 359 print name, f
359 360 fc = 0
360 361 if not re.match(match, f):
361 362 if debug:
362 363 print "Skipping %s for %s it doesn't match %s" % (
363 364 name, match, f)
364 365 continue
365 366 fp = open(f)
366 367 pre = post = fp.read()
367 368 fp.close()
368 369 if "no-" + "check-code" in pre:
369 370 if debug:
370 371 print "Skipping %s for %s it has no- and check-code" % (
371 372 name, f)
372 373 break
373 374 for p, r in filters:
374 375 post = re.sub(p, r, post)
375 376 if warnings:
376 377 pats = pats[0] + pats[1]
377 378 else:
378 379 pats = pats[0]
379 380 # print post # uncomment to show filtered version
380 381
381 382 if debug:
382 383 print "Checking %s for %s" % (name, f)
383 384
384 385 prelines = None
385 386 errors = []
386 387 for pat in pats:
387 388 if len(pat) == 3:
388 389 p, msg, ignore = pat
389 390 else:
390 391 p, msg = pat
391 392 ignore = None
392 393
393 394 # fix-up regexes for multi-line searches
394 395 po = p
395 396 # \s doesn't match \n
396 397 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
397 398 # [^...] doesn't match newline
398 399 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
399 400
400 401 #print po, '=>', p
401 402
402 403 pos = 0
403 404 n = 0
404 405 for m in re.finditer(p, post, re.MULTILINE):
405 406 if prelines is None:
406 407 prelines = pre.splitlines()
407 408 postlines = post.splitlines(True)
408 409
409 410 start = m.start()
410 411 while n < len(postlines):
411 412 step = len(postlines[n])
412 413 if pos + step > start:
413 414 break
414 415 pos += step
415 416 n += 1
416 417 l = prelines[n]
417 418
418 419 if "check-code" + "-ignore" in l:
419 420 if debug:
420 421 print "Skipping %s for %s:%s (check-code -ignore)" % (
421 422 name, f, n)
422 423 continue
423 424 elif ignore and re.search(ignore, l, re.MULTILINE):
424 425 continue
425 426 bd = ""
426 427 if blame:
427 428 bd = 'working directory'
428 429 if not blamecache:
429 430 blamecache = getblame(f)
430 431 if n < len(blamecache):
431 432 bl, bu, br = blamecache[n]
432 433 if bl == l:
433 434 bd = '%s@%s' % (bu, br)
434 435 errors.append((f, lineno and n + 1, l, msg, bd))
435 436 result = False
436 437
437 438 errors.sort()
438 439 for e in errors:
439 440 logfunc(*e)
440 441 fc += 1
441 442 if maxerr and fc >= maxerr:
442 443 print " (too many errors, giving up)"
443 444 break
444 445
445 446 return result
446 447
447 448 if __name__ == "__main__":
448 449 parser = optparse.OptionParser("%prog [options] [files]")
449 450 parser.add_option("-w", "--warnings", action="store_true",
450 451 help="include warning-level checks")
451 452 parser.add_option("-p", "--per-file", type="int",
452 453 help="max warnings per file")
453 454 parser.add_option("-b", "--blame", action="store_true",
454 455 help="use annotate to generate blame info")
455 456 parser.add_option("", "--debug", action="store_true",
456 457 help="show debug information")
457 458 parser.add_option("", "--nolineno", action="store_false",
458 459 dest='lineno', help="don't show line numbers")
459 460
460 461 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
461 462 lineno=True)
462 463 (options, args) = parser.parse_args()
463 464
464 465 if len(args) == 0:
465 466 check = glob.glob("*")
466 467 else:
467 468 check = args
468 469
469 470 ret = 0
470 471 for f in check:
471 472 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
472 473 blame=options.blame, debug=options.debug,
473 474 lineno=options.lineno):
474 475 ret = 1
475 476 sys.exit(ret)
General Comments 0
You need to be logged in to leave comments. Login now