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