##// END OF EJS Templates
check-code: stop forbidding return code result...
marmoute -
r48293:9b1710c5 default
parent child Browse files
Show More
@@ -1,1109 +1,1108 b''
1 1 #!/usr/bin/env python3
2 2 #
3 3 # check-code - a style and portability checker for Mercurial
4 4 #
5 5 # Copyright 2010 Olivia Mackall <olivia@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 """style and portability checker for Mercurial
11 11
12 12 when a rule triggers wrong, do one of the following (prefer one from top):
13 13 * do the work-around the rule suggests
14 14 * doublecheck that it is a false match
15 15 * improve the rule pattern
16 16 * add an ignore pattern to the rule (3rd arg) which matches your good line
17 17 (you can append a short comment and match this, like: #re-raises)
18 18 * change the pattern to a warning and list the exception in test-check-code-hg
19 19 * ONLY use no--check-code for skipping entire files from external sources
20 20 """
21 21
22 22 from __future__ import absolute_import, print_function
23 23 import glob
24 24 import keyword
25 25 import optparse
26 26 import os
27 27 import re
28 28 import sys
29 29
30 30 if sys.version_info[0] < 3:
31 31 opentext = open
32 32 else:
33 33
34 34 def opentext(f):
35 35 return open(f, encoding='latin1')
36 36
37 37
38 38 try:
39 39 xrange
40 40 except NameError:
41 41 xrange = range
42 42 try:
43 43 import re2
44 44 except ImportError:
45 45 re2 = None
46 46
47 47 import testparseutil
48 48
49 49
50 50 def compilere(pat, multiline=False):
51 51 if multiline:
52 52 pat = '(?m)' + pat
53 53 if re2:
54 54 try:
55 55 return re2.compile(pat)
56 56 except re2.error:
57 57 pass
58 58 return re.compile(pat)
59 59
60 60
61 61 # check "rules depending on implementation of repquote()" in each
62 62 # patterns (especially pypats), before changing around repquote()
63 63 _repquotefixedmap = {
64 64 ' ': ' ',
65 65 '\n': '\n',
66 66 '.': 'p',
67 67 ':': 'q',
68 68 '%': '%',
69 69 '\\': 'b',
70 70 '*': 'A',
71 71 '+': 'P',
72 72 '-': 'M',
73 73 }
74 74
75 75
76 76 def _repquoteencodechr(i):
77 77 if i > 255:
78 78 return 'u'
79 79 c = chr(i)
80 80 if c in _repquotefixedmap:
81 81 return _repquotefixedmap[c]
82 82 if c.isalpha():
83 83 return 'x'
84 84 if c.isdigit():
85 85 return 'n'
86 86 return 'o'
87 87
88 88
89 89 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
90 90
91 91
92 92 def repquote(m):
93 93 t = m.group('text')
94 94 t = t.translate(_repquotett)
95 95 return m.group('quote') + t + m.group('quote')
96 96
97 97
98 98 def reppython(m):
99 99 comment = m.group('comment')
100 100 if comment:
101 101 l = len(comment.rstrip())
102 102 return "#" * l + comment[l:]
103 103 return repquote(m)
104 104
105 105
106 106 def repcomment(m):
107 107 return m.group(1) + "#" * len(m.group(2))
108 108
109 109
110 110 def repccomment(m):
111 111 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
112 112 return m.group(1) + t + "*/"
113 113
114 114
115 115 def repcallspaces(m):
116 116 t = re.sub(r"\n\s+", "\n", m.group(2))
117 117 return m.group(1) + t
118 118
119 119
120 120 def repinclude(m):
121 121 return m.group(1) + "<foo>"
122 122
123 123
124 124 def rephere(m):
125 125 t = re.sub(r"\S", "x", m.group(2))
126 126 return m.group(1) + t
127 127
128 128
129 129 testpats = [
130 130 [
131 131 (r'\b(push|pop)d\b', "don't use 'pushd' or 'popd', use 'cd'"),
132 132 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
133 133 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
134 134 (r'(?<!hg )grep.* -a', "don't use 'grep -a', use in-line python"),
135 135 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
136 136 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
137 137 (r'echo -n', "don't use 'echo -n', use printf"),
138 138 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
139 139 (r'head -c', "don't use 'head -c', use 'dd'"),
140 140 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
141 141 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
142 142 (r'\bls\b.*-\w*R', "don't use 'ls -R', use 'find'"),
143 143 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
144 144 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
145 145 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
146 146 (
147 147 r'\[[^\]]+==',
148 148 '[ foo == bar ] is a bashism, use [ foo = bar ] instead',
149 149 ),
150 150 (
151 151 r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
152 152 "use egrep for extended grep syntax",
153 153 ),
154 154 (r'(^|\|\s*)e?grep .*\\S', "don't use \\S in regular expression"),
155 155 (r'(?<!!)/bin/', "don't use explicit paths for tools"),
156 156 (r'#!.*/bash', "don't use bash in shebang, use sh"),
157 157 (r'[^\n]\Z', "no trailing newline"),
158 158 (r'export .*=', "don't export and assign at once"),
159 159 (r'^source\b', "don't use 'source', use '.'"),
160 160 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
161 161 (r'\bls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
162 162 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
163 163 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
164 164 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
165 165 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
166 166 (r'^alias\b.*=', "don't use alias, use a function"),
167 167 (r'if\s*!', "don't use '!' to negate exit status"),
168 168 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
169 169 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
170 170 (
171 171 r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
172 172 "put a backslash-escaped newline after sed 'i' command",
173 173 ),
174 174 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
175 175 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
176 176 (r'[\s="`\']python\s(?!bindings)', "don't use 'python', use '$PYTHON'"),
177 177 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
178 178 (r'\butil\.Abort\b', "directly use error.Abort"),
179 179 (r'\|&', "don't use |&, use 2>&1"),
180 180 (r'\w = +\w', "only one space after = allowed"),
181 181 (
182 182 r'\bsed\b.*[^\\]\\n',
183 183 "don't use 'sed ... \\n', use a \\ and a newline",
184 184 ),
185 185 (r'env.*-u', "don't use 'env -u VAR', use 'unset VAR'"),
186 186 (r'cp.* -r ', "don't use 'cp -r', use 'cp -R'"),
187 187 (r'grep.* -[ABC]', "don't use grep's context flags"),
188 188 (
189 189 r'find.*-printf',
190 190 "don't use 'find -printf', it doesn't exist on BSD find(1)",
191 191 ),
192 192 (r'\$RANDOM ', "don't use bash-only $RANDOM to generate random values"),
193 193 ],
194 194 # warnings
195 195 [
196 196 (r'^function', "don't use 'function', use old style"),
197 197 (r'^diff.*-\w*N', "don't use 'diff -N'"),
198 198 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
199 199 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
200 200 (r'kill (`|\$\()', "don't use kill, use killdaemons.py"),
201 201 ],
202 202 ]
203 203
204 204 testfilters = [
205 205 (r"( *)(#([^!][^\n]*\S)?)", repcomment),
206 206 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
207 207 ]
208 208
209 209 uprefix = r"^ \$ "
210 210 utestpats = [
211 211 [
212 212 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
213 213 (
214 214 uprefix + r'.*\|\s*sed[^|>\n]*\n',
215 215 "use regex test output patterns instead of sed",
216 216 ),
217 217 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
218 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
219 218 (
220 219 uprefix + r'.*\|\| echo.*(fail|error)',
221 220 "explicit exit code checks unnecessary",
222 221 ),
223 222 (uprefix + r'set -e', "don't use set -e"),
224 223 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
225 224 (
226 225 uprefix + r'.*:\.\S*/',
227 226 "x:.y in a path does not work on msys, rewrite "
228 227 "as x://.y, or see `hg log -k msys` for alternatives",
229 228 r'-\S+:\.|' '# no-msys', # -Rxxx
230 229 ), # in test-pull.t which is skipped on windows
231 230 (
232 231 r'^ [^$>].*27\.0\.0\.1',
233 232 'use $LOCALIP not an explicit loopback address',
234 233 ),
235 234 (
236 235 r'^ (?![>$] ).*\$LOCALIP.*[^)]$',
237 236 'mark $LOCALIP output lines with (glob) to help tests in BSD jails',
238 237 ),
239 238 (
240 239 r'^ (cat|find): .*: \$ENOENT\$',
241 240 'use test -f to test for file existence',
242 241 ),
243 242 (
244 243 r'^ diff -[^ -]*p',
245 244 "don't use (external) diff with -p for portability",
246 245 ),
247 246 (r' readlink ', 'use readlink.py instead of readlink'),
248 247 (
249 248 r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
250 249 "glob timezone field in diff output for portability",
251 250 ),
252 251 (
253 252 r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
254 253 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability",
255 254 ),
256 255 (
257 256 r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
258 257 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability",
259 258 ),
260 259 (
261 260 r'^ @@ -[0-9]+ [+][0-9]+ @@',
262 261 "use '@@ -N* +N* @@ (glob)' style chunk header for portability",
263 262 ),
264 263 (
265 264 uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
266 265 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
267 266 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)",
268 267 ),
269 268 ],
270 269 # warnings
271 270 [
272 271 (
273 272 r'^ (?!.*\$LOCALIP)[^*?/\n]* \(glob\)$',
274 273 "glob match with no glob string (?, *, /, and $LOCALIP)",
275 274 ),
276 275 ],
277 276 ]
278 277
279 278 # transform plain test rules to unified test's
280 279 for i in [0, 1]:
281 280 for tp in testpats[i]:
282 281 p = tp[0]
283 282 m = tp[1]
284 283 if p.startswith('^'):
285 284 p = "^ [$>] (%s)" % p[1:]
286 285 else:
287 286 p = "^ [$>] .*(%s)" % p
288 287 utestpats[i].append((p, m) + tp[2:])
289 288
290 289 # don't transform the following rules:
291 290 # " > \t" and " \t" should be allowed in unified tests
292 291 testpats[0].append((r'^( *)\t', "don't use tabs to indent"))
293 292 utestpats[0].append((r'^( ?)\t', "don't use tabs to indent"))
294 293
295 294 utestfilters = [
296 295 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
297 296 (r"( +)(#([^!][^\n]*\S)?)", repcomment),
298 297 ]
299 298
300 299 # common patterns to check *.py
301 300 commonpypats = [
302 301 [
303 302 (r'\\$', 'Use () to wrap long lines in Python, not \\'),
304 303 (
305 304 r'^\s*def\s*\w+\s*\(.*,\s*\(',
306 305 "tuple parameter unpacking not available in Python 3+",
307 306 ),
308 307 (
309 308 r'lambda\s*\(.*,.*\)',
310 309 "tuple parameter unpacking not available in Python 3+",
311 310 ),
312 311 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
313 312 (r'(?<!\.)\breduce\s*\(.*', "reduce is not available in Python 3+"),
314 313 (
315 314 r'\bdict\(.*=',
316 315 'dict() is different in Py2 and 3 and is slower than {}',
317 316 'dict-from-generator',
318 317 ),
319 318 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
320 319 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
321 320 (r'^\s*\t', "don't use tabs"),
322 321 (r'\S;\s*\n', "semicolon"),
323 322 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
324 323 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
325 324 (r'(\w|\)),\w', "missing whitespace after ,"),
326 325 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
327 326 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
328 327 (
329 328 (
330 329 # a line ending with a colon, potentially with trailing comments
331 330 r':([ \t]*#[^\n]*)?\n'
332 331 # one that is not a pass and not only a comment
333 332 r'(?P<indent>[ \t]+)[^#][^\n]+\n'
334 333 # more lines at the same indent level
335 334 r'((?P=indent)[^\n]+\n)*'
336 335 # a pass at the same indent level, which is bogus
337 336 r'(?P=indent)pass[ \t\n#]'
338 337 ),
339 338 'omit superfluous pass',
340 339 ),
341 340 (r'[^\n]\Z', "no trailing newline"),
342 341 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
343 342 (
344 343 r'^\s+(self\.)?[A-Za-z][a-z0-9]+[A-Z]\w* = ',
345 344 "don't use camelcase in identifiers",
346 345 r'#.*camelcase-required',
347 346 ),
348 347 (
349 348 r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
350 349 "linebreak after :",
351 350 ),
352 351 (
353 352 r'class\s[^( \n]+:',
354 353 "old-style class, use class foo(object)",
355 354 r'#.*old-style',
356 355 ),
357 356 (
358 357 r'class\s[^( \n]+\(\):',
359 358 "class foo() creates old style object, use class foo(object)",
360 359 r'#.*old-style',
361 360 ),
362 361 (
363 362 r'\b(%s)\('
364 363 % '|'.join(k for k in keyword.kwlist if k not in ('print', 'exec')),
365 364 "Python keyword is not a function",
366 365 ),
367 366 # (r'class\s[A-Z][^\(]*\((?!Exception)',
368 367 # "don't capitalize non-exception classes"),
369 368 # (r'in range\(', "use xrange"),
370 369 # (r'^\s*print\s+', "avoid using print in core and extensions"),
371 370 (r'[\x80-\xff]', "non-ASCII character literal"),
372 371 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
373 372 (
374 373 r'([\(\[][ \t]\S)|(\S[ \t][\)\]])',
375 374 "gratuitous whitespace in () or []",
376 375 ),
377 376 # (r'\s\s=', "gratuitous whitespace before ="),
378 377 (
379 378 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
380 379 "missing whitespace around operator",
381 380 ),
382 381 (
383 382 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
384 383 "missing whitespace around operator",
385 384 ),
386 385 (
387 386 r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
388 387 "missing whitespace around operator",
389 388 ),
390 389 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]', "wrong whitespace around ="),
391 390 (
392 391 r'\([^()]*( =[^=]|[^<>!=]= )',
393 392 "no whitespace around = for named parameters",
394 393 ),
395 394 (
396 395 r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
397 396 "don't use old-style two-argument raise, use Exception(message)",
398 397 ),
399 398 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
400 399 (
401 400 r' [=!]=\s+(True|False|None)',
402 401 "comparison with singleton, use 'is' or 'is not' instead",
403 402 ),
404 403 (
405 404 r'^\s*(while|if) [01]:',
406 405 "use True/False for constant Boolean expression",
407 406 ),
408 407 (r'^\s*if False(:| +and)', 'Remove code instead of using `if False`'),
409 408 (
410 409 r'(?:(?<!def)\s+|\()hasattr\(',
411 410 'hasattr(foo, bar) is broken on py2, use util.safehasattr(foo, bar) '
412 411 'instead',
413 412 r'#.*hasattr-py3-only',
414 413 ),
415 414 (r'opener\([^)]*\).read\(', "use opener.read() instead"),
416 415 (r'opener\([^)]*\).write\(', "use opener.write() instead"),
417 416 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
418 417 (r'\.debug\(\_', "don't mark debug messages for translation"),
419 418 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
420 419 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
421 420 (
422 421 r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
423 422 'legacy exception syntax; use "as" instead of ","',
424 423 ),
425 424 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
426 425 (r'\bdef\s+__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
427 426 (
428 427 r'os\.path\.join\(.*, *(""|\'\')\)',
429 428 "use pathutil.normasprefix(path) instead of os.path.join(path, '')",
430 429 ),
431 430 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
432 431 # XXX only catch mutable arguments on the first line of the definition
433 432 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
434 433 (r'\butil\.Abort\b', "directly use error.Abort"),
435 434 (
436 435 r'^@(\w*\.)?cachefunc',
437 436 "module-level @cachefunc is risky, please avoid",
438 437 ),
439 438 (
440 439 r'^import Queue',
441 440 "don't use Queue, use pycompat.queue.Queue + "
442 441 "pycompat.queue.Empty",
443 442 ),
444 443 (
445 444 r'^import cStringIO',
446 445 "don't use cStringIO.StringIO, use util.stringio",
447 446 ),
448 447 (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
449 448 (
450 449 r'^import SocketServer',
451 450 "don't use SockerServer, use util.socketserver",
452 451 ),
453 452 (r'^import urlparse', "don't use urlparse, use util.urlreq"),
454 453 (r'^import xmlrpclib', "don't use xmlrpclib, use util.xmlrpclib"),
455 454 (r'^import cPickle', "don't use cPickle, use util.pickle"),
456 455 (r'^import pickle', "don't use pickle, use util.pickle"),
457 456 (r'^import httplib', "don't use httplib, use util.httplib"),
458 457 (r'^import BaseHTTPServer', "use util.httpserver instead"),
459 458 (
460 459 r'^(from|import) mercurial\.(cext|pure|cffi)',
461 460 "use mercurial.policy.importmod instead",
462 461 ),
463 462 (r'\.next\(\)', "don't use .next(), use next(...)"),
464 463 (
465 464 r'([a-z]*).revision\(\1\.node\(',
466 465 "don't convert rev to node before passing to revision(nodeorrev)",
467 466 ),
468 467 (r'platform\.system\(\)', "don't use platform.system(), use pycompat"),
469 468 ],
470 469 # warnings
471 470 [],
472 471 ]
473 472
474 473 # patterns to check normal *.py files
475 474 pypats = [
476 475 [
477 476 # Ideally, these should be placed in "commonpypats" for
478 477 # consistency of coding rules in Mercurial source tree.
479 478 # But on the other hand, these are not so seriously required for
480 479 # python code fragments embedded in test scripts. Fixing test
481 480 # scripts for these patterns requires many changes, and has less
482 481 # profit than effort.
483 482 (r'raise Exception', "don't raise generic exceptions"),
484 483 (r'[\s\(](open|file)\([^)]*\)\.read\(', "use util.readfile() instead"),
485 484 (
486 485 r'[\s\(](open|file)\([^)]*\)\.write\(',
487 486 "use util.writefile() instead",
488 487 ),
489 488 (
490 489 r'^[\s\(]*(open(er)?|file)\([^)]*\)(?!\.close\(\))',
491 490 "always assign an opened file to a variable, and close it afterwards",
492 491 ),
493 492 (
494 493 r'[\s\(](open|file)\([^)]*\)\.(?!close\(\))',
495 494 "always assign an opened file to a variable, and close it afterwards",
496 495 ),
497 496 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
498 497 (r'^import atexit', "don't use atexit, use ui.atexit"),
499 498 # rules depending on implementation of repquote()
500 499 (
501 500 r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
502 501 'string join across lines with no space',
503 502 ),
504 503 (
505 504 r'''(?x)ui\.(status|progress|write|note|warn)\(
506 505 [ \t\n#]*
507 506 (?# any strings/comments might precede a string, which
508 507 # contains translatable message)
509 508 b?((['"]|\'\'\'|""")[ \npq%bAPMxno]*(['"]|\'\'\'|""")[ \t\n#]+)*
510 509 (?# sequence consisting of below might precede translatable message
511 510 # - formatting string: "% 10s", "%05d", "% -3.2f", "%*s", "%%" ...
512 511 # - escaped character: "\\", "\n", "\0" ...
513 512 # - character other than '%', 'b' as '\', and 'x' as alphabet)
514 513 (['"]|\'\'\'|""")
515 514 ((%([ n]?[PM]?([np]+|A))?x)|%%|b[bnx]|[ \nnpqAPMo])*x
516 515 (?# this regexp can't use [^...] style,
517 516 # because _preparepats forcibly adds "\n" into [^...],
518 517 # even though this regexp wants match it against "\n")''',
519 518 "missing _() in ui message (use () to hide false-positives)",
520 519 ),
521 520 ]
522 521 + commonpypats[0],
523 522 # warnings
524 523 [
525 524 # rules depending on implementation of repquote()
526 525 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
527 526 ]
528 527 + commonpypats[1],
529 528 ]
530 529
531 530 # patterns to check *.py for embedded ones in test script
532 531 embeddedpypats = [
533 532 [] + commonpypats[0],
534 533 # warnings
535 534 [] + commonpypats[1],
536 535 ]
537 536
538 537 # common filters to convert *.py
539 538 commonpyfilters = [
540 539 (
541 540 r"""(?msx)(?P<comment>\#.*?$)|
542 541 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
543 542 (?P<text>(([^\\]|\\.)*?))
544 543 (?P=quote))""",
545 544 reppython,
546 545 ),
547 546 ]
548 547
549 548 # filters to convert normal *.py files
550 549 pyfilters = [] + commonpyfilters
551 550
552 551 # non-filter patterns
553 552 pynfpats = [
554 553 [
555 554 (r'pycompat\.osname\s*[=!]=\s*[\'"]nt[\'"]', "use pycompat.iswindows"),
556 555 (r'pycompat\.osname\s*[=!]=\s*[\'"]posix[\'"]', "use pycompat.isposix"),
557 556 (
558 557 r'pycompat\.sysplatform\s*[!=]=\s*[\'"]darwin[\'"]',
559 558 "use pycompat.isdarwin",
560 559 ),
561 560 ],
562 561 # warnings
563 562 [],
564 563 ]
565 564
566 565 # filters to convert *.py for embedded ones in test script
567 566 embeddedpyfilters = [] + commonpyfilters
568 567
569 568 # extension non-filter patterns
570 569 pyextnfpats = [
571 570 [(r'^"""\n?[A-Z]', "don't capitalize docstring title")],
572 571 # warnings
573 572 [],
574 573 ]
575 574
576 575 txtfilters = []
577 576
578 577 txtpats = [
579 578 [
580 579 (r'\s$', 'trailing whitespace'),
581 580 ('.. note::[ \n][^\n]', 'add two newlines after note::'),
582 581 ],
583 582 [],
584 583 ]
585 584
586 585 cpats = [
587 586 [
588 587 (r'//', "don't use //-style comments"),
589 588 (r'\S\t', "don't use tabs except for indent"),
590 589 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
591 590 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
592 591 (r'return\(', "return is not a function"),
593 592 (r' ;', "no space before ;"),
594 593 (r'[^;] \)', "no space before )"),
595 594 (r'[)][{]', "space between ) and {"),
596 595 (r'\w+\* \w+', "use int *foo, not int* foo"),
597 596 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
598 597 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
599 598 (r'\w,\w', "missing whitespace after ,"),
600 599 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
601 600 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
602 601 (r'^#\s+\w', "use #foo, not # foo"),
603 602 (r'[^\n]\Z', "no trailing newline"),
604 603 (r'^\s*#import\b', "use only #include in standard C code"),
605 604 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
606 605 (r'strcat\(', "don't use strcat"),
607 606 # rules depending on implementation of repquote()
608 607 ],
609 608 # warnings
610 609 [
611 610 # rules depending on implementation of repquote()
612 611 ],
613 612 ]
614 613
615 614 cfilters = [
616 615 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
617 616 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
618 617 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
619 618 (r'(\()([^)]+\))', repcallspaces),
620 619 ]
621 620
622 621 inutilpats = [
623 622 [
624 623 (r'\bui\.', "don't use ui in util"),
625 624 ],
626 625 # warnings
627 626 [],
628 627 ]
629 628
630 629 inrevlogpats = [
631 630 [
632 631 (r'\brepo\.', "don't use repo in revlog"),
633 632 ],
634 633 # warnings
635 634 [],
636 635 ]
637 636
638 637 webtemplatefilters = []
639 638
640 639 webtemplatepats = [
641 640 [],
642 641 [
643 642 (
644 643 r'{desc(\|(?!websub|firstline)[^\|]*)+}',
645 644 'follow desc keyword with either firstline or websub',
646 645 ),
647 646 ],
648 647 ]
649 648
650 649 allfilesfilters = []
651 650
652 651 allfilespats = [
653 652 [
654 653 (
655 654 r'(http|https)://[a-zA-Z0-9./]*selenic.com/',
656 655 'use mercurial-scm.org domain URL',
657 656 ),
658 657 (
659 658 r'mercurial@selenic\.com',
660 659 'use mercurial-scm.org domain for mercurial ML address',
661 660 ),
662 661 (
663 662 r'mercurial-devel@selenic\.com',
664 663 'use mercurial-scm.org domain for mercurial-devel ML address',
665 664 ),
666 665 ],
667 666 # warnings
668 667 [],
669 668 ]
670 669
671 670 py3pats = [
672 671 [
673 672 (
674 673 r'os\.environ',
675 674 "use encoding.environ instead (py3)",
676 675 r'#.*re-exports',
677 676 ),
678 677 (r'os\.name', "use pycompat.osname instead (py3)"),
679 678 (r'os\.getcwd', "use encoding.getcwd instead (py3)", r'#.*re-exports'),
680 679 (r'os\.sep', "use pycompat.ossep instead (py3)"),
681 680 (r'os\.pathsep', "use pycompat.ospathsep instead (py3)"),
682 681 (r'os\.altsep', "use pycompat.osaltsep instead (py3)"),
683 682 (r'sys\.platform', "use pycompat.sysplatform instead (py3)"),
684 683 (r'getopt\.getopt', "use pycompat.getoptb instead (py3)"),
685 684 (r'os\.getenv', "use encoding.environ.get instead"),
686 685 (r'os\.setenv', "modifying the environ dict is not preferred"),
687 686 (r'(?<!pycompat\.)xrange', "use pycompat.xrange instead (py3)"),
688 687 ],
689 688 # warnings
690 689 [],
691 690 ]
692 691
693 692 checks = [
694 693 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
695 694 ('python', r'.*\.(py|cgi)$', r'^#!.*python', [], pynfpats),
696 695 ('python', r'.*hgext.*\.py$', '', [], pyextnfpats),
697 696 (
698 697 'python 3',
699 698 r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
700 699 '',
701 700 pyfilters,
702 701 py3pats,
703 702 ),
704 703 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
705 704 ('c', r'.*\.[ch]$', '', cfilters, cpats),
706 705 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
707 706 (
708 707 'layering violation repo in revlog',
709 708 r'mercurial/revlog\.py',
710 709 '',
711 710 pyfilters,
712 711 inrevlogpats,
713 712 ),
714 713 (
715 714 'layering violation ui in util',
716 715 r'mercurial/util\.py',
717 716 '',
718 717 pyfilters,
719 718 inutilpats,
720 719 ),
721 720 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
722 721 (
723 722 'web template',
724 723 r'mercurial/templates/.*\.tmpl',
725 724 '',
726 725 webtemplatefilters,
727 726 webtemplatepats,
728 727 ),
729 728 ('all except for .po', r'.*(?<!\.po)$', '', allfilesfilters, allfilespats),
730 729 ]
731 730
732 731 # (desc,
733 732 # func to pick up embedded code fragments,
734 733 # list of patterns to convert target files
735 734 # list of patterns to detect errors/warnings)
736 735 embeddedchecks = [
737 736 (
738 737 'embedded python',
739 738 testparseutil.pyembedded,
740 739 embeddedpyfilters,
741 740 embeddedpypats,
742 741 )
743 742 ]
744 743
745 744
746 745 def _preparepats():
747 746 def preparefailandwarn(failandwarn):
748 747 for pats in failandwarn:
749 748 for i, pseq in enumerate(pats):
750 749 # fix-up regexes for multi-line searches
751 750 p = pseq[0]
752 751 # \s doesn't match \n (done in two steps)
753 752 # first, we replace \s that appears in a set already
754 753 p = re.sub(r'\[\\s', r'[ \\t', p)
755 754 # now we replace other \s instances.
756 755 p = re.sub(r'(?<!(\\|\[))\\s', r'[ \\t]', p)
757 756 # [^...] doesn't match newline
758 757 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
759 758
760 759 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
761 760
762 761 def preparefilters(filters):
763 762 for i, flt in enumerate(filters):
764 763 filters[i] = re.compile(flt[0]), flt[1]
765 764
766 765 for cs in (checks, embeddedchecks):
767 766 for c in cs:
768 767 failandwarn = c[-1]
769 768 preparefailandwarn(failandwarn)
770 769
771 770 filters = c[-2]
772 771 preparefilters(filters)
773 772
774 773
775 774 class norepeatlogger(object):
776 775 def __init__(self):
777 776 self._lastseen = None
778 777
779 778 def log(self, fname, lineno, line, msg, blame):
780 779 """print error related a to given line of a given file.
781 780
782 781 The faulty line will also be printed but only once in the case
783 782 of multiple errors.
784 783
785 784 :fname: filename
786 785 :lineno: line number
787 786 :line: actual content of the line
788 787 :msg: error message
789 788 """
790 789 msgid = fname, lineno, line
791 790 if msgid != self._lastseen:
792 791 if blame:
793 792 print("%s:%d (%s):" % (fname, lineno, blame))
794 793 else:
795 794 print("%s:%d:" % (fname, lineno))
796 795 print(" > %s" % line)
797 796 self._lastseen = msgid
798 797 print(" " + msg)
799 798
800 799
801 800 _defaultlogger = norepeatlogger()
802 801
803 802
804 803 def getblame(f):
805 804 lines = []
806 805 for l in os.popen('hg annotate -un %s' % f):
807 806 start, line = l.split(':', 1)
808 807 user, rev = start.split()
809 808 lines.append((line[1:-1], user, rev))
810 809 return lines
811 810
812 811
813 812 def checkfile(
814 813 f,
815 814 logfunc=_defaultlogger.log,
816 815 maxerr=None,
817 816 warnings=False,
818 817 blame=False,
819 818 debug=False,
820 819 lineno=True,
821 820 ):
822 821 """checks style and portability of a given file
823 822
824 823 :f: filepath
825 824 :logfunc: function used to report error
826 825 logfunc(filename, linenumber, linecontent, errormessage)
827 826 :maxerr: number of error to display before aborting.
828 827 Set to false (default) to report all errors
829 828
830 829 return True if no error is found, False otherwise.
831 830 """
832 831 result = True
833 832
834 833 try:
835 834 with opentext(f) as fp:
836 835 try:
837 836 pre = fp.read()
838 837 except UnicodeDecodeError as e:
839 838 print("%s while reading %s" % (e, f))
840 839 return result
841 840 except IOError as e:
842 841 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
843 842 return result
844 843
845 844 # context information shared while single checkfile() invocation
846 845 context = {'blamecache': None}
847 846
848 847 for name, match, magic, filters, pats in checks:
849 848 if debug:
850 849 print(name, f)
851 850 if not (re.match(match, f) or (magic and re.search(magic, pre))):
852 851 if debug:
853 852 print(
854 853 "Skipping %s for %s it doesn't match %s" % (name, match, f)
855 854 )
856 855 continue
857 856 if "no-" "check-code" in pre:
858 857 # If you're looking at this line, it's because a file has:
859 858 # no- check- code
860 859 # but the reason to output skipping is to make life for
861 860 # tests easier. So, instead of writing it with a normal
862 861 # spelling, we write it with the expected spelling from
863 862 # tests/test-check-code.t
864 863 print("Skipping %s it has no-che?k-code (glob)" % f)
865 864 return "Skip" # skip checking this file
866 865
867 866 fc = _checkfiledata(
868 867 name,
869 868 f,
870 869 pre,
871 870 filters,
872 871 pats,
873 872 context,
874 873 logfunc,
875 874 maxerr,
876 875 warnings,
877 876 blame,
878 877 debug,
879 878 lineno,
880 879 )
881 880 if fc:
882 881 result = False
883 882
884 883 if f.endswith('.t') and "no-" "check-code" not in pre:
885 884 if debug:
886 885 print("Checking embedded code in %s" % f)
887 886
888 887 prelines = pre.splitlines()
889 888 embeddederros = []
890 889 for name, embedded, filters, pats in embeddedchecks:
891 890 # "reset curmax at each repetition" treats maxerr as "max
892 891 # nubmer of errors in an actual file per entry of
893 892 # (embedded)checks"
894 893 curmaxerr = maxerr
895 894
896 895 for found in embedded(f, prelines, embeddederros):
897 896 filename, starts, ends, code = found
898 897 fc = _checkfiledata(
899 898 name,
900 899 f,
901 900 code,
902 901 filters,
903 902 pats,
904 903 context,
905 904 logfunc,
906 905 curmaxerr,
907 906 warnings,
908 907 blame,
909 908 debug,
910 909 lineno,
911 910 offset=starts - 1,
912 911 )
913 912 if fc:
914 913 result = False
915 914 if curmaxerr:
916 915 if fc >= curmaxerr:
917 916 break
918 917 curmaxerr -= fc
919 918
920 919 return result
921 920
922 921
923 922 def _checkfiledata(
924 923 name,
925 924 f,
926 925 filedata,
927 926 filters,
928 927 pats,
929 928 context,
930 929 logfunc,
931 930 maxerr,
932 931 warnings,
933 932 blame,
934 933 debug,
935 934 lineno,
936 935 offset=None,
937 936 ):
938 937 """Execute actual error check for file data
939 938
940 939 :name: of the checking category
941 940 :f: filepath
942 941 :filedata: content of a file
943 942 :filters: to be applied before checking
944 943 :pats: to detect errors
945 944 :context: a dict of information shared while single checkfile() invocation
946 945 Valid keys: 'blamecache'.
947 946 :logfunc: function used to report error
948 947 logfunc(filename, linenumber, linecontent, errormessage)
949 948 :maxerr: number of error to display before aborting, or False to
950 949 report all errors
951 950 :warnings: whether warning level checks should be applied
952 951 :blame: whether blame information should be displayed at error reporting
953 952 :debug: whether debug information should be displayed
954 953 :lineno: whether lineno should be displayed at error reporting
955 954 :offset: line number offset of 'filedata' in 'f' for checking
956 955 an embedded code fragment, or None (offset=0 is different
957 956 from offset=None)
958 957
959 958 returns number of detected errors.
960 959 """
961 960 blamecache = context['blamecache']
962 961 if offset is None:
963 962 lineoffset = 0
964 963 else:
965 964 lineoffset = offset
966 965
967 966 fc = 0
968 967 pre = post = filedata
969 968
970 969 if True: # TODO: get rid of this redundant 'if' block
971 970 for p, r in filters:
972 971 post = re.sub(p, r, post)
973 972 nerrs = len(pats[0]) # nerr elements are errors
974 973 if warnings:
975 974 pats = pats[0] + pats[1]
976 975 else:
977 976 pats = pats[0]
978 977 # print post # uncomment to show filtered version
979 978
980 979 if debug:
981 980 print("Checking %s for %s" % (name, f))
982 981
983 982 prelines = None
984 983 errors = []
985 984 for i, pat in enumerate(pats):
986 985 if len(pat) == 3:
987 986 p, msg, ignore = pat
988 987 else:
989 988 p, msg = pat
990 989 ignore = None
991 990 if i >= nerrs:
992 991 msg = "warning: " + msg
993 992
994 993 pos = 0
995 994 n = 0
996 995 for m in p.finditer(post):
997 996 if prelines is None:
998 997 prelines = pre.splitlines()
999 998 postlines = post.splitlines(True)
1000 999
1001 1000 start = m.start()
1002 1001 while n < len(postlines):
1003 1002 step = len(postlines[n])
1004 1003 if pos + step > start:
1005 1004 break
1006 1005 pos += step
1007 1006 n += 1
1008 1007 l = prelines[n]
1009 1008
1010 1009 if ignore and re.search(ignore, l, re.MULTILINE):
1011 1010 if debug:
1012 1011 print(
1013 1012 "Skipping %s for %s:%s (ignore pattern)"
1014 1013 % (name, f, (n + lineoffset))
1015 1014 )
1016 1015 continue
1017 1016 bd = ""
1018 1017 if blame:
1019 1018 bd = 'working directory'
1020 1019 if blamecache is None:
1021 1020 blamecache = getblame(f)
1022 1021 context['blamecache'] = blamecache
1023 1022 if (n + lineoffset) < len(blamecache):
1024 1023 bl, bu, br = blamecache[(n + lineoffset)]
1025 1024 if offset is None and bl == l:
1026 1025 bd = '%s@%s' % (bu, br)
1027 1026 elif offset is not None and bl.endswith(l):
1028 1027 # "offset is not None" means "checking
1029 1028 # embedded code fragment". In this case,
1030 1029 # "l" does not have information about the
1031 1030 # beginning of an *original* line in the
1032 1031 # file (e.g. ' > ').
1033 1032 # Therefore, use "str.endswith()", and
1034 1033 # show "maybe" for a little loose
1035 1034 # examination.
1036 1035 bd = '%s@%s, maybe' % (bu, br)
1037 1036
1038 1037 errors.append((f, lineno and (n + lineoffset + 1), l, msg, bd))
1039 1038
1040 1039 errors.sort()
1041 1040 for e in errors:
1042 1041 logfunc(*e)
1043 1042 fc += 1
1044 1043 if maxerr and fc >= maxerr:
1045 1044 print(" (too many errors, giving up)")
1046 1045 break
1047 1046
1048 1047 return fc
1049 1048
1050 1049
1051 1050 def main():
1052 1051 parser = optparse.OptionParser("%prog [options] [files | -]")
1053 1052 parser.add_option(
1054 1053 "-w",
1055 1054 "--warnings",
1056 1055 action="store_true",
1057 1056 help="include warning-level checks",
1058 1057 )
1059 1058 parser.add_option(
1060 1059 "-p", "--per-file", type="int", help="max warnings per file"
1061 1060 )
1062 1061 parser.add_option(
1063 1062 "-b",
1064 1063 "--blame",
1065 1064 action="store_true",
1066 1065 help="use annotate to generate blame info",
1067 1066 )
1068 1067 parser.add_option(
1069 1068 "", "--debug", action="store_true", help="show debug information"
1070 1069 )
1071 1070 parser.add_option(
1072 1071 "",
1073 1072 "--nolineno",
1074 1073 action="store_false",
1075 1074 dest='lineno',
1076 1075 help="don't show line numbers",
1077 1076 )
1078 1077
1079 1078 parser.set_defaults(
1080 1079 per_file=15, warnings=False, blame=False, debug=False, lineno=True
1081 1080 )
1082 1081 (options, args) = parser.parse_args()
1083 1082
1084 1083 if len(args) == 0:
1085 1084 check = glob.glob("*")
1086 1085 elif args == ['-']:
1087 1086 # read file list from stdin
1088 1087 check = sys.stdin.read().splitlines()
1089 1088 else:
1090 1089 check = args
1091 1090
1092 1091 _preparepats()
1093 1092
1094 1093 ret = 0
1095 1094 for f in check:
1096 1095 if not checkfile(
1097 1096 f,
1098 1097 maxerr=options.per_file,
1099 1098 warnings=options.warnings,
1100 1099 blame=options.blame,
1101 1100 debug=options.debug,
1102 1101 lineno=options.lineno,
1103 1102 ):
1104 1103 ret = 1
1105 1104 return ret
1106 1105
1107 1106
1108 1107 if __name__ == "__main__":
1109 1108 sys.exit(main())
General Comments 0
You need to be logged in to leave comments. Login now