##// END OF EJS Templates
check-code: reject .next(...)
timeless -
r29217:2f9ad6ca default
parent child Browse files
Show More
@@ -1,622 +1,623
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 def repquote(m):
54 54 fromc = '.:'
55 55 tochr = 'pq'
56 56 def encodechr(i):
57 57 if i > 255:
58 58 return 'u'
59 59 c = chr(i)
60 60 if c in ' \n':
61 61 return c
62 62 if c.isalpha():
63 63 return 'x'
64 64 if c.isdigit():
65 65 return 'n'
66 66 try:
67 67 return tochr[fromc.find(c)]
68 68 except (ValueError, IndexError):
69 69 return 'o'
70 70 t = m.group('text')
71 71 tt = ''.join(encodechr(i) for i in xrange(256))
72 72 t = t.translate(tt)
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'ls +[^|\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'^ (cat|find): .*: No such file or directory',
193 193 'use test -f to test for file existence'),
194 194 (r'^ diff -[^ -]*p',
195 195 "don't use (external) diff with -p for portability"),
196 196 (r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
197 197 "glob timezone field in diff output for portability"),
198 198 (r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
199 199 "use '@@ -N* +N,n @@ (glob)' style chunk header 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]+ @@',
203 203 "use '@@ -N* +N* @@ (glob)' style chunk header for portability"),
204 204 (uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
205 205 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
206 206 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)"),
207 207 ],
208 208 # warnings
209 209 [
210 210 (r'^ [^*?/\n]* \(glob\)$',
211 211 "glob match with no glob character (?*/)"),
212 212 ]
213 213 ]
214 214
215 215 for i in [0, 1]:
216 216 for tp in testpats[i]:
217 217 p = tp[0]
218 218 m = tp[1]
219 219 if p.startswith(r'^'):
220 220 p = r"^ [$>] (%s)" % p[1:]
221 221 else:
222 222 p = r"^ [$>] .*(%s)" % p
223 223 utestpats[i].append((p, m) + tp[2:])
224 224
225 225 utestfilters = [
226 226 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
227 227 (r"( *)(#([^\n]*\S)?)", repcomment),
228 228 ]
229 229
230 230 pypats = [
231 231 [
232 232 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
233 233 "tuple parameter unpacking not available in Python 3+"),
234 234 (r'lambda\s*\(.*,.*\)',
235 235 "tuple parameter unpacking not available in Python 3+"),
236 236 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
237 237 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
238 238 (r'dict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
239 239 'dict-from-generator'),
240 240 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
241 241 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
242 242 (r'^\s*\t', "don't use tabs"),
243 243 (r'\S;\s*\n', "semicolon"),
244 244 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
245 245 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
246 246 (r'(\w|\)),\w', "missing whitespace after ,"),
247 247 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
248 248 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
249 249 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
250 250 (r'.{81}', "line too long"),
251 251 (r' x+[xo][\'"]\n\s+[\'"]x', 'string join across lines with no space'),
252 252 (r'[^\n]\Z', "no trailing newline"),
253 253 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
254 254 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
255 255 # "don't use underbars in identifiers"),
256 256 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
257 257 "don't use camelcase in identifiers"),
258 258 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
259 259 "linebreak after :"),
260 260 (r'class\s[^( \n]+:', "old-style class, use class foo(object)",
261 261 r'#.*old-style'),
262 262 (r'class\s[^( \n]+\(\):',
263 263 "class foo() creates old style object, use class foo(object)",
264 264 r'#.*old-style'),
265 265 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
266 266 if k not in ('print', 'exec')),
267 267 "Python keyword is not a function"),
268 268 (r',]', "unneeded trailing ',' in list"),
269 269 # (r'class\s[A-Z][^\(]*\((?!Exception)',
270 270 # "don't capitalize non-exception classes"),
271 271 # (r'in range\(', "use xrange"),
272 272 # (r'^\s*print\s+', "avoid using print in core and extensions"),
273 273 (r'[\x80-\xff]', "non-ASCII character literal"),
274 274 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
275 275 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
276 276 "gratuitous whitespace after Python keyword"),
277 277 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
278 278 # (r'\s\s=', "gratuitous whitespace before ="),
279 279 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
280 280 "missing whitespace around operator"),
281 281 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
282 282 "missing whitespace around operator"),
283 283 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
284 284 "missing whitespace around operator"),
285 285 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
286 286 "wrong whitespace around ="),
287 287 (r'\([^()]*( =[^=]|[^<>!=]= )',
288 288 "no whitespace around = for named parameters"),
289 289 (r'raise Exception', "don't raise generic exceptions"),
290 290 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
291 291 "don't use old-style two-argument raise, use Exception(message)"),
292 292 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
293 293 (r' [=!]=\s+(True|False|None)',
294 294 "comparison with singleton, use 'is' or 'is not' instead"),
295 295 (r'^\s*(while|if) [01]:',
296 296 "use True/False for constant Boolean expression"),
297 297 (r'(?:(?<!def)\s+|\()hasattr',
298 298 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
299 299 (r'opener\([^)]*\).read\(',
300 300 "use opener.read() instead"),
301 301 (r'opener\([^)]*\).write\(',
302 302 "use opener.write() instead"),
303 303 (r'[\s\(](open|file)\([^)]*\)\.read\(',
304 304 "use util.readfile() instead"),
305 305 (r'[\s\(](open|file)\([^)]*\)\.write\(',
306 306 "use util.writefile() instead"),
307 307 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
308 308 "always assign an opened file to a variable, and close it afterwards"),
309 309 (r'[\s\(](open|file)\([^)]*\)\.',
310 310 "always assign an opened file to a variable, and close it afterwards"),
311 311 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
312 312 (r'\.debug\(\_', "don't mark debug messages for translation"),
313 313 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
314 314 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
315 315 (r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
316 316 'legacy exception syntax; use "as" instead of ","'),
317 317 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
318 318 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
319 319 "missing _() in ui message (use () to hide false-positives)"),
320 320 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
321 321 (r'\b__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
322 322 (r'os\.path\.join\(.*, *(""|\'\')\)',
323 323 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
324 324 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
325 325 # XXX only catch mutable arguments on the first line of the definition
326 326 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
327 327 (r'\butil\.Abort\b', "directly use error.Abort"),
328 328 (r'^import Queue', "don't use Queue, use util.queue + util.empty"),
329 329 (r'^import cStringIO', "don't use cStringIO.StringIO, use util.stringio"),
330 330 (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
331 (r'\.next\(\)', "don't use .next(), use next(...)"),
331 332 ],
332 333 # warnings
333 334 [
334 335 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
335 336 ]
336 337 ]
337 338
338 339 pyfilters = [
339 340 (r"""(?msx)(?P<comment>\#.*?$)|
340 341 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
341 342 (?P<text>(([^\\]|\\.)*?))
342 343 (?P=quote))""", reppython),
343 344 ]
344 345
345 346 txtfilters = []
346 347
347 348 txtpats = [
348 349 [
349 350 ('\s$', 'trailing whitespace'),
350 351 ('.. note::[ \n][^\n]', 'add two newlines after note::')
351 352 ],
352 353 []
353 354 ]
354 355
355 356 cpats = [
356 357 [
357 358 (r'//', "don't use //-style comments"),
358 359 (r'^ ', "don't use spaces to indent"),
359 360 (r'\S\t', "don't use tabs except for indent"),
360 361 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
361 362 (r'.{81}', "line too long"),
362 363 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
363 364 (r'return\(', "return is not a function"),
364 365 (r' ;', "no space before ;"),
365 366 (r'[^;] \)', "no space before )"),
366 367 (r'[)][{]', "space between ) and {"),
367 368 (r'\w+\* \w+', "use int *foo, not int* foo"),
368 369 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
369 370 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
370 371 (r'\w,\w', "missing whitespace after ,"),
371 372 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
372 373 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
373 374 (r'^#\s+\w', "use #foo, not # foo"),
374 375 (r'[^\n]\Z', "no trailing newline"),
375 376 (r'^\s*#import\b', "use only #include in standard C code"),
376 377 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
377 378 (r'strcat\(', "don't use strcat"),
378 379 ],
379 380 # warnings
380 381 []
381 382 ]
382 383
383 384 cfilters = [
384 385 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
385 386 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
386 387 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
387 388 (r'(\()([^)]+\))', repcallspaces),
388 389 ]
389 390
390 391 inutilpats = [
391 392 [
392 393 (r'\bui\.', "don't use ui in util"),
393 394 ],
394 395 # warnings
395 396 []
396 397 ]
397 398
398 399 inrevlogpats = [
399 400 [
400 401 (r'\brepo\.', "don't use repo in revlog"),
401 402 ],
402 403 # warnings
403 404 []
404 405 ]
405 406
406 407 webtemplatefilters = []
407 408
408 409 webtemplatepats = [
409 410 [],
410 411 [
411 412 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
412 413 'follow desc keyword with either firstline or websub'),
413 414 ]
414 415 ]
415 416
416 417 checks = [
417 418 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
418 419 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
419 420 ('c', r'.*\.[ch]$', '', cfilters, cpats),
420 421 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
421 422 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
422 423 pyfilters, inrevlogpats),
423 424 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
424 425 inutilpats),
425 426 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
426 427 ('web template', r'mercurial/templates/.*\.tmpl', '',
427 428 webtemplatefilters, webtemplatepats),
428 429 ]
429 430
430 431 def _preparepats():
431 432 for c in checks:
432 433 failandwarn = c[-1]
433 434 for pats in failandwarn:
434 435 for i, pseq in enumerate(pats):
435 436 # fix-up regexes for multi-line searches
436 437 p = pseq[0]
437 438 # \s doesn't match \n
438 439 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
439 440 # [^...] doesn't match newline
440 441 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
441 442
442 443 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
443 444 filters = c[3]
444 445 for i, flt in enumerate(filters):
445 446 filters[i] = re.compile(flt[0]), flt[1]
446 447 _preparepats()
447 448
448 449 class norepeatlogger(object):
449 450 def __init__(self):
450 451 self._lastseen = None
451 452
452 453 def log(self, fname, lineno, line, msg, blame):
453 454 """print error related a to given line of a given file.
454 455
455 456 The faulty line will also be printed but only once in the case
456 457 of multiple errors.
457 458
458 459 :fname: filename
459 460 :lineno: line number
460 461 :line: actual content of the line
461 462 :msg: error message
462 463 """
463 464 msgid = fname, lineno, line
464 465 if msgid != self._lastseen:
465 466 if blame:
466 467 print("%s:%d (%s):" % (fname, lineno, blame))
467 468 else:
468 469 print("%s:%d:" % (fname, lineno))
469 470 print(" > %s" % line)
470 471 self._lastseen = msgid
471 472 print(" " + msg)
472 473
473 474 _defaultlogger = norepeatlogger()
474 475
475 476 def getblame(f):
476 477 lines = []
477 478 for l in os.popen('hg annotate -un %s' % f):
478 479 start, line = l.split(':', 1)
479 480 user, rev = start.split()
480 481 lines.append((line[1:-1], user, rev))
481 482 return lines
482 483
483 484 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
484 485 blame=False, debug=False, lineno=True):
485 486 """checks style and portability of a given file
486 487
487 488 :f: filepath
488 489 :logfunc: function used to report error
489 490 logfunc(filename, linenumber, linecontent, errormessage)
490 491 :maxerr: number of error to display before aborting.
491 492 Set to false (default) to report all errors
492 493
493 494 return True if no error is found, False otherwise.
494 495 """
495 496 blamecache = None
496 497 result = True
497 498
498 499 try:
499 500 with opentext(f) as fp:
500 501 try:
501 502 pre = post = fp.read()
502 503 except UnicodeDecodeError as e:
503 504 print("%s while reading %s" % (e, f))
504 505 return result
505 506 except IOError as e:
506 507 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
507 508 return result
508 509
509 510 for name, match, magic, filters, pats in checks:
510 511 if debug:
511 512 print(name, f)
512 513 fc = 0
513 514 if not (re.match(match, f) or (magic and re.search(magic, pre))):
514 515 if debug:
515 516 print("Skipping %s for %s it doesn't match %s" % (
516 517 name, match, f))
517 518 continue
518 519 if "no-" "check-code" in pre:
519 520 # If you're looking at this line, it's because a file has:
520 521 # no- check- code
521 522 # but the reason to output skipping is to make life for
522 523 # tests easier. So, instead of writing it with a normal
523 524 # spelling, we write it with the expected spelling from
524 525 # tests/test-check-code.t
525 526 print("Skipping %s it has no-che?k-code (glob)" % f)
526 527 return "Skip" # skip checking this file
527 528 for p, r in filters:
528 529 post = re.sub(p, r, post)
529 530 nerrs = len(pats[0]) # nerr elements are errors
530 531 if warnings:
531 532 pats = pats[0] + pats[1]
532 533 else:
533 534 pats = pats[0]
534 535 # print post # uncomment to show filtered version
535 536
536 537 if debug:
537 538 print("Checking %s for %s" % (name, f))
538 539
539 540 prelines = None
540 541 errors = []
541 542 for i, pat in enumerate(pats):
542 543 if len(pat) == 3:
543 544 p, msg, ignore = pat
544 545 else:
545 546 p, msg = pat
546 547 ignore = None
547 548 if i >= nerrs:
548 549 msg = "warning: " + msg
549 550
550 551 pos = 0
551 552 n = 0
552 553 for m in p.finditer(post):
553 554 if prelines is None:
554 555 prelines = pre.splitlines()
555 556 postlines = post.splitlines(True)
556 557
557 558 start = m.start()
558 559 while n < len(postlines):
559 560 step = len(postlines[n])
560 561 if pos + step > start:
561 562 break
562 563 pos += step
563 564 n += 1
564 565 l = prelines[n]
565 566
566 567 if ignore and re.search(ignore, l, re.MULTILINE):
567 568 if debug:
568 569 print("Skipping %s for %s:%s (ignore pattern)" % (
569 570 name, f, n))
570 571 continue
571 572 bd = ""
572 573 if blame:
573 574 bd = 'working directory'
574 575 if not blamecache:
575 576 blamecache = getblame(f)
576 577 if n < len(blamecache):
577 578 bl, bu, br = blamecache[n]
578 579 if bl == l:
579 580 bd = '%s@%s' % (bu, br)
580 581
581 582 errors.append((f, lineno and n + 1, l, msg, bd))
582 583 result = False
583 584
584 585 errors.sort()
585 586 for e in errors:
586 587 logfunc(*e)
587 588 fc += 1
588 589 if maxerr and fc >= maxerr:
589 590 print(" (too many errors, giving up)")
590 591 break
591 592
592 593 return result
593 594
594 595 if __name__ == "__main__":
595 596 parser = optparse.OptionParser("%prog [options] [files]")
596 597 parser.add_option("-w", "--warnings", action="store_true",
597 598 help="include warning-level checks")
598 599 parser.add_option("-p", "--per-file", type="int",
599 600 help="max warnings per file")
600 601 parser.add_option("-b", "--blame", action="store_true",
601 602 help="use annotate to generate blame info")
602 603 parser.add_option("", "--debug", action="store_true",
603 604 help="show debug information")
604 605 parser.add_option("", "--nolineno", action="store_false",
605 606 dest='lineno', help="don't show line numbers")
606 607
607 608 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
608 609 lineno=True)
609 610 (options, args) = parser.parse_args()
610 611
611 612 if len(args) == 0:
612 613 check = glob.glob("*")
613 614 else:
614 615 check = args
615 616
616 617 ret = 0
617 618 for f in check:
618 619 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
619 620 blame=options.blame, debug=options.debug,
620 621 lineno=options.lineno):
621 622 ret = 1
622 623 sys.exit(ret)
General Comments 0
You need to be logged in to leave comments. Login now