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