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