##// END OF EJS Templates
contrib: add a check to check-code to ban superfluous pass statements...
Augie Fackler -
r34383:b52f22d9 default
parent child Browse files
Show More
@@ -1,720 +1,730 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 """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 if sys.version_info[0] < 3:
30 30 opentext = open
31 31 else:
32 32 def opentext(f):
33 33 return open(f, encoding='ascii')
34 34 try:
35 35 xrange
36 36 except NameError:
37 37 xrange = range
38 38 try:
39 39 import re2
40 40 except ImportError:
41 41 re2 = None
42 42
43 43 def compilere(pat, multiline=False):
44 44 if multiline:
45 45 pat = '(?m)' + pat
46 46 if re2:
47 47 try:
48 48 return re2.compile(pat)
49 49 except re2.error:
50 50 pass
51 51 return re.compile(pat)
52 52
53 53 # check "rules depending on implementation of repquote()" in each
54 54 # patterns (especially pypats), before changing around repquote()
55 55 _repquotefixedmap = {' ': ' ', '\n': '\n', '.': 'p', ':': 'q',
56 56 '%': '%', '\\': 'b', '*': 'A', '+': 'P', '-': 'M'}
57 57 def _repquoteencodechr(i):
58 58 if i > 255:
59 59 return 'u'
60 60 c = chr(i)
61 61 if c in _repquotefixedmap:
62 62 return _repquotefixedmap[c]
63 63 if c.isalpha():
64 64 return 'x'
65 65 if c.isdigit():
66 66 return 'n'
67 67 return 'o'
68 68 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
69 69
70 70 def repquote(m):
71 71 t = m.group('text')
72 72 t = t.translate(_repquotett)
73 73 return m.group('quote') + t + m.group('quote')
74 74
75 75 def reppython(m):
76 76 comment = m.group('comment')
77 77 if comment:
78 78 l = len(comment.rstrip())
79 79 return "#" * l + comment[l:]
80 80 return repquote(m)
81 81
82 82 def repcomment(m):
83 83 return m.group(1) + "#" * len(m.group(2))
84 84
85 85 def repccomment(m):
86 86 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
87 87 return m.group(1) + t + "*/"
88 88
89 89 def repcallspaces(m):
90 90 t = re.sub(r"\n\s+", "\n", m.group(2))
91 91 return m.group(1) + t
92 92
93 93 def repinclude(m):
94 94 return m.group(1) + "<foo>"
95 95
96 96 def rephere(m):
97 97 t = re.sub(r"\S", "x", m.group(2))
98 98 return m.group(1) + t
99 99
100 100
101 101 testpats = [
102 102 [
103 103 (r'\b(push|pop)d\b', "don't use 'pushd' or 'popd', use 'cd'"),
104 104 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
105 105 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
106 106 (r'(?<!hg )grep.* -a', "don't use 'grep -a', use in-line python"),
107 107 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
108 108 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
109 109 (r'echo -n', "don't use 'echo -n', use printf"),
110 110 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
111 111 (r'head -c', "don't use 'head -c', use 'dd'"),
112 112 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
113 113 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
114 114 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
115 115 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
116 116 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
117 117 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
118 118 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
119 119 (r'\[[^\]]+==', '[ foo == bar ] is a bashism, use [ foo = bar ] instead'),
120 120 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
121 121 "use egrep for extended grep syntax"),
122 122 (r'(^|\|\s*)e?grep .*\\S', "don't use \\S in regular expression"),
123 123 (r'(?<!!)/bin/', "don't use explicit paths for tools"),
124 124 (r'#!.*/bash', "don't use bash in shebang, use sh"),
125 125 (r'[^\n]\Z', "no trailing newline"),
126 126 (r'export .*=', "don't export and assign at once"),
127 127 (r'^source\b', "don't use 'source', use '.'"),
128 128 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
129 129 (r'\bls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
130 130 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
131 131 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
132 132 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
133 133 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
134 134 (r'^alias\b.*=', "don't use alias, use a function"),
135 135 (r'if\s*!', "don't use '!' to negate exit status"),
136 136 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
137 137 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
138 138 (r'^( *)\t', "don't use tabs to indent"),
139 139 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
140 140 "put a backslash-escaped newline after sed 'i' command"),
141 141 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
142 142 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
143 143 (r'[\s="`\']python\s(?!bindings)', "don't use 'python', use '$PYTHON'"),
144 144 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
145 145 (r'\butil\.Abort\b', "directly use error.Abort"),
146 146 (r'\|&', "don't use |&, use 2>&1"),
147 147 (r'\w = +\w', "only one space after = allowed"),
148 148 (r'\bsed\b.*[^\\]\\n', "don't use 'sed ... \\n', use a \\ and a newline"),
149 149 (r'env.*-u', "don't use 'env -u VAR', use 'unset VAR'"),
150 150 (r'cp.* -r ', "don't use 'cp -r', use 'cp -R'"),
151 151 (r'grep.* -[ABC] ', "don't use grep's context flags"),
152 152 ],
153 153 # warnings
154 154 [
155 155 (r'^function', "don't use 'function', use old style"),
156 156 (r'^diff.*-\w*N', "don't use 'diff -N'"),
157 157 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
158 158 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
159 159 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
160 160 ]
161 161 ]
162 162
163 163 testfilters = [
164 164 (r"( *)(#([^!][^\n]*\S)?)", repcomment),
165 165 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
166 166 ]
167 167
168 168 winglobmsg = "use (glob) to match Windows paths too"
169 169 uprefix = r"^ \$ "
170 170 utestpats = [
171 171 [
172 172 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
173 173 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
174 174 "use regex test output patterns instead of sed"),
175 175 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
176 176 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
177 177 (uprefix + r'.*\|\| echo.*(fail|error)',
178 178 "explicit exit code checks unnecessary"),
179 179 (uprefix + r'set -e', "don't use set -e"),
180 180 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
181 181 (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite "
182 182 "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx
183 183 '# no-msys'), # in test-pull.t which is skipped on windows
184 184 (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg),
185 185 (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$',
186 186 winglobmsg),
187 187 (r'^ pulling from \$TESTTMP/.*[^)]$', winglobmsg,
188 188 '\$TESTTMP/unix-repo$'), # in test-issue1802.t which skipped on windows
189 189 (r'^ reverting (?!subrepo ).*/.*[^)]$', winglobmsg),
190 190 (r'^ cloning subrepo \S+/.*[^)]$', winglobmsg),
191 191 (r'^ pushing to \$TESTTMP/.*[^)]$', winglobmsg),
192 192 (r'^ pushing subrepo \S+/\S+ to.*[^)]$', winglobmsg),
193 193 (r'^ moving \S+/.*[^)]$', winglobmsg),
194 194 (r'^ no changes made to subrepo since.*/.*[^)]$', winglobmsg),
195 195 (r'^ .*: largefile \S+ not available from file:.*/.*[^)]$', winglobmsg),
196 196 (r'^ .*file://\$TESTTMP',
197 197 'write "file:/*/$TESTTMP" + (glob) to match on windows too'),
198 198 (r'^ [^$>].*27\.0\.0\.1',
199 199 'use $LOCALIP not an explicit loopback address'),
200 200 (r'^ [^$>].*\$LOCALIP.*[^)]$',
201 201 'mark $LOCALIP output lines with (glob) to help tests in BSD jails'),
202 202 (r'^ (cat|find): .*: No such file or directory',
203 203 'use test -f to test for file existence'),
204 204 (r'^ diff -[^ -]*p',
205 205 "don't use (external) diff with -p for portability"),
206 206 (r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
207 207 "glob timezone field in diff output for portability"),
208 208 (r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
209 209 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability"),
210 210 (r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
211 211 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability"),
212 212 (r'^ @@ -[0-9]+ [+][0-9]+ @@',
213 213 "use '@@ -N* +N* @@ (glob)' style chunk header for portability"),
214 214 (uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
215 215 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
216 216 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)"),
217 217 ],
218 218 # warnings
219 219 [
220 220 (r'^ (?!.*\$LOCALIP)[^*?/\n]* \(glob\)$',
221 221 "glob match with no glob string (?, *, /, and $LOCALIP)"),
222 222 ]
223 223 ]
224 224
225 225 for i in [0, 1]:
226 226 for tp in testpats[i]:
227 227 p = tp[0]
228 228 m = tp[1]
229 229 if p.startswith(r'^'):
230 230 p = r"^ [$>] (%s)" % p[1:]
231 231 else:
232 232 p = r"^ [$>] .*(%s)" % p
233 233 utestpats[i].append((p, m) + tp[2:])
234 234
235 235 utestfilters = [
236 236 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
237 237 (r"( +)(#([^!][^\n]*\S)?)", repcomment),
238 238 ]
239 239
240 240 pypats = [
241 241 [
242 242 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
243 243 "tuple parameter unpacking not available in Python 3+"),
244 244 (r'lambda\s*\(.*,.*\)',
245 245 "tuple parameter unpacking not available in Python 3+"),
246 246 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
247 247 (r'(?<!\.)\breduce\s*\(.*', "reduce is not available in Python 3+"),
248 248 (r'\bdict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
249 249 'dict-from-generator'),
250 250 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
251 251 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
252 252 (r'^\s*\t', "don't use tabs"),
253 253 (r'\S;\s*\n', "semicolon"),
254 254 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
255 255 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
256 256 (r'(\w|\)),\w', "missing whitespace after ,"),
257 257 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
258 258 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
259 259 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
260 ((
261 # a line ending with a colon, potentially with trailing comments
262 r':([ \t]*#[^\n]*)?\n'
263 # one that is not a pass and not only a comment
264 r'(?P<indent>[ \t]+)[^#][^\n]+\n'
265 # more lines at the same indent level
266 r'((?P=indent)[^\n]+\n)*'
267 # a pass at the same indent level, which is bogus
268 r'(?P=indent)pass[ \t\n#]'
269 ), 'omit superfluous pass'),
260 270 (r'.{81}', "line too long"),
261 271 (r'[^\n]\Z', "no trailing newline"),
262 272 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
263 273 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
264 274 # "don't use underbars in identifiers"),
265 275 (r'^\s+(self\.)?[A-Za-z][a-z0-9]+[A-Z]\w* = ',
266 276 "don't use camelcase in identifiers"),
267 277 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
268 278 "linebreak after :"),
269 279 (r'class\s[^( \n]+:', "old-style class, use class foo(object)",
270 280 r'#.*old-style'),
271 281 (r'class\s[^( \n]+\(\):',
272 282 "class foo() creates old style object, use class foo(object)",
273 283 r'#.*old-style'),
274 284 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
275 285 if k not in ('print', 'exec')),
276 286 "Python keyword is not a function"),
277 287 (r',]', "unneeded trailing ',' in list"),
278 288 # (r'class\s[A-Z][^\(]*\((?!Exception)',
279 289 # "don't capitalize non-exception classes"),
280 290 # (r'in range\(', "use xrange"),
281 291 # (r'^\s*print\s+', "avoid using print in core and extensions"),
282 292 (r'[\x80-\xff]', "non-ASCII character literal"),
283 293 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
284 294 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
285 295 "gratuitous whitespace after Python keyword"),
286 296 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
287 297 # (r'\s\s=', "gratuitous whitespace before ="),
288 298 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
289 299 "missing whitespace around operator"),
290 300 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
291 301 "missing whitespace around operator"),
292 302 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
293 303 "missing whitespace around operator"),
294 304 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
295 305 "wrong whitespace around ="),
296 306 (r'\([^()]*( =[^=]|[^<>!=]= )',
297 307 "no whitespace around = for named parameters"),
298 308 (r'raise Exception', "don't raise generic exceptions"),
299 309 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
300 310 "don't use old-style two-argument raise, use Exception(message)"),
301 311 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
302 312 (r' [=!]=\s+(True|False|None)',
303 313 "comparison with singleton, use 'is' or 'is not' instead"),
304 314 (r'^\s*(while|if) [01]:',
305 315 "use True/False for constant Boolean expression"),
306 316 (r'^\s*if False(:| +and)', 'Remove code instead of using `if False`'),
307 317 (r'(?:(?<!def)\s+|\()hasattr\(',
308 318 'hasattr(foo, bar) is broken on py2, use util.safehasattr(foo, bar) '
309 319 'instead', r'#.*hasattr-py3-only'),
310 320 (r'opener\([^)]*\).read\(',
311 321 "use opener.read() instead"),
312 322 (r'opener\([^)]*\).write\(',
313 323 "use opener.write() instead"),
314 324 (r'[\s\(](open|file)\([^)]*\)\.read\(',
315 325 "use util.readfile() instead"),
316 326 (r'[\s\(](open|file)\([^)]*\)\.write\(',
317 327 "use util.writefile() instead"),
318 328 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
319 329 "always assign an opened file to a variable, and close it afterwards"),
320 330 (r'[\s\(](open|file)\([^)]*\)\.',
321 331 "always assign an opened file to a variable, and close it afterwards"),
322 332 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
323 333 (r'\.debug\(\_', "don't mark debug messages for translation"),
324 334 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
325 335 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
326 336 (r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
327 337 'legacy exception syntax; use "as" instead of ","'),
328 338 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
329 339 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
330 340 (r'\bdef\s+__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
331 341 (r'os\.path\.join\(.*, *(""|\'\')\)',
332 342 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
333 343 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
334 344 # XXX only catch mutable arguments on the first line of the definition
335 345 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
336 346 (r'\butil\.Abort\b', "directly use error.Abort"),
337 347 (r'^@(\w*\.)?cachefunc', "module-level @cachefunc is risky, please avoid"),
338 348 (r'^import Queue', "don't use Queue, use util.queue + util.empty"),
339 349 (r'^import cStringIO', "don't use cStringIO.StringIO, use util.stringio"),
340 350 (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
341 351 (r'^import SocketServer', "don't use SockerServer, use util.socketserver"),
342 352 (r'^import urlparse', "don't use urlparse, use util.urlreq"),
343 353 (r'^import xmlrpclib', "don't use xmlrpclib, use util.xmlrpclib"),
344 354 (r'^import cPickle', "don't use cPickle, use util.pickle"),
345 355 (r'^import pickle', "don't use pickle, use util.pickle"),
346 356 (r'^import httplib', "don't use httplib, use util.httplib"),
347 357 (r'^import BaseHTTPServer', "use util.httpserver instead"),
348 358 (r'^(from|import) mercurial\.(cext|pure|cffi)',
349 359 "use mercurial.policy.importmod instead"),
350 360 (r'\.next\(\)', "don't use .next(), use next(...)"),
351 361 (r'([a-z]*).revision\(\1\.node\(',
352 362 "don't convert rev to node before passing to revision(nodeorrev)"),
353 363
354 364 # rules depending on implementation of repquote()
355 365 (r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
356 366 'string join across lines with no space'),
357 367 (r'''(?x)ui\.(status|progress|write|note|warn)\(
358 368 [ \t\n#]*
359 369 (?# any strings/comments might precede a string, which
360 370 # contains translatable message)
361 371 ((['"]|\'\'\'|""")[ \npq%bAPMxno]*(['"]|\'\'\'|""")[ \t\n#]+)*
362 372 (?# sequence consisting of below might precede translatable message
363 373 # - formatting string: "% 10s", "%05d", "% -3.2f", "%*s", "%%" ...
364 374 # - escaped character: "\\", "\n", "\0" ...
365 375 # - character other than '%', 'b' as '\', and 'x' as alphabet)
366 376 (['"]|\'\'\'|""")
367 377 ((%([ n]?[PM]?([np]+|A))?x)|%%|b[bnx]|[ \nnpqAPMo])*x
368 378 (?# this regexp can't use [^...] style,
369 379 # because _preparepats forcibly adds "\n" into [^...],
370 380 # even though this regexp wants match it against "\n")''',
371 381 "missing _() in ui message (use () to hide false-positives)"),
372 382 ],
373 383 # warnings
374 384 [
375 385 # rules depending on implementation of repquote()
376 386 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
377 387 ]
378 388 ]
379 389
380 390 pyfilters = [
381 391 (r"""(?msx)(?P<comment>\#.*?$)|
382 392 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
383 393 (?P<text>(([^\\]|\\.)*?))
384 394 (?P=quote))""", reppython),
385 395 ]
386 396
387 397 # extension non-filter patterns
388 398 pyextnfpats = [
389 399 [(r'^"""\n?[A-Z]', "don't capitalize docstring title")],
390 400 # warnings
391 401 [],
392 402 ]
393 403
394 404 txtfilters = []
395 405
396 406 txtpats = [
397 407 [
398 408 ('\s$', 'trailing whitespace'),
399 409 ('.. note::[ \n][^\n]', 'add two newlines after note::')
400 410 ],
401 411 []
402 412 ]
403 413
404 414 cpats = [
405 415 [
406 416 (r'//', "don't use //-style comments"),
407 417 (r'^ ', "don't use spaces to indent"),
408 418 (r'\S\t', "don't use tabs except for indent"),
409 419 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
410 420 (r'.{81}', "line too long"),
411 421 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
412 422 (r'return\(', "return is not a function"),
413 423 (r' ;', "no space before ;"),
414 424 (r'[^;] \)', "no space before )"),
415 425 (r'[)][{]', "space between ) and {"),
416 426 (r'\w+\* \w+', "use int *foo, not int* foo"),
417 427 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
418 428 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
419 429 (r'\w,\w', "missing whitespace after ,"),
420 430 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
421 431 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
422 432 (r'^#\s+\w', "use #foo, not # foo"),
423 433 (r'[^\n]\Z', "no trailing newline"),
424 434 (r'^\s*#import\b', "use only #include in standard C code"),
425 435 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
426 436 (r'strcat\(', "don't use strcat"),
427 437
428 438 # rules depending on implementation of repquote()
429 439 ],
430 440 # warnings
431 441 [
432 442 # rules depending on implementation of repquote()
433 443 ]
434 444 ]
435 445
436 446 cfilters = [
437 447 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
438 448 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
439 449 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
440 450 (r'(\()([^)]+\))', repcallspaces),
441 451 ]
442 452
443 453 inutilpats = [
444 454 [
445 455 (r'\bui\.', "don't use ui in util"),
446 456 ],
447 457 # warnings
448 458 []
449 459 ]
450 460
451 461 inrevlogpats = [
452 462 [
453 463 (r'\brepo\.', "don't use repo in revlog"),
454 464 ],
455 465 # warnings
456 466 []
457 467 ]
458 468
459 469 webtemplatefilters = []
460 470
461 471 webtemplatepats = [
462 472 [],
463 473 [
464 474 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
465 475 'follow desc keyword with either firstline or websub'),
466 476 ]
467 477 ]
468 478
469 479 allfilesfilters = []
470 480
471 481 allfilespats = [
472 482 [
473 483 (r'(http|https)://[a-zA-Z0-9./]*selenic.com/',
474 484 'use mercurial-scm.org domain URL'),
475 485 (r'mercurial@selenic\.com',
476 486 'use mercurial-scm.org domain for mercurial ML address'),
477 487 (r'mercurial-devel@selenic\.com',
478 488 'use mercurial-scm.org domain for mercurial-devel ML address'),
479 489 ],
480 490 # warnings
481 491 [],
482 492 ]
483 493
484 494 py3pats = [
485 495 [
486 496 (r'os\.environ', "use encoding.environ instead (py3)", r'#.*re-exports'),
487 497 (r'os\.name', "use pycompat.osname instead (py3)"),
488 498 (r'os\.getcwd', "use pycompat.getcwd instead (py3)"),
489 499 (r'os\.sep', "use pycompat.ossep instead (py3)"),
490 500 (r'os\.pathsep', "use pycompat.ospathsep instead (py3)"),
491 501 (r'os\.altsep', "use pycompat.osaltsep instead (py3)"),
492 502 (r'sys\.platform', "use pycompat.sysplatform instead (py3)"),
493 503 (r'getopt\.getopt', "use pycompat.getoptb instead (py3)"),
494 504 (r'os\.getenv', "use encoding.environ.get instead"),
495 505 (r'os\.setenv', "modifying the environ dict is not preferred"),
496 506 ],
497 507 # warnings
498 508 [],
499 509 ]
500 510
501 511 checks = [
502 512 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
503 513 ('python', r'.*hgext.*\.py$', '', [], pyextnfpats),
504 514 ('python 3', r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
505 515 '', pyfilters, py3pats),
506 516 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
507 517 ('c', r'.*\.[ch]$', '', cfilters, cpats),
508 518 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
509 519 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
510 520 pyfilters, inrevlogpats),
511 521 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
512 522 inutilpats),
513 523 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
514 524 ('web template', r'mercurial/templates/.*\.tmpl', '',
515 525 webtemplatefilters, webtemplatepats),
516 526 ('all except for .po', r'.*(?<!\.po)$', '',
517 527 allfilesfilters, allfilespats),
518 528 ]
519 529
520 530 def _preparepats():
521 531 for c in checks:
522 532 failandwarn = c[-1]
523 533 for pats in failandwarn:
524 534 for i, pseq in enumerate(pats):
525 535 # fix-up regexes for multi-line searches
526 536 p = pseq[0]
527 537 # \s doesn't match \n
528 538 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
529 539 # [^...] doesn't match newline
530 540 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
531 541
532 542 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
533 543 filters = c[3]
534 544 for i, flt in enumerate(filters):
535 545 filters[i] = re.compile(flt[0]), flt[1]
536 546
537 547 class norepeatlogger(object):
538 548 def __init__(self):
539 549 self._lastseen = None
540 550
541 551 def log(self, fname, lineno, line, msg, blame):
542 552 """print error related a to given line of a given file.
543 553
544 554 The faulty line will also be printed but only once in the case
545 555 of multiple errors.
546 556
547 557 :fname: filename
548 558 :lineno: line number
549 559 :line: actual content of the line
550 560 :msg: error message
551 561 """
552 562 msgid = fname, lineno, line
553 563 if msgid != self._lastseen:
554 564 if blame:
555 565 print("%s:%d (%s):" % (fname, lineno, blame))
556 566 else:
557 567 print("%s:%d:" % (fname, lineno))
558 568 print(" > %s" % line)
559 569 self._lastseen = msgid
560 570 print(" " + msg)
561 571
562 572 _defaultlogger = norepeatlogger()
563 573
564 574 def getblame(f):
565 575 lines = []
566 576 for l in os.popen('hg annotate -un %s' % f):
567 577 start, line = l.split(':', 1)
568 578 user, rev = start.split()
569 579 lines.append((line[1:-1], user, rev))
570 580 return lines
571 581
572 582 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
573 583 blame=False, debug=False, lineno=True):
574 584 """checks style and portability of a given file
575 585
576 586 :f: filepath
577 587 :logfunc: function used to report error
578 588 logfunc(filename, linenumber, linecontent, errormessage)
579 589 :maxerr: number of error to display before aborting.
580 590 Set to false (default) to report all errors
581 591
582 592 return True if no error is found, False otherwise.
583 593 """
584 594 blamecache = None
585 595 result = True
586 596
587 597 try:
588 598 with opentext(f) as fp:
589 599 try:
590 600 pre = post = fp.read()
591 601 except UnicodeDecodeError as e:
592 602 print("%s while reading %s" % (e, f))
593 603 return result
594 604 except IOError as e:
595 605 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
596 606 return result
597 607
598 608 for name, match, magic, filters, pats in checks:
599 609 post = pre # discard filtering result of previous check
600 610 if debug:
601 611 print(name, f)
602 612 fc = 0
603 613 if not (re.match(match, f) or (magic and re.search(magic, pre))):
604 614 if debug:
605 615 print("Skipping %s for %s it doesn't match %s" % (
606 616 name, match, f))
607 617 continue
608 618 if "no-" "check-code" in pre:
609 619 # If you're looking at this line, it's because a file has:
610 620 # no- check- code
611 621 # but the reason to output skipping is to make life for
612 622 # tests easier. So, instead of writing it with a normal
613 623 # spelling, we write it with the expected spelling from
614 624 # tests/test-check-code.t
615 625 print("Skipping %s it has no-che?k-code (glob)" % f)
616 626 return "Skip" # skip checking this file
617 627 for p, r in filters:
618 628 post = re.sub(p, r, post)
619 629 nerrs = len(pats[0]) # nerr elements are errors
620 630 if warnings:
621 631 pats = pats[0] + pats[1]
622 632 else:
623 633 pats = pats[0]
624 634 # print post # uncomment to show filtered version
625 635
626 636 if debug:
627 637 print("Checking %s for %s" % (name, f))
628 638
629 639 prelines = None
630 640 errors = []
631 641 for i, pat in enumerate(pats):
632 642 if len(pat) == 3:
633 643 p, msg, ignore = pat
634 644 else:
635 645 p, msg = pat
636 646 ignore = None
637 647 if i >= nerrs:
638 648 msg = "warning: " + msg
639 649
640 650 pos = 0
641 651 n = 0
642 652 for m in p.finditer(post):
643 653 if prelines is None:
644 654 prelines = pre.splitlines()
645 655 postlines = post.splitlines(True)
646 656
647 657 start = m.start()
648 658 while n < len(postlines):
649 659 step = len(postlines[n])
650 660 if pos + step > start:
651 661 break
652 662 pos += step
653 663 n += 1
654 664 l = prelines[n]
655 665
656 666 if ignore and re.search(ignore, l, re.MULTILINE):
657 667 if debug:
658 668 print("Skipping %s for %s:%s (ignore pattern)" % (
659 669 name, f, n))
660 670 continue
661 671 bd = ""
662 672 if blame:
663 673 bd = 'working directory'
664 674 if not blamecache:
665 675 blamecache = getblame(f)
666 676 if n < len(blamecache):
667 677 bl, bu, br = blamecache[n]
668 678 if bl == l:
669 679 bd = '%s@%s' % (bu, br)
670 680
671 681 errors.append((f, lineno and n + 1, l, msg, bd))
672 682 result = False
673 683
674 684 errors.sort()
675 685 for e in errors:
676 686 logfunc(*e)
677 687 fc += 1
678 688 if maxerr and fc >= maxerr:
679 689 print(" (too many errors, giving up)")
680 690 break
681 691
682 692 return result
683 693
684 694 def main():
685 695 parser = optparse.OptionParser("%prog [options] [files | -]")
686 696 parser.add_option("-w", "--warnings", action="store_true",
687 697 help="include warning-level checks")
688 698 parser.add_option("-p", "--per-file", type="int",
689 699 help="max warnings per file")
690 700 parser.add_option("-b", "--blame", action="store_true",
691 701 help="use annotate to generate blame info")
692 702 parser.add_option("", "--debug", action="store_true",
693 703 help="show debug information")
694 704 parser.add_option("", "--nolineno", action="store_false",
695 705 dest='lineno', help="don't show line numbers")
696 706
697 707 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
698 708 lineno=True)
699 709 (options, args) = parser.parse_args()
700 710
701 711 if len(args) == 0:
702 712 check = glob.glob("*")
703 713 elif args == ['-']:
704 714 # read file list from stdin
705 715 check = sys.stdin.read().splitlines()
706 716 else:
707 717 check = args
708 718
709 719 _preparepats()
710 720
711 721 ret = 0
712 722 for f in check:
713 723 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
714 724 blame=options.blame, debug=options.debug,
715 725 lineno=options.lineno):
716 726 ret = 1
717 727 return ret
718 728
719 729 if __name__ == "__main__":
720 730 sys.exit(main())
@@ -1,320 +1,364 b''
1 1 $ cat > correct.py <<EOF
2 2 > def toto(arg1, arg2):
3 3 > del arg2
4 4 > return (5 + 6, 9)
5 5 > EOF
6 6 $ cat > wrong.py <<EOF
7 7 > def toto( arg1, arg2):
8 8 > del(arg2)
9 9 > return ( 5+6, 9)
10 10 > EOF
11 11 $ cat > quote.py <<EOF
12 12 > # let's use quote in comments
13 13 > (''' ( 4x5 )
14 14 > but """\\''' and finally''',
15 15 > """let's fool checkpatch""", '1+2',
16 16 > '"""', 42+1, """and
17 17 > ( 4-1 ) """, "( 1+1 )\" and ")
18 18 > a, '\\\\\\\\', "\\\\\\" x-2", "c-1"
19 19 > EOF
20 20 $ cat > classstyle.py <<EOF
21 21 > class newstyle_class(object):
22 22 > pass
23 23 >
24 24 > class oldstyle_class:
25 25 > pass
26 26 >
27 27 > class empty():
28 28 > pass
29 29 >
30 30 > no_class = 1:
31 31 > pass
32 32 > EOF
33 33 $ check_code="$TESTDIR"/../contrib/check-code.py
34 34 $ "$check_code" ./wrong.py ./correct.py ./quote.py ./classstyle.py
35 35 ./wrong.py:1:
36 36 > def toto( arg1, arg2):
37 37 gratuitous whitespace in () or []
38 38 ./wrong.py:2:
39 39 > del(arg2)
40 40 Python keyword is not a function
41 41 ./wrong.py:3:
42 42 > return ( 5+6, 9)
43 43 gratuitous whitespace in () or []
44 44 missing whitespace in expression
45 45 ./quote.py:5:
46 46 > '"""', 42+1, """and
47 47 missing whitespace in expression
48 48 ./classstyle.py:4:
49 49 > class oldstyle_class:
50 50 old-style class, use class foo(object)
51 51 ./classstyle.py:7:
52 52 > class empty():
53 53 class foo() creates old style object, use class foo(object)
54 54 [1]
55 55 $ cat > python3-compat.py << EOF
56 56 > foo <> bar
57 57 > reduce(lambda a, b: a + b, [1, 2, 3, 4])
58 58 > dict(key=value)
59 59 > EOF
60 60 $ "$check_code" python3-compat.py
61 61 python3-compat.py:1:
62 62 > foo <> bar
63 63 <> operator is not available in Python 3+, use !=
64 64 python3-compat.py:2:
65 65 > reduce(lambda a, b: a + b, [1, 2, 3, 4])
66 66 reduce is not available in Python 3+
67 67 python3-compat.py:3:
68 68 > dict(key=value)
69 69 dict() is different in Py2 and 3 and is slower than {}
70 70 [1]
71 71
72 72 $ cat > foo.c <<EOF
73 73 > void narf() {
74 74 > strcpy(foo, bar);
75 75 > // strcpy_s is okay, but this comment is not
76 76 > strcpy_s(foo, bar);
77 77 > }
78 78 > EOF
79 79 $ "$check_code" ./foo.c
80 80 ./foo.c:2:
81 81 > strcpy(foo, bar);
82 82 don't use strcpy, use strlcpy or memcpy
83 83 ./foo.c:3:
84 84 > // strcpy_s is okay, but this comment is not
85 85 don't use //-style comments
86 86 [1]
87 87
88 88 $ cat > is-op.py <<EOF
89 89 > # is-operator comparing number or string literal
90 90 > x = None
91 91 > y = x is 'foo'
92 92 > y = x is "foo"
93 93 > y = x is 5346
94 94 > y = x is -6
95 95 > y = x is not 'foo'
96 96 > y = x is not "foo"
97 97 > y = x is not 5346
98 98 > y = x is not -6
99 99 > EOF
100 100
101 101 $ "$check_code" ./is-op.py
102 102 ./is-op.py:3:
103 103 > y = x is 'foo'
104 104 object comparison with literal
105 105 ./is-op.py:4:
106 106 > y = x is "foo"
107 107 object comparison with literal
108 108 ./is-op.py:5:
109 109 > y = x is 5346
110 110 object comparison with literal
111 111 ./is-op.py:6:
112 112 > y = x is -6
113 113 object comparison with literal
114 114 ./is-op.py:7:
115 115 > y = x is not 'foo'
116 116 object comparison with literal
117 117 ./is-op.py:8:
118 118 > y = x is not "foo"
119 119 object comparison with literal
120 120 ./is-op.py:9:
121 121 > y = x is not 5346
122 122 object comparison with literal
123 123 ./is-op.py:10:
124 124 > y = x is not -6
125 125 object comparison with literal
126 126 [1]
127 127
128 128 $ cat > for-nolineno.py <<EOF
129 129 > except:
130 130 > EOF
131 131 $ "$check_code" for-nolineno.py --nolineno
132 132 for-nolineno.py:0:
133 133 > except:
134 134 naked except clause
135 135 [1]
136 136
137 137 $ cat > warning.t <<EOF
138 138 > $ function warnonly {
139 139 > > }
140 140 > $ diff -N aaa
141 141 > $ function onwarn {}
142 142 > EOF
143 143 $ "$check_code" warning.t
144 144 $ "$check_code" --warn warning.t
145 145 warning.t:1:
146 146 > $ function warnonly {
147 147 warning: don't use 'function', use old style
148 148 warning.t:3:
149 149 > $ diff -N aaa
150 150 warning: don't use 'diff -N'
151 151 warning.t:4:
152 152 > $ function onwarn {}
153 153 warning: don't use 'function', use old style
154 154 [1]
155 155 $ cat > error.t <<EOF
156 156 > $ [ foo == bar ]
157 157 > EOF
158 158 $ "$check_code" error.t
159 159 error.t:1:
160 160 > $ [ foo == bar ]
161 161 [ foo == bar ] is a bashism, use [ foo = bar ] instead
162 162 [1]
163 163 $ rm error.t
164 164 $ cat > raise-format.py <<EOF
165 165 > raise SomeException, message
166 166 > # this next line is okay
167 167 > raise SomeException(arg1, arg2)
168 168 > EOF
169 169 $ "$check_code" not-existing.py raise-format.py
170 170 Skipping*not-existing.py* (glob)
171 171 raise-format.py:1:
172 172 > raise SomeException, message
173 173 don't use old-style two-argument raise, use Exception(message)
174 174 [1]
175 175
176 176 $ cat > rst.py <<EOF
177 177 > """problematic rst text
178 178 >
179 179 > .. note::
180 180 > wrong
181 181 > """
182 182 >
183 183 > '''
184 184 >
185 185 > .. note::
186 186 >
187 187 > valid
188 188 >
189 189 > new text
190 190 >
191 191 > .. note::
192 192 >
193 193 > also valid
194 194 > '''
195 195 >
196 196 > """mixed
197 197 >
198 198 > .. note::
199 199 >
200 200 > good
201 201 >
202 202 > .. note::
203 203 > plus bad
204 204 > """
205 205 > EOF
206 206 $ $check_code -w rst.py
207 207 rst.py:3:
208 208 > .. note::
209 209 warning: add two newlines after '.. note::'
210 210 rst.py:26:
211 211 > .. note::
212 212 warning: add two newlines after '.. note::'
213 213 [1]
214 214
215 215 $ cat > ./map-inside-gettext.py <<EOF
216 216 > print(_("map inside gettext %s" % v))
217 217 >
218 218 > print(_("concatenating " " by " " space %s" % v))
219 219 > print(_("concatenating " + " by " + " '+' %s" % v))
220 220 >
221 221 > print(_("mapping operation in different line %s"
222 222 > % v))
223 223 >
224 224 > print(_(
225 225 > "leading spaces inside of '(' %s" % v))
226 226 > EOF
227 227 $ "$check_code" ./map-inside-gettext.py
228 228 ./map-inside-gettext.py:1:
229 229 > print(_("map inside gettext %s" % v))
230 230 don't use % inside _()
231 231 ./map-inside-gettext.py:3:
232 232 > print(_("concatenating " " by " " space %s" % v))
233 233 don't use % inside _()
234 234 ./map-inside-gettext.py:4:
235 235 > print(_("concatenating " + " by " + " '+' %s" % v))
236 236 don't use % inside _()
237 237 ./map-inside-gettext.py:6:
238 238 > print(_("mapping operation in different line %s"
239 239 don't use % inside _()
240 240 ./map-inside-gettext.py:9:
241 241 > print(_(
242 242 don't use % inside _()
243 243 [1]
244 244
245 245 web templates
246 246
247 247 $ mkdir -p mercurial/templates
248 248 $ cat > mercurial/templates/example.tmpl <<EOF
249 249 > {desc}
250 250 > {desc|escape}
251 251 > {desc|firstline}
252 252 > {desc|websub}
253 253 > EOF
254 254
255 255 $ "$check_code" --warnings mercurial/templates/example.tmpl
256 256 mercurial/templates/example.tmpl:2:
257 257 > {desc|escape}
258 258 warning: follow desc keyword with either firstline or websub
259 259 [1]
260 260
261 261 'string join across lines with no space' detection
262 262
263 263 $ cat > stringjoin.py <<EOF
264 264 > foo = (' foo'
265 265 > 'bar foo.'
266 266 > 'bar foo:'
267 267 > 'bar foo@'
268 268 > 'bar foo%'
269 269 > 'bar foo*'
270 270 > 'bar foo+'
271 271 > 'bar foo-'
272 272 > 'bar')
273 273 > EOF
274 274
275 275 'missing _() in ui message' detection
276 276
277 277 $ cat > uigettext.py <<EOF
278 278 > ui.status("% 10s %05d % -3.2f %*s %%"
279 279 > # this use '\\\\' instead of '\\', because the latter in
280 280 > # heredoc on shell becomes just '\'
281 281 > '\\\\ \n \t \0'
282 282 > """12345
283 283 > """
284 284 > '''.:*+-=
285 285 > ''' "%-6d \n 123456 .:*+-= foobar")
286 286 > EOF
287 287
288 superfluous pass
289
290 $ cat > superfluous_pass.py <<EOF
291 > # correct examples
292 > if foo:
293 > pass
294 > else:
295 > # comment-only line means still need pass
296 > pass
297 > def nothing():
298 > pass
299 > class empty(object):
300 > pass
301 > if whatever:
302 > passvalue(value)
303 > # bad examples
304 > if foo:
305 > "foo"
306 > pass
307 > else: # trailing comment doesn't fool checker
308 > wat()
309 > pass
310 > def nothing():
311 > "docstring means no pass"
312 > pass
313 > class empty(object):
314 > """multiline
315 > docstring also
316 > means no pass"""
317 > pass
318 > EOF
319
288 320 (Checking multiple invalid files at once examines whether caching
289 321 translation table for repquote() works as expected or not. All files
290 322 should break rules depending on result of repquote(), in this case)
291 323
292 $ "$check_code" stringjoin.py uigettext.py
324 $ "$check_code" stringjoin.py uigettext.py superfluous_pass.py
293 325 stringjoin.py:1:
294 326 > foo = (' foo'
295 327 string join across lines with no space
296 328 stringjoin.py:2:
297 329 > 'bar foo.'
298 330 string join across lines with no space
299 331 stringjoin.py:3:
300 332 > 'bar foo:'
301 333 string join across lines with no space
302 334 stringjoin.py:4:
303 335 > 'bar foo@'
304 336 string join across lines with no space
305 337 stringjoin.py:5:
306 338 > 'bar foo%'
307 339 string join across lines with no space
308 340 stringjoin.py:6:
309 341 > 'bar foo*'
310 342 string join across lines with no space
311 343 stringjoin.py:7:
312 344 > 'bar foo+'
313 345 string join across lines with no space
314 346 stringjoin.py:8:
315 347 > 'bar foo-'
316 348 string join across lines with no space
317 349 uigettext.py:1:
318 350 > ui.status("% 10s %05d % -3.2f %*s %%"
319 351 missing _() in ui message (use () to hide false-positives)
352 superfluous_pass.py:14:
353 > if foo:
354 omit superfluous pass
355 superfluous_pass.py:17:
356 > else: # trailing comment doesn't fool checker
357 omit superfluous pass
358 superfluous_pass.py:20:
359 > def nothing():
360 omit superfluous pass
361 superfluous_pass.py:23:
362 > class empty(object):
363 omit superfluous pass
320 364 [1]
General Comments 0
You need to be logged in to leave comments. Login now