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