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