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