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