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