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