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