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