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