##// END OF EJS Templates
check-code: prevent use of strcpy
Augie Fackler -
r28594:d3990da5 default
parent child Browse files
Show More
@@ -1,603 +1,604
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 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
362 363 ],
363 364 # warnings
364 365 []
365 366 ]
366 367
367 368 cfilters = [
368 369 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
369 370 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
370 371 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
371 372 (r'(\()([^)]+\))', repcallspaces),
372 373 ]
373 374
374 375 inutilpats = [
375 376 [
376 377 (r'\bui\.', "don't use ui in util"),
377 378 ],
378 379 # warnings
379 380 []
380 381 ]
381 382
382 383 inrevlogpats = [
383 384 [
384 385 (r'\brepo\.', "don't use repo in revlog"),
385 386 ],
386 387 # warnings
387 388 []
388 389 ]
389 390
390 391 webtemplatefilters = []
391 392
392 393 webtemplatepats = [
393 394 [],
394 395 [
395 396 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
396 397 'follow desc keyword with either firstline or websub'),
397 398 ]
398 399 ]
399 400
400 401 checks = [
401 402 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
402 403 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
403 404 ('c', r'.*\.[ch]$', '', cfilters, cpats),
404 405 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
405 406 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
406 407 pyfilters, inrevlogpats),
407 408 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
408 409 inutilpats),
409 410 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
410 411 ('web template', r'mercurial/templates/.*\.tmpl', '',
411 412 webtemplatefilters, webtemplatepats),
412 413 ]
413 414
414 415 def _preparepats():
415 416 for c in checks:
416 417 failandwarn = c[-1]
417 418 for pats in failandwarn:
418 419 for i, pseq in enumerate(pats):
419 420 # fix-up regexes for multi-line searches
420 421 p = pseq[0]
421 422 # \s doesn't match \n
422 423 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
423 424 # [^...] doesn't match newline
424 425 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
425 426
426 427 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
427 428 filters = c[3]
428 429 for i, flt in enumerate(filters):
429 430 filters[i] = re.compile(flt[0]), flt[1]
430 431 _preparepats()
431 432
432 433 class norepeatlogger(object):
433 434 def __init__(self):
434 435 self._lastseen = None
435 436
436 437 def log(self, fname, lineno, line, msg, blame):
437 438 """print error related a to given line of a given file.
438 439
439 440 The faulty line will also be printed but only once in the case
440 441 of multiple errors.
441 442
442 443 :fname: filename
443 444 :lineno: line number
444 445 :line: actual content of the line
445 446 :msg: error message
446 447 """
447 448 msgid = fname, lineno, line
448 449 if msgid != self._lastseen:
449 450 if blame:
450 451 print("%s:%d (%s):" % (fname, lineno, blame))
451 452 else:
452 453 print("%s:%d:" % (fname, lineno))
453 454 print(" > %s" % line)
454 455 self._lastseen = msgid
455 456 print(" " + msg)
456 457
457 458 _defaultlogger = norepeatlogger()
458 459
459 460 def getblame(f):
460 461 lines = []
461 462 for l in os.popen('hg annotate -un %s' % f):
462 463 start, line = l.split(':', 1)
463 464 user, rev = start.split()
464 465 lines.append((line[1:-1], user, rev))
465 466 return lines
466 467
467 468 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
468 469 blame=False, debug=False, lineno=True):
469 470 """checks style and portability of a given file
470 471
471 472 :f: filepath
472 473 :logfunc: function used to report error
473 474 logfunc(filename, linenumber, linecontent, errormessage)
474 475 :maxerr: number of error to display before aborting.
475 476 Set to false (default) to report all errors
476 477
477 478 return True if no error is found, False otherwise.
478 479 """
479 480 blamecache = None
480 481 result = True
481 482
482 483 try:
483 484 fp = open(f)
484 485 except IOError as e:
485 486 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
486 487 return result
487 488 pre = post = fp.read()
488 489 fp.close()
489 490
490 491 for name, match, magic, filters, pats in checks:
491 492 if debug:
492 493 print(name, f)
493 494 fc = 0
494 495 if not (re.match(match, f) or (magic and re.search(magic, pre))):
495 496 if debug:
496 497 print("Skipping %s for %s it doesn't match %s" % (
497 498 name, match, f))
498 499 continue
499 500 if "no-" "check-code" in pre:
500 501 # If you're looking at this line, it's because a file has:
501 502 # no- check- code
502 503 # but the reason to output skipping is to make life for
503 504 # tests easier. So, instead of writing it with a normal
504 505 # spelling, we write it with the expected spelling from
505 506 # tests/test-check-code.t
506 507 print("Skipping %s it has no-che?k-code (glob)" % f)
507 508 return "Skip" # skip checking this file
508 509 for p, r in filters:
509 510 post = re.sub(p, r, post)
510 511 nerrs = len(pats[0]) # nerr elements are errors
511 512 if warnings:
512 513 pats = pats[0] + pats[1]
513 514 else:
514 515 pats = pats[0]
515 516 # print post # uncomment to show filtered version
516 517
517 518 if debug:
518 519 print("Checking %s for %s" % (name, f))
519 520
520 521 prelines = None
521 522 errors = []
522 523 for i, pat in enumerate(pats):
523 524 if len(pat) == 3:
524 525 p, msg, ignore = pat
525 526 else:
526 527 p, msg = pat
527 528 ignore = None
528 529 if i >= nerrs:
529 530 msg = "warning: " + msg
530 531
531 532 pos = 0
532 533 n = 0
533 534 for m in p.finditer(post):
534 535 if prelines is None:
535 536 prelines = pre.splitlines()
536 537 postlines = post.splitlines(True)
537 538
538 539 start = m.start()
539 540 while n < len(postlines):
540 541 step = len(postlines[n])
541 542 if pos + step > start:
542 543 break
543 544 pos += step
544 545 n += 1
545 546 l = prelines[n]
546 547
547 548 if ignore and re.search(ignore, l, re.MULTILINE):
548 549 if debug:
549 550 print("Skipping %s for %s:%s (ignore pattern)" % (
550 551 name, f, n))
551 552 continue
552 553 bd = ""
553 554 if blame:
554 555 bd = 'working directory'
555 556 if not blamecache:
556 557 blamecache = getblame(f)
557 558 if n < len(blamecache):
558 559 bl, bu, br = blamecache[n]
559 560 if bl == l:
560 561 bd = '%s@%s' % (bu, br)
561 562
562 563 errors.append((f, lineno and n + 1, l, msg, bd))
563 564 result = False
564 565
565 566 errors.sort()
566 567 for e in errors:
567 568 logfunc(*e)
568 569 fc += 1
569 570 if maxerr and fc >= maxerr:
570 571 print(" (too many errors, giving up)")
571 572 break
572 573
573 574 return result
574 575
575 576 if __name__ == "__main__":
576 577 parser = optparse.OptionParser("%prog [options] [files]")
577 578 parser.add_option("-w", "--warnings", action="store_true",
578 579 help="include warning-level checks")
579 580 parser.add_option("-p", "--per-file", type="int",
580 581 help="max warnings per file")
581 582 parser.add_option("-b", "--blame", action="store_true",
582 583 help="use annotate to generate blame info")
583 584 parser.add_option("", "--debug", action="store_true",
584 585 help="show debug information")
585 586 parser.add_option("", "--nolineno", action="store_false",
586 587 dest='lineno', help="don't show line numbers")
587 588
588 589 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
589 590 lineno=True)
590 591 (options, args) = parser.parse_args()
591 592
592 593 if len(args) == 0:
593 594 check = glob.glob("*")
594 595 else:
595 596 check = args
596 597
597 598 ret = 0
598 599 for f in check:
599 600 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
600 601 blame=options.blame, debug=options.debug,
601 602 lineno=options.lineno):
602 603 ret = 1
603 604 sys.exit(ret)
@@ -1,234 +1,250
1 1 $ cat > correct.py <<EOF
2 2 > def toto(arg1, arg2):
3 3 > del arg2
4 4 > return (5 + 6, 9)
5 5 > EOF
6 6 $ cat > wrong.py <<EOF
7 7 > def toto( arg1, arg2):
8 8 > del(arg2)
9 9 > return ( 5+6, 9)
10 10 > EOF
11 11 $ cat > quote.py <<EOF
12 12 > # let's use quote in comments
13 13 > (''' ( 4x5 )
14 14 > but """\\''' and finally''',
15 15 > """let's fool checkpatch""", '1+2',
16 16 > '"""', 42+1, """and
17 17 > ( 4-1 ) """, "( 1+1 )\" and ")
18 18 > a, '\\\\\\\\', "\\\\\\" x-2", "c-1"
19 19 > EOF
20 20 $ cat > classstyle.py <<EOF
21 21 > class newstyle_class(object):
22 22 > pass
23 23 >
24 24 > class oldstyle_class:
25 25 > pass
26 26 >
27 27 > class empty():
28 28 > pass
29 29 >
30 30 > no_class = 1:
31 31 > pass
32 32 > EOF
33 33 $ check_code="$TESTDIR"/../contrib/check-code.py
34 34 $ "$check_code" ./wrong.py ./correct.py ./quote.py ./classstyle.py
35 35 ./wrong.py:1:
36 36 > def toto( arg1, arg2):
37 37 gratuitous whitespace in () or []
38 38 ./wrong.py:2:
39 39 > del(arg2)
40 40 Python keyword is not a function
41 41 ./wrong.py:3:
42 42 > return ( 5+6, 9)
43 43 gratuitous whitespace in () or []
44 44 missing whitespace in expression
45 45 ./quote.py:5:
46 46 > '"""', 42+1, """and
47 47 missing whitespace in expression
48 48 ./classstyle.py:4:
49 49 > class oldstyle_class:
50 50 old-style class, use class foo(object)
51 51 ./classstyle.py:7:
52 52 > class empty():
53 53 class foo() creates old style object, use class foo(object)
54 54 [1]
55 55 $ cat > python3-compat.py << EOF
56 56 > foo <> bar
57 57 > reduce(lambda a, b: a + b, [1, 2, 3, 4])
58 58 > dict(key=value)
59 59 > EOF
60 60 $ "$check_code" python3-compat.py
61 61 python3-compat.py:1:
62 62 > foo <> bar
63 63 <> operator is not available in Python 3+, use !=
64 64 python3-compat.py:2:
65 65 > reduce(lambda a, b: a + b, [1, 2, 3, 4])
66 66 reduce is not available in Python 3+
67 67 python3-compat.py:3:
68 68 > dict(key=value)
69 69 dict() is different in Py2 and 3 and is slower than {}
70 70 [1]
71 71
72 $ cat > foo.c <<EOF
73 > void narf() {
74 > strcpy(foo, bar);
75 > // strcpy_s is okay, but this comment is not
76 > strcpy_s(foo, bar);
77 > }
78 > EOF
79 $ "$check_code" ./foo.c
80 ./foo.c:2:
81 > strcpy(foo, bar);
82 don't use strcpy, use strlcpy or memcpy
83 ./foo.c:3:
84 > // strcpy_s is okay, but this comment is not
85 don't use //-style comments
86 [1]
87
72 88 $ cat > is-op.py <<EOF
73 89 > # is-operator comparing number or string literal
74 90 > x = None
75 91 > y = x is 'foo'
76 92 > y = x is "foo"
77 93 > y = x is 5346
78 94 > y = x is -6
79 95 > y = x is not 'foo'
80 96 > y = x is not "foo"
81 97 > y = x is not 5346
82 98 > y = x is not -6
83 99 > EOF
84 100
85 101 $ "$check_code" ./is-op.py
86 102 ./is-op.py:3:
87 103 > y = x is 'foo'
88 104 object comparison with literal
89 105 ./is-op.py:4:
90 106 > y = x is "foo"
91 107 object comparison with literal
92 108 ./is-op.py:5:
93 109 > y = x is 5346
94 110 object comparison with literal
95 111 ./is-op.py:6:
96 112 > y = x is -6
97 113 object comparison with literal
98 114 ./is-op.py:7:
99 115 > y = x is not 'foo'
100 116 object comparison with literal
101 117 ./is-op.py:8:
102 118 > y = x is not "foo"
103 119 object comparison with literal
104 120 ./is-op.py:9:
105 121 > y = x is not 5346
106 122 object comparison with literal
107 123 ./is-op.py:10:
108 124 > y = x is not -6
109 125 object comparison with literal
110 126 [1]
111 127
112 128 $ cat > for-nolineno.py <<EOF
113 129 > except:
114 130 > EOF
115 131 $ "$check_code" for-nolineno.py --nolineno
116 132 for-nolineno.py:0:
117 133 > except:
118 134 naked except clause
119 135 [1]
120 136
121 137 $ cat > warning.t <<EOF
122 138 > $ function warnonly {
123 139 > > }
124 140 > $ diff -N aaa
125 141 > $ function onwarn {}
126 142 > EOF
127 143 $ "$check_code" warning.t
128 144 $ "$check_code" --warn warning.t
129 145 warning.t:1:
130 146 > $ function warnonly {
131 147 warning: don't use 'function', use old style
132 148 warning.t:3:
133 149 > $ diff -N aaa
134 150 warning: don't use 'diff -N'
135 151 warning.t:4:
136 152 > $ function onwarn {}
137 153 warning: don't use 'function', use old style
138 154 [1]
139 155 $ cat > raise-format.py <<EOF
140 156 > raise SomeException, message
141 157 > # this next line is okay
142 158 > raise SomeException(arg1, arg2)
143 159 > EOF
144 160 $ "$check_code" not-existing.py raise-format.py
145 161 Skipping*not-existing.py* (glob)
146 162 raise-format.py:1:
147 163 > raise SomeException, message
148 164 don't use old-style two-argument raise, use Exception(message)
149 165 [1]
150 166
151 167 $ cat > rst.py <<EOF
152 168 > """problematic rst text
153 169 >
154 170 > .. note::
155 171 > wrong
156 172 > """
157 173 >
158 174 > '''
159 175 >
160 176 > .. note::
161 177 >
162 178 > valid
163 179 >
164 180 > new text
165 181 >
166 182 > .. note::
167 183 >
168 184 > also valid
169 185 > '''
170 186 >
171 187 > """mixed
172 188 >
173 189 > .. note::
174 190 >
175 191 > good
176 192 >
177 193 > .. note::
178 194 > plus bad
179 195 > """
180 196 > EOF
181 197 $ $check_code -w rst.py
182 198 rst.py:3:
183 199 > .. note::
184 200 warning: add two newlines after '.. note::'
185 201 rst.py:26:
186 202 > .. note::
187 203 warning: add two newlines after '.. note::'
188 204 [1]
189 205
190 206 $ cat > ./map-inside-gettext.py <<EOF
191 207 > print _("map inside gettext %s" % v)
192 208 >
193 209 > print _("concatenating " " by " " space %s" % v)
194 210 > print _("concatenating " + " by " + " '+' %s" % v)
195 211 >
196 212 > print _("mapping operation in different line %s"
197 213 > % v)
198 214 >
199 215 > print _(
200 216 > "leading spaces inside of '(' %s" % v)
201 217 > EOF
202 218 $ "$check_code" ./map-inside-gettext.py
203 219 ./map-inside-gettext.py:1:
204 220 > print _("map inside gettext %s" % v)
205 221 don't use % inside _()
206 222 ./map-inside-gettext.py:3:
207 223 > print _("concatenating " " by " " space %s" % v)
208 224 don't use % inside _()
209 225 ./map-inside-gettext.py:4:
210 226 > print _("concatenating " + " by " + " '+' %s" % v)
211 227 don't use % inside _()
212 228 ./map-inside-gettext.py:6:
213 229 > print _("mapping operation in different line %s"
214 230 don't use % inside _()
215 231 ./map-inside-gettext.py:9:
216 232 > print _(
217 233 don't use % inside _()
218 234 [1]
219 235
220 236 web templates
221 237
222 238 $ mkdir -p mercurial/templates
223 239 $ cat > mercurial/templates/example.tmpl <<EOF
224 240 > {desc}
225 241 > {desc|escape}
226 242 > {desc|firstline}
227 243 > {desc|websub}
228 244 > EOF
229 245
230 246 $ "$check_code" --warnings mercurial/templates/example.tmpl
231 247 mercurial/templates/example.tmpl:2:
232 248 > {desc|escape}
233 249 warning: follow desc keyword with either firstline or websub
234 250 [1]
General Comments 0
You need to be logged in to leave comments. Login now