##// END OF EJS Templates
check-code: remove redundant import style check...
Gregory Szorc -
r28399:2af0156c default
parent child Browse files
Show More
@@ -1,602 +1,599 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, # no-py24)
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 import re, glob, os, sys
23 23 import keyword
24 24 import optparse
25 25 try:
26 26 import re2
27 27 except ImportError:
28 28 re2 = None
29 29
30 30 def compilere(pat, multiline=False):
31 31 if multiline:
32 32 pat = '(?m)' + pat
33 33 if re2:
34 34 try:
35 35 return re2.compile(pat)
36 36 except re2.error:
37 37 pass
38 38 return re.compile(pat)
39 39
40 40 def repquote(m):
41 41 fromc = '.:'
42 42 tochr = 'pq'
43 43 def encodechr(i):
44 44 if i > 255:
45 45 return 'u'
46 46 c = chr(i)
47 47 if c in ' \n':
48 48 return c
49 49 if c.isalpha():
50 50 return 'x'
51 51 if c.isdigit():
52 52 return 'n'
53 53 try:
54 54 return tochr[fromc.find(c)]
55 55 except (ValueError, IndexError):
56 56 return 'o'
57 57 t = m.group('text')
58 58 tt = ''.join(encodechr(i) for i in xrange(256))
59 59 t = t.translate(tt)
60 60 return m.group('quote') + t + m.group('quote')
61 61
62 62 def reppython(m):
63 63 comment = m.group('comment')
64 64 if comment:
65 65 l = len(comment.rstrip())
66 66 return "#" * l + comment[l:]
67 67 return repquote(m)
68 68
69 69 def repcomment(m):
70 70 return m.group(1) + "#" * len(m.group(2))
71 71
72 72 def repccomment(m):
73 73 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
74 74 return m.group(1) + t + "*/"
75 75
76 76 def repcallspaces(m):
77 77 t = re.sub(r"\n\s+", "\n", m.group(2))
78 78 return m.group(1) + t
79 79
80 80 def repinclude(m):
81 81 return m.group(1) + "<foo>"
82 82
83 83 def rephere(m):
84 84 t = re.sub(r"\S", "x", m.group(2))
85 85 return m.group(1) + t
86 86
87 87
88 88 testpats = [
89 89 [
90 90 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
91 91 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
92 92 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
93 93 (r'(?<!hg )grep.* -a', "don't use 'grep -a', use in-line python"),
94 94 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
95 95 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
96 96 (r'echo -n', "don't use 'echo -n', use printf"),
97 97 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
98 98 (r'head -c', "don't use 'head -c', use 'dd'"),
99 99 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
100 100 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
101 101 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
102 102 (r'printf.*[^\\]\\([1-9]|0\d)', "don't use 'printf \NNN', use Python"),
103 103 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
104 104 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
105 105 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
106 106 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
107 107 "use egrep for extended grep syntax"),
108 108 (r'/bin/', "don't use explicit paths for tools"),
109 109 (r'[^\n]\Z', "no trailing newline"),
110 110 (r'export .*=', "don't export and assign at once"),
111 111 (r'^source\b', "don't use 'source', use '.'"),
112 112 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
113 113 (r'ls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
114 114 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
115 115 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
116 116 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
117 117 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
118 118 (r'^alias\b.*=', "don't use alias, use a function"),
119 119 (r'if\s*!', "don't use '!' to negate exit status"),
120 120 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
121 121 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
122 122 (r'^( *)\t', "don't use tabs to indent"),
123 123 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
124 124 "put a backslash-escaped newline after sed 'i' command"),
125 125 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
126 126 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
127 127 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
128 128 (r'\butil\.Abort\b', "directly use error.Abort"),
129 129 (r'\|&', "don't use |&, use 2>&1"),
130 130 (r'\w = +\w', "only one space after = allowed"),
131 131 ],
132 132 # warnings
133 133 [
134 134 (r'^function', "don't use 'function', use old style"),
135 135 (r'^diff.*-\w*N', "don't use 'diff -N'"),
136 136 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
137 137 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
138 138 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
139 139 ]
140 140 ]
141 141
142 142 testfilters = [
143 143 (r"( *)(#([^\n]*\S)?)", repcomment),
144 144 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
145 145 ]
146 146
147 147 winglobmsg = "use (glob) to match Windows paths too"
148 148 uprefix = r"^ \$ "
149 149 utestpats = [
150 150 [
151 151 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
152 152 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
153 153 "use regex test output patterns instead of sed"),
154 154 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
155 155 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
156 156 (uprefix + r'.*\|\| echo.*(fail|error)',
157 157 "explicit exit code checks unnecessary"),
158 158 (uprefix + r'set -e', "don't use set -e"),
159 159 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
160 160 (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite "
161 161 "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx
162 162 '# no-msys'), # in test-pull.t which is skipped on windows
163 163 (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg),
164 164 (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$',
165 165 winglobmsg),
166 166 (r'^ pulling from \$TESTTMP/.*[^)]$', winglobmsg,
167 167 '\$TESTTMP/unix-repo$'), # in test-issue1802.t which skipped on windows
168 168 (r'^ reverting (?!subrepo ).*/.*[^)]$', winglobmsg),
169 169 (r'^ cloning subrepo \S+/.*[^)]$', winglobmsg),
170 170 (r'^ pushing to \$TESTTMP/.*[^)]$', winglobmsg),
171 171 (r'^ pushing subrepo \S+/\S+ to.*[^)]$', winglobmsg),
172 172 (r'^ moving \S+/.*[^)]$', winglobmsg),
173 173 (r'^ no changes made to subrepo since.*/.*[^)]$', winglobmsg),
174 174 (r'^ .*: largefile \S+ not available from file:.*/.*[^)]$', winglobmsg),
175 175 (r'^ .*file://\$TESTTMP',
176 176 'write "file:/*/$TESTTMP" + (glob) to match on windows too'),
177 177 (r'^ (cat|find): .*: No such file or directory',
178 178 'use test -f to test for file existence'),
179 179 (r'^ diff -[^ -]*p',
180 180 "don't use (external) diff with -p for portability"),
181 181 (r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
182 182 "glob timezone field in diff output for portability"),
183 183 (r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
184 184 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability"),
185 185 (r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
186 186 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability"),
187 187 (r'^ @@ -[0-9]+ [+][0-9]+ @@',
188 188 "use '@@ -N* +N* @@ (glob)' style chunk header for portability"),
189 189 (uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
190 190 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
191 191 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)"),
192 192 ],
193 193 # warnings
194 194 [
195 195 (r'^ [^*?/\n]* \(glob\)$',
196 196 "glob match with no glob character (?*/)"),
197 197 ]
198 198 ]
199 199
200 200 for i in [0, 1]:
201 201 for tp in testpats[i]:
202 202 p = tp[0]
203 203 m = tp[1]
204 204 if p.startswith(r'^'):
205 205 p = r"^ [$>] (%s)" % p[1:]
206 206 else:
207 207 p = r"^ [$>] .*(%s)" % p
208 208 utestpats[i].append((p, m) + tp[2:])
209 209
210 210 utestfilters = [
211 211 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
212 212 (r"( *)(#([^\n]*\S)?)", repcomment),
213 213 ]
214 214
215 215 pypats = [
216 216 [
217 217 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
218 218 "tuple parameter unpacking not available in Python 3+"),
219 219 (r'lambda\s*\(.*,.*\)',
220 220 "tuple parameter unpacking not available in Python 3+"),
221 (r'import (.+,[^.]+\.[^.]+|[^.]+\.[^.]+,)',
222 '2to3 can\'t always rewrite "import qux, foo.bar", '
223 'use "import foo.bar" on its own line instead.'),
224 221 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
225 222 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
226 223 (r'dict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
227 224 'dict-from-generator'),
228 225 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
229 226 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
230 227 (r'^\s*\t', "don't use tabs"),
231 228 (r'\S;\s*\n', "semicolon"),
232 229 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
233 230 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
234 231 (r'(\w|\)),\w', "missing whitespace after ,"),
235 232 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
236 233 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
237 234 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
238 235 (r'.{81}', "line too long"),
239 236 (r' x+[xo][\'"]\n\s+[\'"]x', 'string join across lines with no space'),
240 237 (r'[^\n]\Z', "no trailing newline"),
241 238 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
242 239 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
243 240 # "don't use underbars in identifiers"),
244 241 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
245 242 "don't use camelcase in identifiers"),
246 243 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
247 244 "linebreak after :"),
248 245 (r'class\s[^( \n]+:', "old-style class, use class foo(object)",
249 246 r'#.*old-style'),
250 247 (r'class\s[^( \n]+\(\):',
251 248 "class foo() creates old style object, use class foo(object)",
252 249 r'#.*old-style'),
253 250 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
254 251 if k not in ('print', 'exec')),
255 252 "Python keyword is not a function"),
256 253 (r',]', "unneeded trailing ',' in list"),
257 254 # (r'class\s[A-Z][^\(]*\((?!Exception)',
258 255 # "don't capitalize non-exception classes"),
259 256 # (r'in range\(', "use xrange"),
260 257 # (r'^\s*print\s+', "avoid using print in core and extensions"),
261 258 (r'[\x80-\xff]', "non-ASCII character literal"),
262 259 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
263 260 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
264 261 "gratuitous whitespace after Python keyword"),
265 262 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
266 263 # (r'\s\s=', "gratuitous whitespace before ="),
267 264 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
268 265 "missing whitespace around operator"),
269 266 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
270 267 "missing whitespace around operator"),
271 268 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
272 269 "missing whitespace around operator"),
273 270 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
274 271 "wrong whitespace around ="),
275 272 (r'\([^()]*( =[^=]|[^<>!=]= )',
276 273 "no whitespace around = for named parameters"),
277 274 (r'raise Exception', "don't raise generic exceptions"),
278 275 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
279 276 "don't use old-style two-argument raise, use Exception(message)"),
280 277 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
281 278 (r' [=!]=\s+(True|False|None)',
282 279 "comparison with singleton, use 'is' or 'is not' instead"),
283 280 (r'^\s*(while|if) [01]:',
284 281 "use True/False for constant Boolean expression"),
285 282 (r'(?:(?<!def)\s+|\()hasattr',
286 283 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
287 284 (r'opener\([^)]*\).read\(',
288 285 "use opener.read() instead"),
289 286 (r'opener\([^)]*\).write\(',
290 287 "use opener.write() instead"),
291 288 (r'[\s\(](open|file)\([^)]*\)\.read\(',
292 289 "use util.readfile() instead"),
293 290 (r'[\s\(](open|file)\([^)]*\)\.write\(',
294 291 "use util.writefile() instead"),
295 292 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
296 293 "always assign an opened file to a variable, and close it afterwards"),
297 294 (r'[\s\(](open|file)\([^)]*\)\.',
298 295 "always assign an opened file to a variable, and close it afterwards"),
299 296 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
300 297 (r'\.debug\(\_', "don't mark debug messages for translation"),
301 298 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
302 299 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
303 300 (r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
304 301 'legacy exception syntax; use "as" instead of ","'),
305 302 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
306 303 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
307 304 "missing _() in ui message (use () to hide false-positives)"),
308 305 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
309 306 (r'\b__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
310 307 (r'os\.path\.join\(.*, *(""|\'\')\)',
311 308 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
312 309 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
313 310 # XXX only catch mutable arguments on the first line of the definition
314 311 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
315 312 (r'\butil\.Abort\b', "directly use error.Abort"),
316 313 ],
317 314 # warnings
318 315 [
319 316 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
320 317 ]
321 318 ]
322 319
323 320 pyfilters = [
324 321 (r"""(?msx)(?P<comment>\#.*?$)|
325 322 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
326 323 (?P<text>(([^\\]|\\.)*?))
327 324 (?P=quote))""", reppython),
328 325 ]
329 326
330 327 txtfilters = []
331 328
332 329 txtpats = [
333 330 [
334 331 ('\s$', 'trailing whitespace'),
335 332 ('.. note::[ \n][^\n]', 'add two newlines after note::')
336 333 ],
337 334 []
338 335 ]
339 336
340 337 cpats = [
341 338 [
342 339 (r'//', "don't use //-style comments"),
343 340 (r'^ ', "don't use spaces to indent"),
344 341 (r'\S\t', "don't use tabs except for indent"),
345 342 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
346 343 (r'.{81}', "line too long"),
347 344 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
348 345 (r'return\(', "return is not a function"),
349 346 (r' ;', "no space before ;"),
350 347 (r'[^;] \)', "no space before )"),
351 348 (r'[)][{]', "space between ) and {"),
352 349 (r'\w+\* \w+', "use int *foo, not int* foo"),
353 350 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
354 351 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
355 352 (r'\w,\w', "missing whitespace after ,"),
356 353 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
357 354 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
358 355 (r'^#\s+\w', "use #foo, not # foo"),
359 356 (r'[^\n]\Z', "no trailing newline"),
360 357 (r'^\s*#import\b', "use only #include in standard C code"),
361 358 ],
362 359 # warnings
363 360 []
364 361 ]
365 362
366 363 cfilters = [
367 364 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
368 365 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
369 366 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
370 367 (r'(\()([^)]+\))', repcallspaces),
371 368 ]
372 369
373 370 inutilpats = [
374 371 [
375 372 (r'\bui\.', "don't use ui in util"),
376 373 ],
377 374 # warnings
378 375 []
379 376 ]
380 377
381 378 inrevlogpats = [
382 379 [
383 380 (r'\brepo\.', "don't use repo in revlog"),
384 381 ],
385 382 # warnings
386 383 []
387 384 ]
388 385
389 386 webtemplatefilters = []
390 387
391 388 webtemplatepats = [
392 389 [],
393 390 [
394 391 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
395 392 'follow desc keyword with either firstline or websub'),
396 393 ]
397 394 ]
398 395
399 396 checks = [
400 397 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
401 398 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
402 399 ('c', r'.*\.[ch]$', '', cfilters, cpats),
403 400 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
404 401 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
405 402 pyfilters, inrevlogpats),
406 403 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
407 404 inutilpats),
408 405 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
409 406 ('web template', r'mercurial/templates/.*\.tmpl', '',
410 407 webtemplatefilters, webtemplatepats),
411 408 ]
412 409
413 410 def _preparepats():
414 411 for c in checks:
415 412 failandwarn = c[-1]
416 413 for pats in failandwarn:
417 414 for i, pseq in enumerate(pats):
418 415 # fix-up regexes for multi-line searches
419 416 p = pseq[0]
420 417 # \s doesn't match \n
421 418 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
422 419 # [^...] doesn't match newline
423 420 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
424 421
425 422 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
426 423 filters = c[3]
427 424 for i, flt in enumerate(filters):
428 425 filters[i] = re.compile(flt[0]), flt[1]
429 426 _preparepats()
430 427
431 428 class norepeatlogger(object):
432 429 def __init__(self):
433 430 self._lastseen = None
434 431
435 432 def log(self, fname, lineno, line, msg, blame):
436 433 """print error related a to given line of a given file.
437 434
438 435 The faulty line will also be printed but only once in the case
439 436 of multiple errors.
440 437
441 438 :fname: filename
442 439 :lineno: line number
443 440 :line: actual content of the line
444 441 :msg: error message
445 442 """
446 443 msgid = fname, lineno, line
447 444 if msgid != self._lastseen:
448 445 if blame:
449 446 print "%s:%d (%s):" % (fname, lineno, blame)
450 447 else:
451 448 print "%s:%d:" % (fname, lineno)
452 449 print " > %s" % line
453 450 self._lastseen = msgid
454 451 print " " + msg
455 452
456 453 _defaultlogger = norepeatlogger()
457 454
458 455 def getblame(f):
459 456 lines = []
460 457 for l in os.popen('hg annotate -un %s' % f):
461 458 start, line = l.split(':', 1)
462 459 user, rev = start.split()
463 460 lines.append((line[1:-1], user, rev))
464 461 return lines
465 462
466 463 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
467 464 blame=False, debug=False, lineno=True):
468 465 """checks style and portability of a given file
469 466
470 467 :f: filepath
471 468 :logfunc: function used to report error
472 469 logfunc(filename, linenumber, linecontent, errormessage)
473 470 :maxerr: number of error to display before aborting.
474 471 Set to false (default) to report all errors
475 472
476 473 return True if no error is found, False otherwise.
477 474 """
478 475 blamecache = None
479 476 result = True
480 477
481 478 try:
482 479 fp = open(f)
483 480 except IOError as e:
484 481 print "Skipping %s, %s" % (f, str(e).split(':', 1)[0])
485 482 return result
486 483 pre = post = fp.read()
487 484 fp.close()
488 485
489 486 for name, match, magic, filters, pats in checks:
490 487 if debug:
491 488 print name, f
492 489 fc = 0
493 490 if not (re.match(match, f) or (magic and re.search(magic, pre))):
494 491 if debug:
495 492 print "Skipping %s for %s it doesn't match %s" % (
496 493 name, match, f)
497 494 continue
498 495 if "no-" "check-code" in pre:
499 496 # If you're looking at this line, it's because a file has:
500 497 # no- check- code
501 498 # but the reason to output skipping is to make life for
502 499 # tests easier. So, instead of writing it with a normal
503 500 # spelling, we write it with the expected spelling from
504 501 # tests/test-check-code.t
505 502 print "Skipping %s it has no-che?k-code (glob)" % f
506 503 return "Skip" # skip checking this file
507 504 for p, r in filters:
508 505 post = re.sub(p, r, post)
509 506 nerrs = len(pats[0]) # nerr elements are errors
510 507 if warnings:
511 508 pats = pats[0] + pats[1]
512 509 else:
513 510 pats = pats[0]
514 511 # print post # uncomment to show filtered version
515 512
516 513 if debug:
517 514 print "Checking %s for %s" % (name, f)
518 515
519 516 prelines = None
520 517 errors = []
521 518 for i, pat in enumerate(pats):
522 519 if len(pat) == 3:
523 520 p, msg, ignore = pat
524 521 else:
525 522 p, msg = pat
526 523 ignore = None
527 524 if i >= nerrs:
528 525 msg = "warning: " + msg
529 526
530 527 pos = 0
531 528 n = 0
532 529 for m in p.finditer(post):
533 530 if prelines is None:
534 531 prelines = pre.splitlines()
535 532 postlines = post.splitlines(True)
536 533
537 534 start = m.start()
538 535 while n < len(postlines):
539 536 step = len(postlines[n])
540 537 if pos + step > start:
541 538 break
542 539 pos += step
543 540 n += 1
544 541 l = prelines[n]
545 542
546 543 if ignore and re.search(ignore, l, re.MULTILINE):
547 544 if debug:
548 545 print "Skipping %s for %s:%s (ignore pattern)" % (
549 546 name, f, n)
550 547 continue
551 548 bd = ""
552 549 if blame:
553 550 bd = 'working directory'
554 551 if not blamecache:
555 552 blamecache = getblame(f)
556 553 if n < len(blamecache):
557 554 bl, bu, br = blamecache[n]
558 555 if bl == l:
559 556 bd = '%s@%s' % (bu, br)
560 557
561 558 errors.append((f, lineno and n + 1, l, msg, bd))
562 559 result = False
563 560
564 561 errors.sort()
565 562 for e in errors:
566 563 logfunc(*e)
567 564 fc += 1
568 565 if maxerr and fc >= maxerr:
569 566 print " (too many errors, giving up)"
570 567 break
571 568
572 569 return result
573 570
574 571 if __name__ == "__main__":
575 572 parser = optparse.OptionParser("%prog [options] [files]")
576 573 parser.add_option("-w", "--warnings", action="store_true",
577 574 help="include warning-level checks")
578 575 parser.add_option("-p", "--per-file", type="int",
579 576 help="max warnings per file")
580 577 parser.add_option("-b", "--blame", action="store_true",
581 578 help="use annotate to generate blame info")
582 579 parser.add_option("", "--debug", action="store_true",
583 580 help="show debug information")
584 581 parser.add_option("", "--nolineno", action="store_false",
585 582 dest='lineno', help="don't show line numbers")
586 583
587 584 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
588 585 lineno=True)
589 586 (options, args) = parser.parse_args()
590 587
591 588 if len(args) == 0:
592 589 check = glob.glob("*")
593 590 else:
594 591 check = args
595 592
596 593 ret = 0
597 594 for f in check:
598 595 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
599 596 blame=options.blame, debug=options.debug,
600 597 lineno=options.lineno):
601 598 ret = 1
602 599 sys.exit(ret)
General Comments 0
You need to be logged in to leave comments. Login now