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