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