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