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