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