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