##// END OF EJS Templates
check-commit: allow foo_bar naming in functions...
Gregory Szorc -
r43382:24a07347 default
parent child Browse files
Show More
@@ -1,1107 +1,1105 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # check-code - a style and portability checker for Mercurial
3 # check-code - a style and portability checker for Mercurial
4 #
4 #
5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 """style and portability checker for Mercurial
10 """style and portability checker for Mercurial
11
11
12 when a rule triggers wrong, do one of the following (prefer one from top):
12 when a rule triggers wrong, do one of the following (prefer one from top):
13 * do the work-around the rule suggests
13 * do the work-around the rule suggests
14 * doublecheck that it is a false match
14 * doublecheck that it is a false match
15 * improve the rule pattern
15 * improve the rule pattern
16 * add an ignore pattern to the rule (3rd arg) which matches your good line
16 * add an ignore pattern to the rule (3rd arg) which matches your good line
17 (you can append a short comment and match this, like: #re-raises)
17 (you can append a short comment and match this, like: #re-raises)
18 * change the pattern to a warning and list the exception in test-check-code-hg
18 * change the pattern to a warning and list the exception in test-check-code-hg
19 * ONLY use no--check-code for skipping entire files from external sources
19 * ONLY use no--check-code for skipping entire files from external sources
20 """
20 """
21
21
22 from __future__ import absolute_import, print_function
22 from __future__ import absolute_import, print_function
23 import glob
23 import glob
24 import keyword
24 import keyword
25 import optparse
25 import optparse
26 import os
26 import os
27 import re
27 import re
28 import sys
28 import sys
29
29
30 if sys.version_info[0] < 3:
30 if sys.version_info[0] < 3:
31 opentext = open
31 opentext = open
32 else:
32 else:
33
33
34 def opentext(f):
34 def opentext(f):
35 return open(f, encoding='latin1')
35 return open(f, encoding='latin1')
36
36
37
37
38 try:
38 try:
39 xrange
39 xrange
40 except NameError:
40 except NameError:
41 xrange = range
41 xrange = range
42 try:
42 try:
43 import re2
43 import re2
44 except ImportError:
44 except ImportError:
45 re2 = None
45 re2 = None
46
46
47 import testparseutil
47 import testparseutil
48
48
49
49
50 def compilere(pat, multiline=False):
50 def compilere(pat, multiline=False):
51 if multiline:
51 if multiline:
52 pat = '(?m)' + pat
52 pat = '(?m)' + pat
53 if re2:
53 if re2:
54 try:
54 try:
55 return re2.compile(pat)
55 return re2.compile(pat)
56 except re2.error:
56 except re2.error:
57 pass
57 pass
58 return re.compile(pat)
58 return re.compile(pat)
59
59
60
60
61 # check "rules depending on implementation of repquote()" in each
61 # check "rules depending on implementation of repquote()" in each
62 # patterns (especially pypats), before changing around repquote()
62 # patterns (especially pypats), before changing around repquote()
63 _repquotefixedmap = {
63 _repquotefixedmap = {
64 ' ': ' ',
64 ' ': ' ',
65 '\n': '\n',
65 '\n': '\n',
66 '.': 'p',
66 '.': 'p',
67 ':': 'q',
67 ':': 'q',
68 '%': '%',
68 '%': '%',
69 '\\': 'b',
69 '\\': 'b',
70 '*': 'A',
70 '*': 'A',
71 '+': 'P',
71 '+': 'P',
72 '-': 'M',
72 '-': 'M',
73 }
73 }
74
74
75
75
76 def _repquoteencodechr(i):
76 def _repquoteencodechr(i):
77 if i > 255:
77 if i > 255:
78 return 'u'
78 return 'u'
79 c = chr(i)
79 c = chr(i)
80 if c in _repquotefixedmap:
80 if c in _repquotefixedmap:
81 return _repquotefixedmap[c]
81 return _repquotefixedmap[c]
82 if c.isalpha():
82 if c.isalpha():
83 return 'x'
83 return 'x'
84 if c.isdigit():
84 if c.isdigit():
85 return 'n'
85 return 'n'
86 return 'o'
86 return 'o'
87
87
88
88
89 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
89 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
90
90
91
91
92 def repquote(m):
92 def repquote(m):
93 t = m.group('text')
93 t = m.group('text')
94 t = t.translate(_repquotett)
94 t = t.translate(_repquotett)
95 return m.group('quote') + t + m.group('quote')
95 return m.group('quote') + t + m.group('quote')
96
96
97
97
98 def reppython(m):
98 def reppython(m):
99 comment = m.group('comment')
99 comment = m.group('comment')
100 if comment:
100 if comment:
101 l = len(comment.rstrip())
101 l = len(comment.rstrip())
102 return "#" * l + comment[l:]
102 return "#" * l + comment[l:]
103 return repquote(m)
103 return repquote(m)
104
104
105
105
106 def repcomment(m):
106 def repcomment(m):
107 return m.group(1) + "#" * len(m.group(2))
107 return m.group(1) + "#" * len(m.group(2))
108
108
109
109
110 def repccomment(m):
110 def repccomment(m):
111 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
111 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
112 return m.group(1) + t + "*/"
112 return m.group(1) + t + "*/"
113
113
114
114
115 def repcallspaces(m):
115 def repcallspaces(m):
116 t = re.sub(r"\n\s+", "\n", m.group(2))
116 t = re.sub(r"\n\s+", "\n", m.group(2))
117 return m.group(1) + t
117 return m.group(1) + t
118
118
119
119
120 def repinclude(m):
120 def repinclude(m):
121 return m.group(1) + "<foo>"
121 return m.group(1) + "<foo>"
122
122
123
123
124 def rephere(m):
124 def rephere(m):
125 t = re.sub(r"\S", "x", m.group(2))
125 t = re.sub(r"\S", "x", m.group(2))
126 return m.group(1) + t
126 return m.group(1) + t
127
127
128
128
129 testpats = [
129 testpats = [
130 [
130 [
131 (r'\b(push|pop)d\b', "don't use 'pushd' or 'popd', use 'cd'"),
131 (r'\b(push|pop)d\b', "don't use 'pushd' or 'popd', use 'cd'"),
132 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
132 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
133 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
133 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
134 (r'(?<!hg )grep.* -a', "don't use 'grep -a', use in-line python"),
134 (r'(?<!hg )grep.* -a', "don't use 'grep -a', use in-line python"),
135 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
135 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
136 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
136 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
137 (r'echo -n', "don't use 'echo -n', use printf"),
137 (r'echo -n', "don't use 'echo -n', use printf"),
138 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
138 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
139 (r'head -c', "don't use 'head -c', use 'dd'"),
139 (r'head -c', "don't use 'head -c', use 'dd'"),
140 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
140 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
141 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
141 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
142 (r'\bls\b.*-\w*R', "don't use 'ls -R', use 'find'"),
142 (r'\bls\b.*-\w*R', "don't use 'ls -R', use 'find'"),
143 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
143 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
144 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
144 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
145 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
145 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
146 (
146 (
147 r'\[[^\]]+==',
147 r'\[[^\]]+==',
148 '[ foo == bar ] is a bashism, use [ foo = bar ] instead',
148 '[ foo == bar ] is a bashism, use [ foo = bar ] instead',
149 ),
149 ),
150 (
150 (
151 r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
151 r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
152 "use egrep for extended grep syntax",
152 "use egrep for extended grep syntax",
153 ),
153 ),
154 (r'(^|\|\s*)e?grep .*\\S', "don't use \\S in regular expression"),
154 (r'(^|\|\s*)e?grep .*\\S', "don't use \\S in regular expression"),
155 (r'(?<!!)/bin/', "don't use explicit paths for tools"),
155 (r'(?<!!)/bin/', "don't use explicit paths for tools"),
156 (r'#!.*/bash', "don't use bash in shebang, use sh"),
156 (r'#!.*/bash', "don't use bash in shebang, use sh"),
157 (r'[^\n]\Z', "no trailing newline"),
157 (r'[^\n]\Z', "no trailing newline"),
158 (r'export .*=', "don't export and assign at once"),
158 (r'export .*=', "don't export and assign at once"),
159 (r'^source\b', "don't use 'source', use '.'"),
159 (r'^source\b', "don't use 'source', use '.'"),
160 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
160 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
161 (r'\bls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
161 (r'\bls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
162 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
162 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
163 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
163 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
164 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
164 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
165 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
165 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
166 (r'^alias\b.*=', "don't use alias, use a function"),
166 (r'^alias\b.*=', "don't use alias, use a function"),
167 (r'if\s*!', "don't use '!' to negate exit status"),
167 (r'if\s*!', "don't use '!' to negate exit status"),
168 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
168 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
169 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
169 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
170 (
170 (
171 r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
171 r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
172 "put a backslash-escaped newline after sed 'i' command",
172 "put a backslash-escaped newline after sed 'i' command",
173 ),
173 ),
174 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
174 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
175 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
175 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
176 (r'[\s="`\']python\s(?!bindings)', "don't use 'python', use '$PYTHON'"),
176 (r'[\s="`\']python\s(?!bindings)', "don't use 'python', use '$PYTHON'"),
177 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
177 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
178 (r'\butil\.Abort\b', "directly use error.Abort"),
178 (r'\butil\.Abort\b', "directly use error.Abort"),
179 (r'\|&', "don't use |&, use 2>&1"),
179 (r'\|&', "don't use |&, use 2>&1"),
180 (r'\w = +\w', "only one space after = allowed"),
180 (r'\w = +\w', "only one space after = allowed"),
181 (
181 (
182 r'\bsed\b.*[^\\]\\n',
182 r'\bsed\b.*[^\\]\\n',
183 "don't use 'sed ... \\n', use a \\ and a newline",
183 "don't use 'sed ... \\n', use a \\ and a newline",
184 ),
184 ),
185 (r'env.*-u', "don't use 'env -u VAR', use 'unset VAR'"),
185 (r'env.*-u', "don't use 'env -u VAR', use 'unset VAR'"),
186 (r'cp.* -r ', "don't use 'cp -r', use 'cp -R'"),
186 (r'cp.* -r ', "don't use 'cp -r', use 'cp -R'"),
187 (r'grep.* -[ABC]', "don't use grep's context flags"),
187 (r'grep.* -[ABC]', "don't use grep's context flags"),
188 (
188 (
189 r'find.*-printf',
189 r'find.*-printf',
190 "don't use 'find -printf', it doesn't exist on BSD find(1)",
190 "don't use 'find -printf', it doesn't exist on BSD find(1)",
191 ),
191 ),
192 (r'\$RANDOM ', "don't use bash-only $RANDOM to generate random values"),
192 (r'\$RANDOM ', "don't use bash-only $RANDOM to generate random values"),
193 ],
193 ],
194 # warnings
194 # warnings
195 [
195 [
196 (r'^function', "don't use 'function', use old style"),
196 (r'^function', "don't use 'function', use old style"),
197 (r'^diff.*-\w*N', "don't use 'diff -N'"),
197 (r'^diff.*-\w*N', "don't use 'diff -N'"),
198 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
198 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
199 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
199 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
200 (r'kill (`|\$\()', "don't use kill, use killdaemons.py"),
200 (r'kill (`|\$\()', "don't use kill, use killdaemons.py"),
201 ],
201 ],
202 ]
202 ]
203
203
204 testfilters = [
204 testfilters = [
205 (r"( *)(#([^!][^\n]*\S)?)", repcomment),
205 (r"( *)(#([^!][^\n]*\S)?)", repcomment),
206 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
206 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
207 ]
207 ]
208
208
209 uprefix = r"^ \$ "
209 uprefix = r"^ \$ "
210 utestpats = [
210 utestpats = [
211 [
211 [
212 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
212 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
213 (
213 (
214 uprefix + r'.*\|\s*sed[^|>\n]*\n',
214 uprefix + r'.*\|\s*sed[^|>\n]*\n',
215 "use regex test output patterns instead of sed",
215 "use regex test output patterns instead of sed",
216 ),
216 ),
217 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
217 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
218 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
218 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
219 (
219 (
220 uprefix + r'.*\|\| echo.*(fail|error)',
220 uprefix + r'.*\|\| echo.*(fail|error)',
221 "explicit exit code checks unnecessary",
221 "explicit exit code checks unnecessary",
222 ),
222 ),
223 (uprefix + r'set -e', "don't use set -e"),
223 (uprefix + r'set -e', "don't use set -e"),
224 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
224 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
225 (
225 (
226 uprefix + r'.*:\.\S*/',
226 uprefix + r'.*:\.\S*/',
227 "x:.y in a path does not work on msys, rewrite "
227 "x:.y in a path does not work on msys, rewrite "
228 "as x://.y, or see `hg log -k msys` for alternatives",
228 "as x://.y, or see `hg log -k msys` for alternatives",
229 r'-\S+:\.|' '# no-msys', # -Rxxx
229 r'-\S+:\.|' '# no-msys', # -Rxxx
230 ), # in test-pull.t which is skipped on windows
230 ), # in test-pull.t which is skipped on windows
231 (
231 (
232 r'^ [^$>].*27\.0\.0\.1',
232 r'^ [^$>].*27\.0\.0\.1',
233 'use $LOCALIP not an explicit loopback address',
233 'use $LOCALIP not an explicit loopback address',
234 ),
234 ),
235 (
235 (
236 r'^ (?![>$] ).*\$LOCALIP.*[^)]$',
236 r'^ (?![>$] ).*\$LOCALIP.*[^)]$',
237 'mark $LOCALIP output lines with (glob) to help tests in BSD jails',
237 'mark $LOCALIP output lines with (glob) to help tests in BSD jails',
238 ),
238 ),
239 (
239 (
240 r'^ (cat|find): .*: \$ENOENT\$',
240 r'^ (cat|find): .*: \$ENOENT\$',
241 'use test -f to test for file existence',
241 'use test -f to test for file existence',
242 ),
242 ),
243 (
243 (
244 r'^ diff -[^ -]*p',
244 r'^ diff -[^ -]*p',
245 "don't use (external) diff with -p for portability",
245 "don't use (external) diff with -p for portability",
246 ),
246 ),
247 (r' readlink ', 'use readlink.py instead of readlink'),
247 (r' readlink ', 'use readlink.py instead of readlink'),
248 (
248 (
249 r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
249 r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
250 "glob timezone field in diff output for portability",
250 "glob timezone field in diff output for portability",
251 ),
251 ),
252 (
252 (
253 r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
253 r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
254 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability",
254 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability",
255 ),
255 ),
256 (
256 (
257 r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
257 r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
258 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability",
258 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability",
259 ),
259 ),
260 (
260 (
261 r'^ @@ -[0-9]+ [+][0-9]+ @@',
261 r'^ @@ -[0-9]+ [+][0-9]+ @@',
262 "use '@@ -N* +N* @@ (glob)' style chunk header for portability",
262 "use '@@ -N* +N* @@ (glob)' style chunk header for portability",
263 ),
263 ),
264 (
264 (
265 uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
265 uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
266 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
266 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
267 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)",
267 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)",
268 ),
268 ),
269 ],
269 ],
270 # warnings
270 # warnings
271 [
271 [
272 (
272 (
273 r'^ (?!.*\$LOCALIP)[^*?/\n]* \(glob\)$',
273 r'^ (?!.*\$LOCALIP)[^*?/\n]* \(glob\)$',
274 "glob match with no glob string (?, *, /, and $LOCALIP)",
274 "glob match with no glob string (?, *, /, and $LOCALIP)",
275 ),
275 ),
276 ],
276 ],
277 ]
277 ]
278
278
279 # transform plain test rules to unified test's
279 # transform plain test rules to unified test's
280 for i in [0, 1]:
280 for i in [0, 1]:
281 for tp in testpats[i]:
281 for tp in testpats[i]:
282 p = tp[0]
282 p = tp[0]
283 m = tp[1]
283 m = tp[1]
284 if p.startswith(r'^'):
284 if p.startswith(r'^'):
285 p = r"^ [$>] (%s)" % p[1:]
285 p = r"^ [$>] (%s)" % p[1:]
286 else:
286 else:
287 p = r"^ [$>] .*(%s)" % p
287 p = r"^ [$>] .*(%s)" % p
288 utestpats[i].append((p, m) + tp[2:])
288 utestpats[i].append((p, m) + tp[2:])
289
289
290 # don't transform the following rules:
290 # don't transform the following rules:
291 # " > \t" and " \t" should be allowed in unified tests
291 # " > \t" and " \t" should be allowed in unified tests
292 testpats[0].append((r'^( *)\t', "don't use tabs to indent"))
292 testpats[0].append((r'^( *)\t', "don't use tabs to indent"))
293 utestpats[0].append((r'^( ?)\t', "don't use tabs to indent"))
293 utestpats[0].append((r'^( ?)\t', "don't use tabs to indent"))
294
294
295 utestfilters = [
295 utestfilters = [
296 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
296 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
297 (r"( +)(#([^!][^\n]*\S)?)", repcomment),
297 (r"( +)(#([^!][^\n]*\S)?)", repcomment),
298 ]
298 ]
299
299
300 # common patterns to check *.py
300 # common patterns to check *.py
301 commonpypats = [
301 commonpypats = [
302 [
302 [
303 (r'\\$', 'Use () to wrap long lines in Python, not \\'),
303 (r'\\$', 'Use () to wrap long lines in Python, not \\'),
304 (
304 (
305 r'^\s*def\s*\w+\s*\(.*,\s*\(',
305 r'^\s*def\s*\w+\s*\(.*,\s*\(',
306 "tuple parameter unpacking not available in Python 3+",
306 "tuple parameter unpacking not available in Python 3+",
307 ),
307 ),
308 (
308 (
309 r'lambda\s*\(.*,.*\)',
309 r'lambda\s*\(.*,.*\)',
310 "tuple parameter unpacking not available in Python 3+",
310 "tuple parameter unpacking not available in Python 3+",
311 ),
311 ),
312 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
312 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
313 (r'(?<!\.)\breduce\s*\(.*', "reduce is not available in Python 3+"),
313 (r'(?<!\.)\breduce\s*\(.*', "reduce is not available in Python 3+"),
314 (
314 (
315 r'\bdict\(.*=',
315 r'\bdict\(.*=',
316 'dict() is different in Py2 and 3 and is slower than {}',
316 'dict() is different in Py2 and 3 and is slower than {}',
317 'dict-from-generator',
317 'dict-from-generator',
318 ),
318 ),
319 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
319 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
320 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
320 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
321 (r'^\s*\t', "don't use tabs"),
321 (r'^\s*\t', "don't use tabs"),
322 (r'\S;\s*\n', "semicolon"),
322 (r'\S;\s*\n', "semicolon"),
323 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
323 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
324 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
324 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
325 (r'(\w|\)),\w', "missing whitespace after ,"),
325 (r'(\w|\)),\w', "missing whitespace after ,"),
326 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
326 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
327 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
327 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
328 (
328 (
329 (
329 (
330 # a line ending with a colon, potentially with trailing comments
330 # a line ending with a colon, potentially with trailing comments
331 r':([ \t]*#[^\n]*)?\n'
331 r':([ \t]*#[^\n]*)?\n'
332 # one that is not a pass and not only a comment
332 # one that is not a pass and not only a comment
333 r'(?P<indent>[ \t]+)[^#][^\n]+\n'
333 r'(?P<indent>[ \t]+)[^#][^\n]+\n'
334 # more lines at the same indent level
334 # more lines at the same indent level
335 r'((?P=indent)[^\n]+\n)*'
335 r'((?P=indent)[^\n]+\n)*'
336 # a pass at the same indent level, which is bogus
336 # a pass at the same indent level, which is bogus
337 r'(?P=indent)pass[ \t\n#]'
337 r'(?P=indent)pass[ \t\n#]'
338 ),
338 ),
339 'omit superfluous pass',
339 'omit superfluous pass',
340 ),
340 ),
341 (r'[^\n]\Z', "no trailing newline"),
341 (r'[^\n]\Z', "no trailing newline"),
342 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
342 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
343 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
344 # "don't use underbars in identifiers"),
345 (
343 (
346 r'^\s+(self\.)?[A-Za-z][a-z0-9]+[A-Z]\w* = ',
344 r'^\s+(self\.)?[A-Za-z][a-z0-9]+[A-Z]\w* = ',
347 "don't use camelcase in identifiers",
345 "don't use camelcase in identifiers",
348 r'#.*camelcase-required',
346 r'#.*camelcase-required',
349 ),
347 ),
350 (
348 (
351 r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
349 r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
352 "linebreak after :",
350 "linebreak after :",
353 ),
351 ),
354 (
352 (
355 r'class\s[^( \n]+:',
353 r'class\s[^( \n]+:',
356 "old-style class, use class foo(object)",
354 "old-style class, use class foo(object)",
357 r'#.*old-style',
355 r'#.*old-style',
358 ),
356 ),
359 (
357 (
360 r'class\s[^( \n]+\(\):',
358 r'class\s[^( \n]+\(\):',
361 "class foo() creates old style object, use class foo(object)",
359 "class foo() creates old style object, use class foo(object)",
362 r'#.*old-style',
360 r'#.*old-style',
363 ),
361 ),
364 (
362 (
365 r'\b(%s)\('
363 r'\b(%s)\('
366 % '|'.join(k for k in keyword.kwlist if k not in ('print', 'exec')),
364 % '|'.join(k for k in keyword.kwlist if k not in ('print', 'exec')),
367 "Python keyword is not a function",
365 "Python keyword is not a function",
368 ),
366 ),
369 # (r'class\s[A-Z][^\(]*\((?!Exception)',
367 # (r'class\s[A-Z][^\(]*\((?!Exception)',
370 # "don't capitalize non-exception classes"),
368 # "don't capitalize non-exception classes"),
371 # (r'in range\(', "use xrange"),
369 # (r'in range\(', "use xrange"),
372 # (r'^\s*print\s+', "avoid using print in core and extensions"),
370 # (r'^\s*print\s+', "avoid using print in core and extensions"),
373 (r'[\x80-\xff]', "non-ASCII character literal"),
371 (r'[\x80-\xff]', "non-ASCII character literal"),
374 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
372 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
375 (
373 (
376 r'([\(\[][ \t]\S)|(\S[ \t][\)\]])',
374 r'([\(\[][ \t]\S)|(\S[ \t][\)\]])',
377 "gratuitous whitespace in () or []",
375 "gratuitous whitespace in () or []",
378 ),
376 ),
379 # (r'\s\s=', "gratuitous whitespace before ="),
377 # (r'\s\s=', "gratuitous whitespace before ="),
380 (
378 (
381 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
379 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
382 "missing whitespace around operator",
380 "missing whitespace around operator",
383 ),
381 ),
384 (
382 (
385 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
383 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
386 "missing whitespace around operator",
384 "missing whitespace around operator",
387 ),
385 ),
388 (
386 (
389 r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
387 r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
390 "missing whitespace around operator",
388 "missing whitespace around operator",
391 ),
389 ),
392 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]', "wrong whitespace around ="),
390 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]', "wrong whitespace around ="),
393 (
391 (
394 r'\([^()]*( =[^=]|[^<>!=]= )',
392 r'\([^()]*( =[^=]|[^<>!=]= )',
395 "no whitespace around = for named parameters",
393 "no whitespace around = for named parameters",
396 ),
394 ),
397 (
395 (
398 r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
396 r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
399 "don't use old-style two-argument raise, use Exception(message)",
397 "don't use old-style two-argument raise, use Exception(message)",
400 ),
398 ),
401 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
399 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
402 (
400 (
403 r' [=!]=\s+(True|False|None)',
401 r' [=!]=\s+(True|False|None)',
404 "comparison with singleton, use 'is' or 'is not' instead",
402 "comparison with singleton, use 'is' or 'is not' instead",
405 ),
403 ),
406 (
404 (
407 r'^\s*(while|if) [01]:',
405 r'^\s*(while|if) [01]:',
408 "use True/False for constant Boolean expression",
406 "use True/False for constant Boolean expression",
409 ),
407 ),
410 (r'^\s*if False(:| +and)', 'Remove code instead of using `if False`'),
408 (r'^\s*if False(:| +and)', 'Remove code instead of using `if False`'),
411 (
409 (
412 r'(?:(?<!def)\s+|\()hasattr\(',
410 r'(?:(?<!def)\s+|\()hasattr\(',
413 'hasattr(foo, bar) is broken on py2, use util.safehasattr(foo, bar) '
411 'hasattr(foo, bar) is broken on py2, use util.safehasattr(foo, bar) '
414 'instead',
412 'instead',
415 r'#.*hasattr-py3-only',
413 r'#.*hasattr-py3-only',
416 ),
414 ),
417 (r'opener\([^)]*\).read\(', "use opener.read() instead"),
415 (r'opener\([^)]*\).read\(', "use opener.read() instead"),
418 (r'opener\([^)]*\).write\(', "use opener.write() instead"),
416 (r'opener\([^)]*\).write\(', "use opener.write() instead"),
419 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
417 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
420 (r'\.debug\(\_', "don't mark debug messages for translation"),
418 (r'\.debug\(\_', "don't mark debug messages for translation"),
421 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
419 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
422 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
420 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
423 (
421 (
424 r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
422 r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
425 'legacy exception syntax; use "as" instead of ","',
423 'legacy exception syntax; use "as" instead of ","',
426 ),
424 ),
427 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
425 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
428 (r'\bdef\s+__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
426 (r'\bdef\s+__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
429 (
427 (
430 r'os\.path\.join\(.*, *(""|\'\')\)',
428 r'os\.path\.join\(.*, *(""|\'\')\)',
431 "use pathutil.normasprefix(path) instead of os.path.join(path, '')",
429 "use pathutil.normasprefix(path) instead of os.path.join(path, '')",
432 ),
430 ),
433 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
431 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
434 # XXX only catch mutable arguments on the first line of the definition
432 # XXX only catch mutable arguments on the first line of the definition
435 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
433 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
436 (r'\butil\.Abort\b', "directly use error.Abort"),
434 (r'\butil\.Abort\b', "directly use error.Abort"),
437 (
435 (
438 r'^@(\w*\.)?cachefunc',
436 r'^@(\w*\.)?cachefunc',
439 "module-level @cachefunc is risky, please avoid",
437 "module-level @cachefunc is risky, please avoid",
440 ),
438 ),
441 (
439 (
442 r'^import Queue',
440 r'^import Queue',
443 "don't use Queue, use pycompat.queue.Queue + "
441 "don't use Queue, use pycompat.queue.Queue + "
444 "pycompat.queue.Empty",
442 "pycompat.queue.Empty",
445 ),
443 ),
446 (
444 (
447 r'^import cStringIO',
445 r'^import cStringIO',
448 "don't use cStringIO.StringIO, use util.stringio",
446 "don't use cStringIO.StringIO, use util.stringio",
449 ),
447 ),
450 (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
448 (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
451 (
449 (
452 r'^import SocketServer',
450 r'^import SocketServer',
453 "don't use SockerServer, use util.socketserver",
451 "don't use SockerServer, use util.socketserver",
454 ),
452 ),
455 (r'^import urlparse', "don't use urlparse, use util.urlreq"),
453 (r'^import urlparse', "don't use urlparse, use util.urlreq"),
456 (r'^import xmlrpclib', "don't use xmlrpclib, use util.xmlrpclib"),
454 (r'^import xmlrpclib', "don't use xmlrpclib, use util.xmlrpclib"),
457 (r'^import cPickle', "don't use cPickle, use util.pickle"),
455 (r'^import cPickle', "don't use cPickle, use util.pickle"),
458 (r'^import pickle', "don't use pickle, use util.pickle"),
456 (r'^import pickle', "don't use pickle, use util.pickle"),
459 (r'^import httplib', "don't use httplib, use util.httplib"),
457 (r'^import httplib', "don't use httplib, use util.httplib"),
460 (r'^import BaseHTTPServer', "use util.httpserver instead"),
458 (r'^import BaseHTTPServer', "use util.httpserver instead"),
461 (
459 (
462 r'^(from|import) mercurial\.(cext|pure|cffi)',
460 r'^(from|import) mercurial\.(cext|pure|cffi)',
463 "use mercurial.policy.importmod instead",
461 "use mercurial.policy.importmod instead",
464 ),
462 ),
465 (r'\.next\(\)', "don't use .next(), use next(...)"),
463 (r'\.next\(\)', "don't use .next(), use next(...)"),
466 (
464 (
467 r'([a-z]*).revision\(\1\.node\(',
465 r'([a-z]*).revision\(\1\.node\(',
468 "don't convert rev to node before passing to revision(nodeorrev)",
466 "don't convert rev to node before passing to revision(nodeorrev)",
469 ),
467 ),
470 (r'platform\.system\(\)', "don't use platform.system(), use pycompat"),
468 (r'platform\.system\(\)', "don't use platform.system(), use pycompat"),
471 ],
469 ],
472 # warnings
470 # warnings
473 [],
471 [],
474 ]
472 ]
475
473
476 # patterns to check normal *.py files
474 # patterns to check normal *.py files
477 pypats = [
475 pypats = [
478 [
476 [
479 # Ideally, these should be placed in "commonpypats" for
477 # Ideally, these should be placed in "commonpypats" for
480 # consistency of coding rules in Mercurial source tree.
478 # consistency of coding rules in Mercurial source tree.
481 # But on the other hand, these are not so seriously required for
479 # But on the other hand, these are not so seriously required for
482 # python code fragments embedded in test scripts. Fixing test
480 # python code fragments embedded in test scripts. Fixing test
483 # scripts for these patterns requires many changes, and has less
481 # scripts for these patterns requires many changes, and has less
484 # profit than effort.
482 # profit than effort.
485 (r'raise Exception', "don't raise generic exceptions"),
483 (r'raise Exception', "don't raise generic exceptions"),
486 (r'[\s\(](open|file)\([^)]*\)\.read\(', "use util.readfile() instead"),
484 (r'[\s\(](open|file)\([^)]*\)\.read\(', "use util.readfile() instead"),
487 (
485 (
488 r'[\s\(](open|file)\([^)]*\)\.write\(',
486 r'[\s\(](open|file)\([^)]*\)\.write\(',
489 "use util.writefile() instead",
487 "use util.writefile() instead",
490 ),
488 ),
491 (
489 (
492 r'^[\s\(]*(open(er)?|file)\([^)]*\)(?!\.close\(\))',
490 r'^[\s\(]*(open(er)?|file)\([^)]*\)(?!\.close\(\))',
493 "always assign an opened file to a variable, and close it afterwards",
491 "always assign an opened file to a variable, and close it afterwards",
494 ),
492 ),
495 (
493 (
496 r'[\s\(](open|file)\([^)]*\)\.(?!close\(\))',
494 r'[\s\(](open|file)\([^)]*\)\.(?!close\(\))',
497 "always assign an opened file to a variable, and close it afterwards",
495 "always assign an opened file to a variable, and close it afterwards",
498 ),
496 ),
499 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
497 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
500 (r'^import atexit', "don't use atexit, use ui.atexit"),
498 (r'^import atexit', "don't use atexit, use ui.atexit"),
501 # rules depending on implementation of repquote()
499 # rules depending on implementation of repquote()
502 (
500 (
503 r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
501 r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
504 'string join across lines with no space',
502 'string join across lines with no space',
505 ),
503 ),
506 (
504 (
507 r'''(?x)ui\.(status|progress|write|note|warn)\(
505 r'''(?x)ui\.(status|progress|write|note|warn)\(
508 [ \t\n#]*
506 [ \t\n#]*
509 (?# any strings/comments might precede a string, which
507 (?# any strings/comments might precede a string, which
510 # contains translatable message)
508 # contains translatable message)
511 b?((['"]|\'\'\'|""")[ \npq%bAPMxno]*(['"]|\'\'\'|""")[ \t\n#]+)*
509 b?((['"]|\'\'\'|""")[ \npq%bAPMxno]*(['"]|\'\'\'|""")[ \t\n#]+)*
512 (?# sequence consisting of below might precede translatable message
510 (?# sequence consisting of below might precede translatable message
513 # - formatting string: "% 10s", "%05d", "% -3.2f", "%*s", "%%" ...
511 # - formatting string: "% 10s", "%05d", "% -3.2f", "%*s", "%%" ...
514 # - escaped character: "\\", "\n", "\0" ...
512 # - escaped character: "\\", "\n", "\0" ...
515 # - character other than '%', 'b' as '\', and 'x' as alphabet)
513 # - character other than '%', 'b' as '\', and 'x' as alphabet)
516 (['"]|\'\'\'|""")
514 (['"]|\'\'\'|""")
517 ((%([ n]?[PM]?([np]+|A))?x)|%%|b[bnx]|[ \nnpqAPMo])*x
515 ((%([ n]?[PM]?([np]+|A))?x)|%%|b[bnx]|[ \nnpqAPMo])*x
518 (?# this regexp can't use [^...] style,
516 (?# this regexp can't use [^...] style,
519 # because _preparepats forcibly adds "\n" into [^...],
517 # because _preparepats forcibly adds "\n" into [^...],
520 # even though this regexp wants match it against "\n")''',
518 # even though this regexp wants match it against "\n")''',
521 "missing _() in ui message (use () to hide false-positives)",
519 "missing _() in ui message (use () to hide false-positives)",
522 ),
520 ),
523 ]
521 ]
524 + commonpypats[0],
522 + commonpypats[0],
525 # warnings
523 # warnings
526 [
524 [
527 # rules depending on implementation of repquote()
525 # rules depending on implementation of repquote()
528 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
526 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
529 ]
527 ]
530 + commonpypats[1],
528 + commonpypats[1],
531 ]
529 ]
532
530
533 # patterns to check *.py for embedded ones in test script
531 # patterns to check *.py for embedded ones in test script
534 embeddedpypats = [
532 embeddedpypats = [
535 [] + commonpypats[0],
533 [] + commonpypats[0],
536 # warnings
534 # warnings
537 [] + commonpypats[1],
535 [] + commonpypats[1],
538 ]
536 ]
539
537
540 # common filters to convert *.py
538 # common filters to convert *.py
541 commonpyfilters = [
539 commonpyfilters = [
542 (
540 (
543 r"""(?msx)(?P<comment>\#.*?$)|
541 r"""(?msx)(?P<comment>\#.*?$)|
544 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
542 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
545 (?P<text>(([^\\]|\\.)*?))
543 (?P<text>(([^\\]|\\.)*?))
546 (?P=quote))""",
544 (?P=quote))""",
547 reppython,
545 reppython,
548 ),
546 ),
549 ]
547 ]
550
548
551 # filters to convert normal *.py files
549 # filters to convert normal *.py files
552 pyfilters = [] + commonpyfilters
550 pyfilters = [] + commonpyfilters
553
551
554 # non-filter patterns
552 # non-filter patterns
555 pynfpats = [
553 pynfpats = [
556 [
554 [
557 (r'pycompat\.osname\s*[=!]=\s*[\'"]nt[\'"]', "use pycompat.iswindows"),
555 (r'pycompat\.osname\s*[=!]=\s*[\'"]nt[\'"]', "use pycompat.iswindows"),
558 (r'pycompat\.osname\s*[=!]=\s*[\'"]posix[\'"]', "use pycompat.isposix"),
556 (r'pycompat\.osname\s*[=!]=\s*[\'"]posix[\'"]', "use pycompat.isposix"),
559 (
557 (
560 r'pycompat\.sysplatform\s*[!=]=\s*[\'"]darwin[\'"]',
558 r'pycompat\.sysplatform\s*[!=]=\s*[\'"]darwin[\'"]',
561 "use pycompat.isdarwin",
559 "use pycompat.isdarwin",
562 ),
560 ),
563 ],
561 ],
564 # warnings
562 # warnings
565 [],
563 [],
566 ]
564 ]
567
565
568 # filters to convert *.py for embedded ones in test script
566 # filters to convert *.py for embedded ones in test script
569 embeddedpyfilters = [] + commonpyfilters
567 embeddedpyfilters = [] + commonpyfilters
570
568
571 # extension non-filter patterns
569 # extension non-filter patterns
572 pyextnfpats = [
570 pyextnfpats = [
573 [(r'^"""\n?[A-Z]', "don't capitalize docstring title")],
571 [(r'^"""\n?[A-Z]', "don't capitalize docstring title")],
574 # warnings
572 # warnings
575 [],
573 [],
576 ]
574 ]
577
575
578 txtfilters = []
576 txtfilters = []
579
577
580 txtpats = [
578 txtpats = [
581 [
579 [
582 (r'\s$', 'trailing whitespace'),
580 (r'\s$', 'trailing whitespace'),
583 ('.. note::[ \n][^\n]', 'add two newlines after note::'),
581 ('.. note::[ \n][^\n]', 'add two newlines after note::'),
584 ],
582 ],
585 [],
583 [],
586 ]
584 ]
587
585
588 cpats = [
586 cpats = [
589 [
587 [
590 (r'//', "don't use //-style comments"),
588 (r'//', "don't use //-style comments"),
591 (r'\S\t', "don't use tabs except for indent"),
589 (r'\S\t', "don't use tabs except for indent"),
592 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
590 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
593 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
591 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
594 (r'return\(', "return is not a function"),
592 (r'return\(', "return is not a function"),
595 (r' ;', "no space before ;"),
593 (r' ;', "no space before ;"),
596 (r'[^;] \)', "no space before )"),
594 (r'[^;] \)', "no space before )"),
597 (r'[)][{]', "space between ) and {"),
595 (r'[)][{]', "space between ) and {"),
598 (r'\w+\* \w+', "use int *foo, not int* foo"),
596 (r'\w+\* \w+', "use int *foo, not int* foo"),
599 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
597 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
600 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
598 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
601 (r'\w,\w', "missing whitespace after ,"),
599 (r'\w,\w', "missing whitespace after ,"),
602 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
600 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
603 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
601 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
604 (r'^#\s+\w', "use #foo, not # foo"),
602 (r'^#\s+\w', "use #foo, not # foo"),
605 (r'[^\n]\Z', "no trailing newline"),
603 (r'[^\n]\Z', "no trailing newline"),
606 (r'^\s*#import\b', "use only #include in standard C code"),
604 (r'^\s*#import\b', "use only #include in standard C code"),
607 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
605 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
608 (r'strcat\(', "don't use strcat"),
606 (r'strcat\(', "don't use strcat"),
609 # rules depending on implementation of repquote()
607 # rules depending on implementation of repquote()
610 ],
608 ],
611 # warnings
609 # warnings
612 [
610 [
613 # rules depending on implementation of repquote()
611 # rules depending on implementation of repquote()
614 ],
612 ],
615 ]
613 ]
616
614
617 cfilters = [
615 cfilters = [
618 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
616 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
619 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
617 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
620 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
618 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
621 (r'(\()([^)]+\))', repcallspaces),
619 (r'(\()([^)]+\))', repcallspaces),
622 ]
620 ]
623
621
624 inutilpats = [
622 inutilpats = [
625 [(r'\bui\.', "don't use ui in util"),],
623 [(r'\bui\.', "don't use ui in util"),],
626 # warnings
624 # warnings
627 [],
625 [],
628 ]
626 ]
629
627
630 inrevlogpats = [
628 inrevlogpats = [
631 [(r'\brepo\.', "don't use repo in revlog"),],
629 [(r'\brepo\.', "don't use repo in revlog"),],
632 # warnings
630 # warnings
633 [],
631 [],
634 ]
632 ]
635
633
636 webtemplatefilters = []
634 webtemplatefilters = []
637
635
638 webtemplatepats = [
636 webtemplatepats = [
639 [],
637 [],
640 [
638 [
641 (
639 (
642 r'{desc(\|(?!websub|firstline)[^\|]*)+}',
640 r'{desc(\|(?!websub|firstline)[^\|]*)+}',
643 'follow desc keyword with either firstline or websub',
641 'follow desc keyword with either firstline or websub',
644 ),
642 ),
645 ],
643 ],
646 ]
644 ]
647
645
648 allfilesfilters = []
646 allfilesfilters = []
649
647
650 allfilespats = [
648 allfilespats = [
651 [
649 [
652 (
650 (
653 r'(http|https)://[a-zA-Z0-9./]*selenic.com/',
651 r'(http|https)://[a-zA-Z0-9./]*selenic.com/',
654 'use mercurial-scm.org domain URL',
652 'use mercurial-scm.org domain URL',
655 ),
653 ),
656 (
654 (
657 r'mercurial@selenic\.com',
655 r'mercurial@selenic\.com',
658 'use mercurial-scm.org domain for mercurial ML address',
656 'use mercurial-scm.org domain for mercurial ML address',
659 ),
657 ),
660 (
658 (
661 r'mercurial-devel@selenic\.com',
659 r'mercurial-devel@selenic\.com',
662 'use mercurial-scm.org domain for mercurial-devel ML address',
660 'use mercurial-scm.org domain for mercurial-devel ML address',
663 ),
661 ),
664 ],
662 ],
665 # warnings
663 # warnings
666 [],
664 [],
667 ]
665 ]
668
666
669 py3pats = [
667 py3pats = [
670 [
668 [
671 (
669 (
672 r'os\.environ',
670 r'os\.environ',
673 "use encoding.environ instead (py3)",
671 "use encoding.environ instead (py3)",
674 r'#.*re-exports',
672 r'#.*re-exports',
675 ),
673 ),
676 (r'os\.name', "use pycompat.osname instead (py3)"),
674 (r'os\.name', "use pycompat.osname instead (py3)"),
677 (r'os\.getcwd', "use encoding.getcwd instead (py3)", r'#.*re-exports'),
675 (r'os\.getcwd', "use encoding.getcwd instead (py3)", r'#.*re-exports'),
678 (r'os\.sep', "use pycompat.ossep instead (py3)"),
676 (r'os\.sep', "use pycompat.ossep instead (py3)"),
679 (r'os\.pathsep', "use pycompat.ospathsep instead (py3)"),
677 (r'os\.pathsep', "use pycompat.ospathsep instead (py3)"),
680 (r'os\.altsep', "use pycompat.osaltsep instead (py3)"),
678 (r'os\.altsep', "use pycompat.osaltsep instead (py3)"),
681 (r'sys\.platform', "use pycompat.sysplatform instead (py3)"),
679 (r'sys\.platform', "use pycompat.sysplatform instead (py3)"),
682 (r'getopt\.getopt', "use pycompat.getoptb instead (py3)"),
680 (r'getopt\.getopt', "use pycompat.getoptb instead (py3)"),
683 (r'os\.getenv', "use encoding.environ.get instead"),
681 (r'os\.getenv', "use encoding.environ.get instead"),
684 (r'os\.setenv', "modifying the environ dict is not preferred"),
682 (r'os\.setenv', "modifying the environ dict is not preferred"),
685 (r'(?<!pycompat\.)xrange', "use pycompat.xrange instead (py3)"),
683 (r'(?<!pycompat\.)xrange', "use pycompat.xrange instead (py3)"),
686 ],
684 ],
687 # warnings
685 # warnings
688 [],
686 [],
689 ]
687 ]
690
688
691 checks = [
689 checks = [
692 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
690 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
693 ('python', r'.*\.(py|cgi)$', r'^#!.*python', [], pynfpats),
691 ('python', r'.*\.(py|cgi)$', r'^#!.*python', [], pynfpats),
694 ('python', r'.*hgext.*\.py$', '', [], pyextnfpats),
692 ('python', r'.*hgext.*\.py$', '', [], pyextnfpats),
695 (
693 (
696 'python 3',
694 'python 3',
697 r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
695 r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
698 '',
696 '',
699 pyfilters,
697 pyfilters,
700 py3pats,
698 py3pats,
701 ),
699 ),
702 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
700 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
703 ('c', r'.*\.[ch]$', '', cfilters, cpats),
701 ('c', r'.*\.[ch]$', '', cfilters, cpats),
704 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
702 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
705 (
703 (
706 'layering violation repo in revlog',
704 'layering violation repo in revlog',
707 r'mercurial/revlog\.py',
705 r'mercurial/revlog\.py',
708 '',
706 '',
709 pyfilters,
707 pyfilters,
710 inrevlogpats,
708 inrevlogpats,
711 ),
709 ),
712 (
710 (
713 'layering violation ui in util',
711 'layering violation ui in util',
714 r'mercurial/util\.py',
712 r'mercurial/util\.py',
715 '',
713 '',
716 pyfilters,
714 pyfilters,
717 inutilpats,
715 inutilpats,
718 ),
716 ),
719 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
717 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
720 (
718 (
721 'web template',
719 'web template',
722 r'mercurial/templates/.*\.tmpl',
720 r'mercurial/templates/.*\.tmpl',
723 '',
721 '',
724 webtemplatefilters,
722 webtemplatefilters,
725 webtemplatepats,
723 webtemplatepats,
726 ),
724 ),
727 ('all except for .po', r'.*(?<!\.po)$', '', allfilesfilters, allfilespats),
725 ('all except for .po', r'.*(?<!\.po)$', '', allfilesfilters, allfilespats),
728 ]
726 ]
729
727
730 # (desc,
728 # (desc,
731 # func to pick up embedded code fragments,
729 # func to pick up embedded code fragments,
732 # list of patterns to convert target files
730 # list of patterns to convert target files
733 # list of patterns to detect errors/warnings)
731 # list of patterns to detect errors/warnings)
734 embeddedchecks = [
732 embeddedchecks = [
735 (
733 (
736 'embedded python',
734 'embedded python',
737 testparseutil.pyembedded,
735 testparseutil.pyembedded,
738 embeddedpyfilters,
736 embeddedpyfilters,
739 embeddedpypats,
737 embeddedpypats,
740 )
738 )
741 ]
739 ]
742
740
743
741
744 def _preparepats():
742 def _preparepats():
745 def preparefailandwarn(failandwarn):
743 def preparefailandwarn(failandwarn):
746 for pats in failandwarn:
744 for pats in failandwarn:
747 for i, pseq in enumerate(pats):
745 for i, pseq in enumerate(pats):
748 # fix-up regexes for multi-line searches
746 # fix-up regexes for multi-line searches
749 p = pseq[0]
747 p = pseq[0]
750 # \s doesn't match \n (done in two steps)
748 # \s doesn't match \n (done in two steps)
751 # first, we replace \s that appears in a set already
749 # first, we replace \s that appears in a set already
752 p = re.sub(r'\[\\s', r'[ \\t', p)
750 p = re.sub(r'\[\\s', r'[ \\t', p)
753 # now we replace other \s instances.
751 # now we replace other \s instances.
754 p = re.sub(r'(?<!(\\|\[))\\s', r'[ \\t]', p)
752 p = re.sub(r'(?<!(\\|\[))\\s', r'[ \\t]', p)
755 # [^...] doesn't match newline
753 # [^...] doesn't match newline
756 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
754 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
757
755
758 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
756 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
759
757
760 def preparefilters(filters):
758 def preparefilters(filters):
761 for i, flt in enumerate(filters):
759 for i, flt in enumerate(filters):
762 filters[i] = re.compile(flt[0]), flt[1]
760 filters[i] = re.compile(flt[0]), flt[1]
763
761
764 for cs in (checks, embeddedchecks):
762 for cs in (checks, embeddedchecks):
765 for c in cs:
763 for c in cs:
766 failandwarn = c[-1]
764 failandwarn = c[-1]
767 preparefailandwarn(failandwarn)
765 preparefailandwarn(failandwarn)
768
766
769 filters = c[-2]
767 filters = c[-2]
770 preparefilters(filters)
768 preparefilters(filters)
771
769
772
770
773 class norepeatlogger(object):
771 class norepeatlogger(object):
774 def __init__(self):
772 def __init__(self):
775 self._lastseen = None
773 self._lastseen = None
776
774
777 def log(self, fname, lineno, line, msg, blame):
775 def log(self, fname, lineno, line, msg, blame):
778 """print error related a to given line of a given file.
776 """print error related a to given line of a given file.
779
777
780 The faulty line will also be printed but only once in the case
778 The faulty line will also be printed but only once in the case
781 of multiple errors.
779 of multiple errors.
782
780
783 :fname: filename
781 :fname: filename
784 :lineno: line number
782 :lineno: line number
785 :line: actual content of the line
783 :line: actual content of the line
786 :msg: error message
784 :msg: error message
787 """
785 """
788 msgid = fname, lineno, line
786 msgid = fname, lineno, line
789 if msgid != self._lastseen:
787 if msgid != self._lastseen:
790 if blame:
788 if blame:
791 print("%s:%d (%s):" % (fname, lineno, blame))
789 print("%s:%d (%s):" % (fname, lineno, blame))
792 else:
790 else:
793 print("%s:%d:" % (fname, lineno))
791 print("%s:%d:" % (fname, lineno))
794 print(" > %s" % line)
792 print(" > %s" % line)
795 self._lastseen = msgid
793 self._lastseen = msgid
796 print(" " + msg)
794 print(" " + msg)
797
795
798
796
799 _defaultlogger = norepeatlogger()
797 _defaultlogger = norepeatlogger()
800
798
801
799
802 def getblame(f):
800 def getblame(f):
803 lines = []
801 lines = []
804 for l in os.popen('hg annotate -un %s' % f):
802 for l in os.popen('hg annotate -un %s' % f):
805 start, line = l.split(':', 1)
803 start, line = l.split(':', 1)
806 user, rev = start.split()
804 user, rev = start.split()
807 lines.append((line[1:-1], user, rev))
805 lines.append((line[1:-1], user, rev))
808 return lines
806 return lines
809
807
810
808
811 def checkfile(
809 def checkfile(
812 f,
810 f,
813 logfunc=_defaultlogger.log,
811 logfunc=_defaultlogger.log,
814 maxerr=None,
812 maxerr=None,
815 warnings=False,
813 warnings=False,
816 blame=False,
814 blame=False,
817 debug=False,
815 debug=False,
818 lineno=True,
816 lineno=True,
819 ):
817 ):
820 """checks style and portability of a given file
818 """checks style and portability of a given file
821
819
822 :f: filepath
820 :f: filepath
823 :logfunc: function used to report error
821 :logfunc: function used to report error
824 logfunc(filename, linenumber, linecontent, errormessage)
822 logfunc(filename, linenumber, linecontent, errormessage)
825 :maxerr: number of error to display before aborting.
823 :maxerr: number of error to display before aborting.
826 Set to false (default) to report all errors
824 Set to false (default) to report all errors
827
825
828 return True if no error is found, False otherwise.
826 return True if no error is found, False otherwise.
829 """
827 """
830 result = True
828 result = True
831
829
832 try:
830 try:
833 with opentext(f) as fp:
831 with opentext(f) as fp:
834 try:
832 try:
835 pre = fp.read()
833 pre = fp.read()
836 except UnicodeDecodeError as e:
834 except UnicodeDecodeError as e:
837 print("%s while reading %s" % (e, f))
835 print("%s while reading %s" % (e, f))
838 return result
836 return result
839 except IOError as e:
837 except IOError as e:
840 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
838 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
841 return result
839 return result
842
840
843 # context information shared while single checkfile() invocation
841 # context information shared while single checkfile() invocation
844 context = {'blamecache': None}
842 context = {'blamecache': None}
845
843
846 for name, match, magic, filters, pats in checks:
844 for name, match, magic, filters, pats in checks:
847 if debug:
845 if debug:
848 print(name, f)
846 print(name, f)
849 if not (re.match(match, f) or (magic and re.search(magic, pre))):
847 if not (re.match(match, f) or (magic and re.search(magic, pre))):
850 if debug:
848 if debug:
851 print(
849 print(
852 "Skipping %s for %s it doesn't match %s" % (name, match, f)
850 "Skipping %s for %s it doesn't match %s" % (name, match, f)
853 )
851 )
854 continue
852 continue
855 if "no-" "check-code" in pre:
853 if "no-" "check-code" in pre:
856 # If you're looking at this line, it's because a file has:
854 # If you're looking at this line, it's because a file has:
857 # no- check- code
855 # no- check- code
858 # but the reason to output skipping is to make life for
856 # but the reason to output skipping is to make life for
859 # tests easier. So, instead of writing it with a normal
857 # tests easier. So, instead of writing it with a normal
860 # spelling, we write it with the expected spelling from
858 # spelling, we write it with the expected spelling from
861 # tests/test-check-code.t
859 # tests/test-check-code.t
862 print("Skipping %s it has no-che?k-code (glob)" % f)
860 print("Skipping %s it has no-che?k-code (glob)" % f)
863 return "Skip" # skip checking this file
861 return "Skip" # skip checking this file
864
862
865 fc = _checkfiledata(
863 fc = _checkfiledata(
866 name,
864 name,
867 f,
865 f,
868 pre,
866 pre,
869 filters,
867 filters,
870 pats,
868 pats,
871 context,
869 context,
872 logfunc,
870 logfunc,
873 maxerr,
871 maxerr,
874 warnings,
872 warnings,
875 blame,
873 blame,
876 debug,
874 debug,
877 lineno,
875 lineno,
878 )
876 )
879 if fc:
877 if fc:
880 result = False
878 result = False
881
879
882 if f.endswith('.t') and "no-" "check-code" not in pre:
880 if f.endswith('.t') and "no-" "check-code" not in pre:
883 if debug:
881 if debug:
884 print("Checking embedded code in %s" % f)
882 print("Checking embedded code in %s" % f)
885
883
886 prelines = pre.splitlines()
884 prelines = pre.splitlines()
887 embeddederros = []
885 embeddederros = []
888 for name, embedded, filters, pats in embeddedchecks:
886 for name, embedded, filters, pats in embeddedchecks:
889 # "reset curmax at each repetition" treats maxerr as "max
887 # "reset curmax at each repetition" treats maxerr as "max
890 # nubmer of errors in an actual file per entry of
888 # nubmer of errors in an actual file per entry of
891 # (embedded)checks"
889 # (embedded)checks"
892 curmaxerr = maxerr
890 curmaxerr = maxerr
893
891
894 for found in embedded(f, prelines, embeddederros):
892 for found in embedded(f, prelines, embeddederros):
895 filename, starts, ends, code = found
893 filename, starts, ends, code = found
896 fc = _checkfiledata(
894 fc = _checkfiledata(
897 name,
895 name,
898 f,
896 f,
899 code,
897 code,
900 filters,
898 filters,
901 pats,
899 pats,
902 context,
900 context,
903 logfunc,
901 logfunc,
904 curmaxerr,
902 curmaxerr,
905 warnings,
903 warnings,
906 blame,
904 blame,
907 debug,
905 debug,
908 lineno,
906 lineno,
909 offset=starts - 1,
907 offset=starts - 1,
910 )
908 )
911 if fc:
909 if fc:
912 result = False
910 result = False
913 if curmaxerr:
911 if curmaxerr:
914 if fc >= curmaxerr:
912 if fc >= curmaxerr:
915 break
913 break
916 curmaxerr -= fc
914 curmaxerr -= fc
917
915
918 return result
916 return result
919
917
920
918
921 def _checkfiledata(
919 def _checkfiledata(
922 name,
920 name,
923 f,
921 f,
924 filedata,
922 filedata,
925 filters,
923 filters,
926 pats,
924 pats,
927 context,
925 context,
928 logfunc,
926 logfunc,
929 maxerr,
927 maxerr,
930 warnings,
928 warnings,
931 blame,
929 blame,
932 debug,
930 debug,
933 lineno,
931 lineno,
934 offset=None,
932 offset=None,
935 ):
933 ):
936 """Execute actual error check for file data
934 """Execute actual error check for file data
937
935
938 :name: of the checking category
936 :name: of the checking category
939 :f: filepath
937 :f: filepath
940 :filedata: content of a file
938 :filedata: content of a file
941 :filters: to be applied before checking
939 :filters: to be applied before checking
942 :pats: to detect errors
940 :pats: to detect errors
943 :context: a dict of information shared while single checkfile() invocation
941 :context: a dict of information shared while single checkfile() invocation
944 Valid keys: 'blamecache'.
942 Valid keys: 'blamecache'.
945 :logfunc: function used to report error
943 :logfunc: function used to report error
946 logfunc(filename, linenumber, linecontent, errormessage)
944 logfunc(filename, linenumber, linecontent, errormessage)
947 :maxerr: number of error to display before aborting, or False to
945 :maxerr: number of error to display before aborting, or False to
948 report all errors
946 report all errors
949 :warnings: whether warning level checks should be applied
947 :warnings: whether warning level checks should be applied
950 :blame: whether blame information should be displayed at error reporting
948 :blame: whether blame information should be displayed at error reporting
951 :debug: whether debug information should be displayed
949 :debug: whether debug information should be displayed
952 :lineno: whether lineno should be displayed at error reporting
950 :lineno: whether lineno should be displayed at error reporting
953 :offset: line number offset of 'filedata' in 'f' for checking
951 :offset: line number offset of 'filedata' in 'f' for checking
954 an embedded code fragment, or None (offset=0 is different
952 an embedded code fragment, or None (offset=0 is different
955 from offset=None)
953 from offset=None)
956
954
957 returns number of detected errors.
955 returns number of detected errors.
958 """
956 """
959 blamecache = context['blamecache']
957 blamecache = context['blamecache']
960 if offset is None:
958 if offset is None:
961 lineoffset = 0
959 lineoffset = 0
962 else:
960 else:
963 lineoffset = offset
961 lineoffset = offset
964
962
965 fc = 0
963 fc = 0
966 pre = post = filedata
964 pre = post = filedata
967
965
968 if True: # TODO: get rid of this redundant 'if' block
966 if True: # TODO: get rid of this redundant 'if' block
969 for p, r in filters:
967 for p, r in filters:
970 post = re.sub(p, r, post)
968 post = re.sub(p, r, post)
971 nerrs = len(pats[0]) # nerr elements are errors
969 nerrs = len(pats[0]) # nerr elements are errors
972 if warnings:
970 if warnings:
973 pats = pats[0] + pats[1]
971 pats = pats[0] + pats[1]
974 else:
972 else:
975 pats = pats[0]
973 pats = pats[0]
976 # print post # uncomment to show filtered version
974 # print post # uncomment to show filtered version
977
975
978 if debug:
976 if debug:
979 print("Checking %s for %s" % (name, f))
977 print("Checking %s for %s" % (name, f))
980
978
981 prelines = None
979 prelines = None
982 errors = []
980 errors = []
983 for i, pat in enumerate(pats):
981 for i, pat in enumerate(pats):
984 if len(pat) == 3:
982 if len(pat) == 3:
985 p, msg, ignore = pat
983 p, msg, ignore = pat
986 else:
984 else:
987 p, msg = pat
985 p, msg = pat
988 ignore = None
986 ignore = None
989 if i >= nerrs:
987 if i >= nerrs:
990 msg = "warning: " + msg
988 msg = "warning: " + msg
991
989
992 pos = 0
990 pos = 0
993 n = 0
991 n = 0
994 for m in p.finditer(post):
992 for m in p.finditer(post):
995 if prelines is None:
993 if prelines is None:
996 prelines = pre.splitlines()
994 prelines = pre.splitlines()
997 postlines = post.splitlines(True)
995 postlines = post.splitlines(True)
998
996
999 start = m.start()
997 start = m.start()
1000 while n < len(postlines):
998 while n < len(postlines):
1001 step = len(postlines[n])
999 step = len(postlines[n])
1002 if pos + step > start:
1000 if pos + step > start:
1003 break
1001 break
1004 pos += step
1002 pos += step
1005 n += 1
1003 n += 1
1006 l = prelines[n]
1004 l = prelines[n]
1007
1005
1008 if ignore and re.search(ignore, l, re.MULTILINE):
1006 if ignore and re.search(ignore, l, re.MULTILINE):
1009 if debug:
1007 if debug:
1010 print(
1008 print(
1011 "Skipping %s for %s:%s (ignore pattern)"
1009 "Skipping %s for %s:%s (ignore pattern)"
1012 % (name, f, (n + lineoffset))
1010 % (name, f, (n + lineoffset))
1013 )
1011 )
1014 continue
1012 continue
1015 bd = ""
1013 bd = ""
1016 if blame:
1014 if blame:
1017 bd = 'working directory'
1015 bd = 'working directory'
1018 if blamecache is None:
1016 if blamecache is None:
1019 blamecache = getblame(f)
1017 blamecache = getblame(f)
1020 context['blamecache'] = blamecache
1018 context['blamecache'] = blamecache
1021 if (n + lineoffset) < len(blamecache):
1019 if (n + lineoffset) < len(blamecache):
1022 bl, bu, br = blamecache[(n + lineoffset)]
1020 bl, bu, br = blamecache[(n + lineoffset)]
1023 if offset is None and bl == l:
1021 if offset is None and bl == l:
1024 bd = '%s@%s' % (bu, br)
1022 bd = '%s@%s' % (bu, br)
1025 elif offset is not None and bl.endswith(l):
1023 elif offset is not None and bl.endswith(l):
1026 # "offset is not None" means "checking
1024 # "offset is not None" means "checking
1027 # embedded code fragment". In this case,
1025 # embedded code fragment". In this case,
1028 # "l" does not have information about the
1026 # "l" does not have information about the
1029 # beginning of an *original* line in the
1027 # beginning of an *original* line in the
1030 # file (e.g. ' > ').
1028 # file (e.g. ' > ').
1031 # Therefore, use "str.endswith()", and
1029 # Therefore, use "str.endswith()", and
1032 # show "maybe" for a little loose
1030 # show "maybe" for a little loose
1033 # examination.
1031 # examination.
1034 bd = '%s@%s, maybe' % (bu, br)
1032 bd = '%s@%s, maybe' % (bu, br)
1035
1033
1036 errors.append((f, lineno and (n + lineoffset + 1), l, msg, bd))
1034 errors.append((f, lineno and (n + lineoffset + 1), l, msg, bd))
1037
1035
1038 errors.sort()
1036 errors.sort()
1039 for e in errors:
1037 for e in errors:
1040 logfunc(*e)
1038 logfunc(*e)
1041 fc += 1
1039 fc += 1
1042 if maxerr and fc >= maxerr:
1040 if maxerr and fc >= maxerr:
1043 print(" (too many errors, giving up)")
1041 print(" (too many errors, giving up)")
1044 break
1042 break
1045
1043
1046 return fc
1044 return fc
1047
1045
1048
1046
1049 def main():
1047 def main():
1050 parser = optparse.OptionParser("%prog [options] [files | -]")
1048 parser = optparse.OptionParser("%prog [options] [files | -]")
1051 parser.add_option(
1049 parser.add_option(
1052 "-w",
1050 "-w",
1053 "--warnings",
1051 "--warnings",
1054 action="store_true",
1052 action="store_true",
1055 help="include warning-level checks",
1053 help="include warning-level checks",
1056 )
1054 )
1057 parser.add_option(
1055 parser.add_option(
1058 "-p", "--per-file", type="int", help="max warnings per file"
1056 "-p", "--per-file", type="int", help="max warnings per file"
1059 )
1057 )
1060 parser.add_option(
1058 parser.add_option(
1061 "-b",
1059 "-b",
1062 "--blame",
1060 "--blame",
1063 action="store_true",
1061 action="store_true",
1064 help="use annotate to generate blame info",
1062 help="use annotate to generate blame info",
1065 )
1063 )
1066 parser.add_option(
1064 parser.add_option(
1067 "", "--debug", action="store_true", help="show debug information"
1065 "", "--debug", action="store_true", help="show debug information"
1068 )
1066 )
1069 parser.add_option(
1067 parser.add_option(
1070 "",
1068 "",
1071 "--nolineno",
1069 "--nolineno",
1072 action="store_false",
1070 action="store_false",
1073 dest='lineno',
1071 dest='lineno',
1074 help="don't show line numbers",
1072 help="don't show line numbers",
1075 )
1073 )
1076
1074
1077 parser.set_defaults(
1075 parser.set_defaults(
1078 per_file=15, warnings=False, blame=False, debug=False, lineno=True
1076 per_file=15, warnings=False, blame=False, debug=False, lineno=True
1079 )
1077 )
1080 (options, args) = parser.parse_args()
1078 (options, args) = parser.parse_args()
1081
1079
1082 if len(args) == 0:
1080 if len(args) == 0:
1083 check = glob.glob("*")
1081 check = glob.glob("*")
1084 elif args == ['-']:
1082 elif args == ['-']:
1085 # read file list from stdin
1083 # read file list from stdin
1086 check = sys.stdin.read().splitlines()
1084 check = sys.stdin.read().splitlines()
1087 else:
1085 else:
1088 check = args
1086 check = args
1089
1087
1090 _preparepats()
1088 _preparepats()
1091
1089
1092 ret = 0
1090 ret = 0
1093 for f in check:
1091 for f in check:
1094 if not checkfile(
1092 if not checkfile(
1095 f,
1093 f,
1096 maxerr=options.per_file,
1094 maxerr=options.per_file,
1097 warnings=options.warnings,
1095 warnings=options.warnings,
1098 blame=options.blame,
1096 blame=options.blame,
1099 debug=options.debug,
1097 debug=options.debug,
1100 lineno=options.lineno,
1098 lineno=options.lineno,
1101 ):
1099 ):
1102 ret = 1
1100 ret = 1
1103 return ret
1101 return ret
1104
1102
1105
1103
1106 if __name__ == "__main__":
1104 if __name__ == "__main__":
1107 sys.exit(main())
1105 sys.exit(main())
@@ -1,109 +1,103 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # Copyright 2014 Matt Mackall <mpm@selenic.com>
3 # Copyright 2014 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # A tool/hook to run basic sanity checks on commits/patches for
5 # A tool/hook to run basic sanity checks on commits/patches for
6 # submission to Mercurial. Install by adding the following to your
6 # submission to Mercurial. Install by adding the following to your
7 # .hg/hgrc:
7 # .hg/hgrc:
8 #
8 #
9 # [hooks]
9 # [hooks]
10 # pretxncommit = contrib/check-commit
10 # pretxncommit = contrib/check-commit
11 #
11 #
12 # The hook can be temporarily bypassed with:
12 # The hook can be temporarily bypassed with:
13 #
13 #
14 # $ BYPASS= hg commit
14 # $ BYPASS= hg commit
15 #
15 #
16 # See also: https://mercurial-scm.org/wiki/ContributingChanges
16 # See also: https://mercurial-scm.org/wiki/ContributingChanges
17
17
18 from __future__ import absolute_import, print_function
18 from __future__ import absolute_import, print_function
19
19
20 import os
20 import os
21 import re
21 import re
22 import sys
22 import sys
23
23
24 commitheader = r"^(?:# [^\n]*\n)*"
24 commitheader = r"^(?:# [^\n]*\n)*"
25 afterheader = commitheader + r"(?!#)"
25 afterheader = commitheader + r"(?!#)"
26 beforepatch = afterheader + r"(?!\n(?!@@))"
26 beforepatch = afterheader + r"(?!\n(?!@@))"
27
27
28 errors = [
28 errors = [
29 (beforepatch + r".*[(]bc[)]", "(BC) needs to be uppercase"),
29 (beforepatch + r".*[(]bc[)]", "(BC) needs to be uppercase"),
30 (beforepatch + r".*[(]issue \d\d\d",
30 (beforepatch + r".*[(]issue \d\d\d",
31 "no space allowed between issue and number"),
31 "no space allowed between issue and number"),
32 (beforepatch + r".*[(]bug(\d|\s)", "use (issueDDDD) instead of bug"),
32 (beforepatch + r".*[(]bug(\d|\s)", "use (issueDDDD) instead of bug"),
33 (commitheader + r"# User [^@\n]+\n", "username is not an email address"),
33 (commitheader + r"# User [^@\n]+\n", "username is not an email address"),
34 (commitheader + r"(?!merge with )[^#]\S+[^:] ",
34 (commitheader + r"(?!merge with )[^#]\S+[^:] ",
35 "summary line doesn't start with 'topic: '"),
35 "summary line doesn't start with 'topic: '"),
36 (afterheader + r"[A-Z][a-z]\S+", "don't capitalize summary lines"),
36 (afterheader + r"[A-Z][a-z]\S+", "don't capitalize summary lines"),
37 (afterheader + r"^\S+: *[A-Z][a-z]\S+", "don't capitalize summary lines"),
37 (afterheader + r"^\S+: *[A-Z][a-z]\S+", "don't capitalize summary lines"),
38 (afterheader + r"\S*[^A-Za-z0-9-_]\S*: ",
38 (afterheader + r"\S*[^A-Za-z0-9-_]\S*: ",
39 "summary keyword should be most user-relevant one-word command or topic"),
39 "summary keyword should be most user-relevant one-word command or topic"),
40 (afterheader + r".*\.\s*\n", "don't add trailing period on summary line"),
40 (afterheader + r".*\.\s*\n", "don't add trailing period on summary line"),
41 (afterheader + r".{79,}", "summary line too long (limit is 78)"),
41 (afterheader + r".{79,}", "summary line too long (limit is 78)"),
42 # Forbid "_" in function name.
43 #
44 # We skip the check for cffi related functions. They use names mapping the
45 # name of the C function. C function names may contain "_".
46 (r"\n\+[ \t]+def (?!cffi)[a-z]+_[a-z]",
47 "adds a function with foo_bar naming"),
48 ]
42 ]
49
43
50 word = re.compile(r'\S')
44 word = re.compile(r'\S')
51 def nonempty(first, second):
45 def nonempty(first, second):
52 if word.search(first):
46 if word.search(first):
53 return first
47 return first
54 return second
48 return second
55
49
56 def checkcommit(commit, node=None):
50 def checkcommit(commit, node=None):
57 exitcode = 0
51 exitcode = 0
58 printed = node is None
52 printed = node is None
59 hits = []
53 hits = []
60 signtag = (afterheader +
54 signtag = (afterheader +
61 r'Added (tag [^ ]+|signature) for changeset [a-f0-9]{12}')
55 r'Added (tag [^ ]+|signature) for changeset [a-f0-9]{12}')
62 if re.search(signtag, commit):
56 if re.search(signtag, commit):
63 return 0
57 return 0
64 for exp, msg in errors:
58 for exp, msg in errors:
65 for m in re.finditer(exp, commit):
59 for m in re.finditer(exp, commit):
66 end = m.end()
60 end = m.end()
67 trailing = re.search(r'(\\n)+$', exp)
61 trailing = re.search(r'(\\n)+$', exp)
68 if trailing:
62 if trailing:
69 end -= len(trailing.group()) / 2
63 end -= len(trailing.group()) / 2
70 hits.append((end, exp, msg))
64 hits.append((end, exp, msg))
71 if hits:
65 if hits:
72 hits.sort()
66 hits.sort()
73 pos = 0
67 pos = 0
74 last = ''
68 last = ''
75 for n, l in enumerate(commit.splitlines(True)):
69 for n, l in enumerate(commit.splitlines(True)):
76 pos += len(l)
70 pos += len(l)
77 while len(hits):
71 while len(hits):
78 end, exp, msg = hits[0]
72 end, exp, msg = hits[0]
79 if pos < end:
73 if pos < end:
80 break
74 break
81 if not printed:
75 if not printed:
82 printed = True
76 printed = True
83 print("node: %s" % node)
77 print("node: %s" % node)
84 print("%d: %s" % (n, msg))
78 print("%d: %s" % (n, msg))
85 print(" %s" % nonempty(l, last)[:-1])
79 print(" %s" % nonempty(l, last)[:-1])
86 if "BYPASS" not in os.environ:
80 if "BYPASS" not in os.environ:
87 exitcode = 1
81 exitcode = 1
88 del hits[0]
82 del hits[0]
89 last = nonempty(l, last)
83 last = nonempty(l, last)
90
84
91 return exitcode
85 return exitcode
92
86
93 def readcommit(node):
87 def readcommit(node):
94 return os.popen("hg export %s" % node).read()
88 return os.popen("hg export %s" % node).read()
95
89
96 if __name__ == "__main__":
90 if __name__ == "__main__":
97 exitcode = 0
91 exitcode = 0
98 node = os.environ.get("HG_NODE")
92 node = os.environ.get("HG_NODE")
99
93
100 if node:
94 if node:
101 commit = readcommit(node)
95 commit = readcommit(node)
102 exitcode = checkcommit(commit)
96 exitcode = checkcommit(commit)
103 elif sys.argv[1:]:
97 elif sys.argv[1:]:
104 for node in sys.argv[1:]:
98 for node in sys.argv[1:]:
105 exitcode |= checkcommit(readcommit(node), node)
99 exitcode |= checkcommit(readcommit(node), node)
106 else:
100 else:
107 commit = sys.stdin.read()
101 commit = sys.stdin.read()
108 exitcode = checkcommit(commit)
102 exitcode = checkcommit(commit)
109 sys.exit(exitcode)
103 sys.exit(exitcode)
@@ -1,135 +1,133 b''
1 Test the 'check-commit' script
1 Test the 'check-commit' script
2 ==============================
2 ==============================
3
3
4 A fine patch:
4 A fine patch:
5
5
6 $ cat > patch-with-long-header.diff << EOF
6 $ cat > patch-with-long-header.diff << EOF
7 > # HG changeset patch
7 > # HG changeset patch
8 > # User timeless <timeless@mozdev.org>
8 > # User timeless <timeless@mozdev.org>
9 > # Date 1448911706 0
9 > # Date 1448911706 0
10 > # Mon Nov 30 19:28:26 2015 +0000
10 > # Mon Nov 30 19:28:26 2015 +0000
11 > # Node ID c41cb6d2b7dbd62b1033727f8606b8c09fc4aa88
11 > # Node ID c41cb6d2b7dbd62b1033727f8606b8c09fc4aa88
12 > # Parent 42aa0e570eaa364a622bc4443b0bcb79b1100a58
12 > # Parent 42aa0e570eaa364a622bc4443b0bcb79b1100a58
13 > # ClownJoke This is a veryly long header that should not be warned about because its not the description
13 > # ClownJoke This is a veryly long header that should not be warned about because its not the description
14 > bundle2: use Oxford comma (issue123) (BC)
14 > bundle2: use Oxford comma (issue123) (BC)
15 >
15 >
16 > diff --git a/hgext/transplant.py b/hgext/transplant.py
16 > diff --git a/hgext/transplant.py b/hgext/transplant.py
17 > --- a/hgext/transplant.py
17 > --- a/hgext/transplant.py
18 > +++ b/hgext/transplant.py
18 > +++ b/hgext/transplant.py
19 > @@ -599,7 +599,7 @@
19 > @@ -599,7 +599,7 @@
20 > return
20 > return
21 > if not (opts.get('source') or revs or
21 > if not (opts.get('source') or revs or
22 > opts.get('merge') or opts.get('branch')):
22 > opts.get('merge') or opts.get('branch')):
23 > - raise error.Abort(_('no source URL, branch revision or revision '
23 > - raise error.Abort(_('no source URL, branch revision or revision '
24 > + raise error.Abort(_('no source URL, branch revision, or revision '
24 > + raise error.Abort(_('no source URL, branch revision, or revision '
25 > 'list provided'))
25 > 'list provided'))
26 > if opts.get('all'):
26 > if opts.get('all'):
27 >
27 >
28 > + def blahblah(x):
28 > + def blahblah(x):
29 > + pass
29 > + pass
30 > EOF
30 > EOF
31 $ cat patch-with-long-header.diff | $TESTDIR/../contrib/check-commit
31 $ cat patch-with-long-header.diff | $TESTDIR/../contrib/check-commit
32
32
33 This would normally be against the rules, but it's okay because that's
33 This would normally be against the rules, but it's okay because that's
34 what tagging and signing looks like:
34 what tagging and signing looks like:
35
35
36 $ cat > creates-a-tag.diff << EOF
36 $ cat > creates-a-tag.diff << EOF
37 > # HG changeset patch
37 > # HG changeset patch
38 > # User Augie Fackler <raf@durin42.com>
38 > # User Augie Fackler <raf@durin42.com>
39 > # Date 1484787778 18000
39 > # Date 1484787778 18000
40 > # Wed Jan 18 20:02:58 2017 -0500
40 > # Wed Jan 18 20:02:58 2017 -0500
41 > # Branch stable
41 > # Branch stable
42 > # Node ID c177635e4acf52923bc3aa9f72a5b1ad1197b173
42 > # Node ID c177635e4acf52923bc3aa9f72a5b1ad1197b173
43 > # Parent a1dd2c0c479e0550040542e392e87bc91262517e
43 > # Parent a1dd2c0c479e0550040542e392e87bc91262517e
44 > Added tag 4.1-rc for changeset a1dd2c0c479e
44 > Added tag 4.1-rc for changeset a1dd2c0c479e
45 >
45 >
46 > diff --git a/.hgtags b/.hgtags
46 > diff --git a/.hgtags b/.hgtags
47 > --- a/.hgtags
47 > --- a/.hgtags
48 > +++ b/.hgtags
48 > +++ b/.hgtags
49 > @@ -150,3 +150,4 @@ 438173c415874f6ac653efc1099dec9c9150e90f
49 > @@ -150,3 +150,4 @@ 438173c415874f6ac653efc1099dec9c9150e90f
50 > eab27446995210c334c3d06f1a659e3b9b5da769 4.0
50 > eab27446995210c334c3d06f1a659e3b9b5da769 4.0
51 > b3b1ae98f6a0e14c1e1ba806a6c18e193b6dae5c 4.0.1
51 > b3b1ae98f6a0e14c1e1ba806a6c18e193b6dae5c 4.0.1
52 > e69874dc1f4e142746ff3df91e678a09c6fc208c 4.0.2
52 > e69874dc1f4e142746ff3df91e678a09c6fc208c 4.0.2
53 > +a1dd2c0c479e0550040542e392e87bc91262517e 4.1-rc
53 > +a1dd2c0c479e0550040542e392e87bc91262517e 4.1-rc
54 > EOF
54 > EOF
55 $ $TESTDIR/../contrib/check-commit < creates-a-tag.diff
55 $ $TESTDIR/../contrib/check-commit < creates-a-tag.diff
56
56
57 A patch with lots of errors:
57 A patch with lots of errors:
58
58
59 $ cat > patch-with-long-header.diff << EOF
59 $ cat > patch-with-long-header.diff << EOF
60 > # HG changeset patch
60 > # HG changeset patch
61 > # User timeless
61 > # User timeless
62 > # Date 1448911706 0
62 > # Date 1448911706 0
63 > # Mon Nov 30 19:28:26 2015 +0000
63 > # Mon Nov 30 19:28:26 2015 +0000
64 > # Node ID c41cb6d2b7dbd62b1033727f8606b8c09fc4aa88
64 > # Node ID c41cb6d2b7dbd62b1033727f8606b8c09fc4aa88
65 > # Parent 42aa0e570eaa364a622bc4443b0bcb79b1100a58
65 > # Parent 42aa0e570eaa364a622bc4443b0bcb79b1100a58
66 > # ClownJoke This is a veryly long header that should not be warned about because its not the description
66 > # ClownJoke This is a veryly long header that should not be warned about because its not the description
67 > transplant/foo: this summary is way too long use Oxford comma (bc) (bug123) (issue 244)
67 > transplant/foo: this summary is way too long use Oxford comma (bc) (bug123) (issue 244)
68 >
68 >
69 > diff --git a/hgext/transplant.py b/hgext/transplant.py
69 > diff --git a/hgext/transplant.py b/hgext/transplant.py
70 > --- a/hgext/transplant.py
70 > --- a/hgext/transplant.py
71 > +++ b/hgext/transplant.py
71 > +++ b/hgext/transplant.py
72 > @@ -599,7 +599,7 @@
72 > @@ -599,7 +599,7 @@
73 > return
73 > return
74 > if not (opts.get('source') or revs or
74 > if not (opts.get('source') or revs or
75 > opts.get('merge') or opts.get('branch')):
75 > opts.get('merge') or opts.get('branch')):
76 > - raise error.Abort(_('no source URL, branch revision or revision '
76 > - raise error.Abort(_('no source URL, branch revision or revision '
77 > + raise error.Abort(_('no source URL, branch revision, or revision '
77 > + raise error.Abort(_('no source URL, branch revision, or revision '
78 > 'list provided'))
78 > 'list provided'))
79 > if opts.get('all'):
79 > if opts.get('all'):
80 > EOF
80 > EOF
81 $ cat patch-with-long-header.diff | $TESTDIR/../contrib/check-commit
81 $ cat patch-with-long-header.diff | $TESTDIR/../contrib/check-commit
82 1: username is not an email address
82 1: username is not an email address
83 # User timeless
83 # User timeless
84 7: summary keyword should be most user-relevant one-word command or topic
84 7: summary keyword should be most user-relevant one-word command or topic
85 transplant/foo: this summary is way too long use Oxford comma (bc) (bug123) (issue 244)
85 transplant/foo: this summary is way too long use Oxford comma (bc) (bug123) (issue 244)
86 7: (BC) needs to be uppercase
86 7: (BC) needs to be uppercase
87 transplant/foo: this summary is way too long use Oxford comma (bc) (bug123) (issue 244)
87 transplant/foo: this summary is way too long use Oxford comma (bc) (bug123) (issue 244)
88 7: use (issueDDDD) instead of bug
88 7: use (issueDDDD) instead of bug
89 transplant/foo: this summary is way too long use Oxford comma (bc) (bug123) (issue 244)
89 transplant/foo: this summary is way too long use Oxford comma (bc) (bug123) (issue 244)
90 7: no space allowed between issue and number
90 7: no space allowed between issue and number
91 transplant/foo: this summary is way too long use Oxford comma (bc) (bug123) (issue 244)
91 transplant/foo: this summary is way too long use Oxford comma (bc) (bug123) (issue 244)
92 7: summary line too long (limit is 78)
92 7: summary line too long (limit is 78)
93 transplant/foo: this summary is way too long use Oxford comma (bc) (bug123) (issue 244)
93 transplant/foo: this summary is way too long use Oxford comma (bc) (bug123) (issue 244)
94 [1]
94 [1]
95
95
96 A patch with other errors:
96 A patch with other errors:
97
97
98 $ cat > patch-with-long-header.diff << EOF
98 $ cat > patch-with-long-header.diff << EOF
99 > # HG changeset patch
99 > # HG changeset patch
100 > # User timeless
100 > # User timeless
101 > # Date 1448911706 0
101 > # Date 1448911706 0
102 > # Mon Nov 30 19:28:26 2015 +0000
102 > # Mon Nov 30 19:28:26 2015 +0000
103 > # Node ID c41cb6d2b7dbd62b1033727f8606b8c09fc4aa88
103 > # Node ID c41cb6d2b7dbd62b1033727f8606b8c09fc4aa88
104 > # Parent 42aa0e570eaa364a622bc4443b0bcb79b1100a58
104 > # Parent 42aa0e570eaa364a622bc4443b0bcb79b1100a58
105 > # ClownJoke This is a veryly long header that should not be warned about because its not the description
105 > # ClownJoke This is a veryly long header that should not be warned about because its not the description
106 > This has no topic and ends with a period.
106 > This has no topic and ends with a period.
107 >
107 >
108 > diff --git a/hgext/transplant.py b/hgext/transplant.py
108 > diff --git a/hgext/transplant.py b/hgext/transplant.py
109 > --- a/hgext/transplant.py
109 > --- a/hgext/transplant.py
110 > +++ b/hgext/transplant.py
110 > +++ b/hgext/transplant.py
111 > @@ -599,7 +599,7 @@
111 > @@ -599,7 +599,7 @@
112 > if opts.get('all'):
112 > if opts.get('all'):
113 >
113 >
114 >
114 >
115 > +
115 > +
116 > + some = otherjunk
116 > + some = otherjunk
117 > +
117 > +
118 > +
118 > +
119 > + def blah_blah(x):
119 > + def blah_blah(x):
120 > + pass
120 > + pass
121 > +
121 > +
122 >
122 >
123 > EOF
123 > EOF
124 $ cat patch-with-long-header.diff | $TESTDIR/../contrib/check-commit
124 $ cat patch-with-long-header.diff | $TESTDIR/../contrib/check-commit
125 1: username is not an email address
125 1: username is not an email address
126 # User timeless
126 # User timeless
127 7: don't capitalize summary lines
127 7: don't capitalize summary lines
128 This has no topic and ends with a period.
128 This has no topic and ends with a period.
129 7: summary line doesn't start with 'topic: '
129 7: summary line doesn't start with 'topic: '
130 This has no topic and ends with a period.
130 This has no topic and ends with a period.
131 7: don't add trailing period on summary line
131 7: don't add trailing period on summary line
132 This has no topic and ends with a period.
132 This has no topic and ends with a period.
133 20: adds a function with foo_bar naming
134 + def blah_blah(x):
135 [1]
133 [1]
General Comments 0
You need to be logged in to leave comments. Login now