##// END OF EJS Templates
check-code: forbid platform.system()...
Jun Wu -
r34643:a679aa58 default
parent child Browse files
Show More
@@ -1,732 +1,733 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 (r'platform\.system\(\)', "don't use platform.system(), use pycompat"),
365 366
366 367 # rules depending on implementation of repquote()
367 368 (r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
368 369 'string join across lines with no space'),
369 370 (r'''(?x)ui\.(status|progress|write|note|warn)\(
370 371 [ \t\n#]*
371 372 (?# any strings/comments might precede a string, which
372 373 # contains translatable message)
373 374 ((['"]|\'\'\'|""")[ \npq%bAPMxno]*(['"]|\'\'\'|""")[ \t\n#]+)*
374 375 (?# sequence consisting of below might precede translatable message
375 376 # - formatting string: "% 10s", "%05d", "% -3.2f", "%*s", "%%" ...
376 377 # - escaped character: "\\", "\n", "\0" ...
377 378 # - character other than '%', 'b' as '\', and 'x' as alphabet)
378 379 (['"]|\'\'\'|""")
379 380 ((%([ n]?[PM]?([np]+|A))?x)|%%|b[bnx]|[ \nnpqAPMo])*x
380 381 (?# this regexp can't use [^...] style,
381 382 # because _preparepats forcibly adds "\n" into [^...],
382 383 # even though this regexp wants match it against "\n")''',
383 384 "missing _() in ui message (use () to hide false-positives)"),
384 385 ],
385 386 # warnings
386 387 [
387 388 # rules depending on implementation of repquote()
388 389 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
389 390 ]
390 391 ]
391 392
392 393 pyfilters = [
393 394 (r"""(?msx)(?P<comment>\#.*?$)|
394 395 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
395 396 (?P<text>(([^\\]|\\.)*?))
396 397 (?P=quote))""", reppython),
397 398 ]
398 399
399 400 # extension non-filter patterns
400 401 pyextnfpats = [
401 402 [(r'^"""\n?[A-Z]', "don't capitalize docstring title")],
402 403 # warnings
403 404 [],
404 405 ]
405 406
406 407 txtfilters = []
407 408
408 409 txtpats = [
409 410 [
410 411 ('\s$', 'trailing whitespace'),
411 412 ('.. note::[ \n][^\n]', 'add two newlines after note::')
412 413 ],
413 414 []
414 415 ]
415 416
416 417 cpats = [
417 418 [
418 419 (r'//', "don't use //-style comments"),
419 420 (r'^ ', "don't use spaces to indent"),
420 421 (r'\S\t', "don't use tabs except for indent"),
421 422 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
422 423 (r'.{81}', "line too long"),
423 424 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
424 425 (r'return\(', "return is not a function"),
425 426 (r' ;', "no space before ;"),
426 427 (r'[^;] \)', "no space before )"),
427 428 (r'[)][{]', "space between ) and {"),
428 429 (r'\w+\* \w+', "use int *foo, not int* foo"),
429 430 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
430 431 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
431 432 (r'\w,\w', "missing whitespace after ,"),
432 433 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
433 434 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
434 435 (r'^#\s+\w', "use #foo, not # foo"),
435 436 (r'[^\n]\Z', "no trailing newline"),
436 437 (r'^\s*#import\b', "use only #include in standard C code"),
437 438 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
438 439 (r'strcat\(', "don't use strcat"),
439 440
440 441 # rules depending on implementation of repquote()
441 442 ],
442 443 # warnings
443 444 [
444 445 # rules depending on implementation of repquote()
445 446 ]
446 447 ]
447 448
448 449 cfilters = [
449 450 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
450 451 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
451 452 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
452 453 (r'(\()([^)]+\))', repcallspaces),
453 454 ]
454 455
455 456 inutilpats = [
456 457 [
457 458 (r'\bui\.', "don't use ui in util"),
458 459 ],
459 460 # warnings
460 461 []
461 462 ]
462 463
463 464 inrevlogpats = [
464 465 [
465 466 (r'\brepo\.', "don't use repo in revlog"),
466 467 ],
467 468 # warnings
468 469 []
469 470 ]
470 471
471 472 webtemplatefilters = []
472 473
473 474 webtemplatepats = [
474 475 [],
475 476 [
476 477 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
477 478 'follow desc keyword with either firstline or websub'),
478 479 ]
479 480 ]
480 481
481 482 allfilesfilters = []
482 483
483 484 allfilespats = [
484 485 [
485 486 (r'(http|https)://[a-zA-Z0-9./]*selenic.com/',
486 487 'use mercurial-scm.org domain URL'),
487 488 (r'mercurial@selenic\.com',
488 489 'use mercurial-scm.org domain for mercurial ML address'),
489 490 (r'mercurial-devel@selenic\.com',
490 491 'use mercurial-scm.org domain for mercurial-devel ML address'),
491 492 ],
492 493 # warnings
493 494 [],
494 495 ]
495 496
496 497 py3pats = [
497 498 [
498 499 (r'os\.environ', "use encoding.environ instead (py3)", r'#.*re-exports'),
499 500 (r'os\.name', "use pycompat.osname instead (py3)"),
500 501 (r'os\.getcwd', "use pycompat.getcwd instead (py3)"),
501 502 (r'os\.sep', "use pycompat.ossep instead (py3)"),
502 503 (r'os\.pathsep', "use pycompat.ospathsep instead (py3)"),
503 504 (r'os\.altsep', "use pycompat.osaltsep instead (py3)"),
504 505 (r'sys\.platform', "use pycompat.sysplatform instead (py3)"),
505 506 (r'getopt\.getopt', "use pycompat.getoptb instead (py3)"),
506 507 (r'os\.getenv', "use encoding.environ.get instead"),
507 508 (r'os\.setenv', "modifying the environ dict is not preferred"),
508 509 ],
509 510 # warnings
510 511 [],
511 512 ]
512 513
513 514 checks = [
514 515 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
515 516 ('python', r'.*hgext.*\.py$', '', [], pyextnfpats),
516 517 ('python 3', r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
517 518 '', pyfilters, py3pats),
518 519 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
519 520 ('c', r'.*\.[ch]$', '', cfilters, cpats),
520 521 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
521 522 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
522 523 pyfilters, inrevlogpats),
523 524 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
524 525 inutilpats),
525 526 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
526 527 ('web template', r'mercurial/templates/.*\.tmpl', '',
527 528 webtemplatefilters, webtemplatepats),
528 529 ('all except for .po', r'.*(?<!\.po)$', '',
529 530 allfilesfilters, allfilespats),
530 531 ]
531 532
532 533 def _preparepats():
533 534 for c in checks:
534 535 failandwarn = c[-1]
535 536 for pats in failandwarn:
536 537 for i, pseq in enumerate(pats):
537 538 # fix-up regexes for multi-line searches
538 539 p = pseq[0]
539 540 # \s doesn't match \n
540 541 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
541 542 # [^...] doesn't match newline
542 543 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
543 544
544 545 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
545 546 filters = c[3]
546 547 for i, flt in enumerate(filters):
547 548 filters[i] = re.compile(flt[0]), flt[1]
548 549
549 550 class norepeatlogger(object):
550 551 def __init__(self):
551 552 self._lastseen = None
552 553
553 554 def log(self, fname, lineno, line, msg, blame):
554 555 """print error related a to given line of a given file.
555 556
556 557 The faulty line will also be printed but only once in the case
557 558 of multiple errors.
558 559
559 560 :fname: filename
560 561 :lineno: line number
561 562 :line: actual content of the line
562 563 :msg: error message
563 564 """
564 565 msgid = fname, lineno, line
565 566 if msgid != self._lastseen:
566 567 if blame:
567 568 print("%s:%d (%s):" % (fname, lineno, blame))
568 569 else:
569 570 print("%s:%d:" % (fname, lineno))
570 571 print(" > %s" % line)
571 572 self._lastseen = msgid
572 573 print(" " + msg)
573 574
574 575 _defaultlogger = norepeatlogger()
575 576
576 577 def getblame(f):
577 578 lines = []
578 579 for l in os.popen('hg annotate -un %s' % f):
579 580 start, line = l.split(':', 1)
580 581 user, rev = start.split()
581 582 lines.append((line[1:-1], user, rev))
582 583 return lines
583 584
584 585 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
585 586 blame=False, debug=False, lineno=True):
586 587 """checks style and portability of a given file
587 588
588 589 :f: filepath
589 590 :logfunc: function used to report error
590 591 logfunc(filename, linenumber, linecontent, errormessage)
591 592 :maxerr: number of error to display before aborting.
592 593 Set to false (default) to report all errors
593 594
594 595 return True if no error is found, False otherwise.
595 596 """
596 597 blamecache = None
597 598 result = True
598 599
599 600 try:
600 601 with opentext(f) as fp:
601 602 try:
602 603 pre = post = fp.read()
603 604 except UnicodeDecodeError as e:
604 605 print("%s while reading %s" % (e, f))
605 606 return result
606 607 except IOError as e:
607 608 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
608 609 return result
609 610
610 611 for name, match, magic, filters, pats in checks:
611 612 post = pre # discard filtering result of previous check
612 613 if debug:
613 614 print(name, f)
614 615 fc = 0
615 616 if not (re.match(match, f) or (magic and re.search(magic, pre))):
616 617 if debug:
617 618 print("Skipping %s for %s it doesn't match %s" % (
618 619 name, match, f))
619 620 continue
620 621 if "no-" "check-code" in pre:
621 622 # If you're looking at this line, it's because a file has:
622 623 # no- check- code
623 624 # but the reason to output skipping is to make life for
624 625 # tests easier. So, instead of writing it with a normal
625 626 # spelling, we write it with the expected spelling from
626 627 # tests/test-check-code.t
627 628 print("Skipping %s it has no-che?k-code (glob)" % f)
628 629 return "Skip" # skip checking this file
629 630 for p, r in filters:
630 631 post = re.sub(p, r, post)
631 632 nerrs = len(pats[0]) # nerr elements are errors
632 633 if warnings:
633 634 pats = pats[0] + pats[1]
634 635 else:
635 636 pats = pats[0]
636 637 # print post # uncomment to show filtered version
637 638
638 639 if debug:
639 640 print("Checking %s for %s" % (name, f))
640 641
641 642 prelines = None
642 643 errors = []
643 644 for i, pat in enumerate(pats):
644 645 if len(pat) == 3:
645 646 p, msg, ignore = pat
646 647 else:
647 648 p, msg = pat
648 649 ignore = None
649 650 if i >= nerrs:
650 651 msg = "warning: " + msg
651 652
652 653 pos = 0
653 654 n = 0
654 655 for m in p.finditer(post):
655 656 if prelines is None:
656 657 prelines = pre.splitlines()
657 658 postlines = post.splitlines(True)
658 659
659 660 start = m.start()
660 661 while n < len(postlines):
661 662 step = len(postlines[n])
662 663 if pos + step > start:
663 664 break
664 665 pos += step
665 666 n += 1
666 667 l = prelines[n]
667 668
668 669 if ignore and re.search(ignore, l, re.MULTILINE):
669 670 if debug:
670 671 print("Skipping %s for %s:%s (ignore pattern)" % (
671 672 name, f, n))
672 673 continue
673 674 bd = ""
674 675 if blame:
675 676 bd = 'working directory'
676 677 if not blamecache:
677 678 blamecache = getblame(f)
678 679 if n < len(blamecache):
679 680 bl, bu, br = blamecache[n]
680 681 if bl == l:
681 682 bd = '%s@%s' % (bu, br)
682 683
683 684 errors.append((f, lineno and n + 1, l, msg, bd))
684 685 result = False
685 686
686 687 errors.sort()
687 688 for e in errors:
688 689 logfunc(*e)
689 690 fc += 1
690 691 if maxerr and fc >= maxerr:
691 692 print(" (too many errors, giving up)")
692 693 break
693 694
694 695 return result
695 696
696 697 def main():
697 698 parser = optparse.OptionParser("%prog [options] [files | -]")
698 699 parser.add_option("-w", "--warnings", action="store_true",
699 700 help="include warning-level checks")
700 701 parser.add_option("-p", "--per-file", type="int",
701 702 help="max warnings per file")
702 703 parser.add_option("-b", "--blame", action="store_true",
703 704 help="use annotate to generate blame info")
704 705 parser.add_option("", "--debug", action="store_true",
705 706 help="show debug information")
706 707 parser.add_option("", "--nolineno", action="store_false",
707 708 dest='lineno', help="don't show line numbers")
708 709
709 710 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
710 711 lineno=True)
711 712 (options, args) = parser.parse_args()
712 713
713 714 if len(args) == 0:
714 715 check = glob.glob("*")
715 716 elif args == ['-']:
716 717 # read file list from stdin
717 718 check = sys.stdin.read().splitlines()
718 719 else:
719 720 check = args
720 721
721 722 _preparepats()
722 723
723 724 ret = 0
724 725 for f in check:
725 726 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
726 727 blame=options.blame, debug=options.debug,
727 728 lineno=options.lineno):
728 729 ret = 1
729 730 return ret
730 731
731 732 if __name__ == "__main__":
732 733 sys.exit(main())
General Comments 0
You need to be logged in to leave comments. Login now