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