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