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