##// END OF EJS Templates
contrib: add line offset information to file check function of check-code.py...
FUJIWARA Katsunori -
r41991:6d6bd903 default
parent child Browse files
Show More
@@ -1,810 +1,828 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 if sys.version_info[0] < 3:
29 if sys.version_info[0] < 3:
30 opentext = open
30 opentext = open
31 else:
31 else:
32 def opentext(f):
32 def opentext(f):
33 return open(f, encoding='latin1')
33 return open(f, encoding='latin1')
34 try:
34 try:
35 xrange
35 xrange
36 except NameError:
36 except NameError:
37 xrange = range
37 xrange = range
38 try:
38 try:
39 import re2
39 import re2
40 except ImportError:
40 except ImportError:
41 re2 = None
41 re2 = None
42
42
43 def compilere(pat, multiline=False):
43 def compilere(pat, multiline=False):
44 if multiline:
44 if multiline:
45 pat = '(?m)' + pat
45 pat = '(?m)' + pat
46 if re2:
46 if re2:
47 try:
47 try:
48 return re2.compile(pat)
48 return re2.compile(pat)
49 except re2.error:
49 except re2.error:
50 pass
50 pass
51 return re.compile(pat)
51 return re.compile(pat)
52
52
53 # check "rules depending on implementation of repquote()" in each
53 # check "rules depending on implementation of repquote()" in each
54 # patterns (especially pypats), before changing around repquote()
54 # patterns (especially pypats), before changing around repquote()
55 _repquotefixedmap = {' ': ' ', '\n': '\n', '.': 'p', ':': 'q',
55 _repquotefixedmap = {' ': ' ', '\n': '\n', '.': 'p', ':': 'q',
56 '%': '%', '\\': 'b', '*': 'A', '+': 'P', '-': 'M'}
56 '%': '%', '\\': 'b', '*': 'A', '+': 'P', '-': 'M'}
57 def _repquoteencodechr(i):
57 def _repquoteencodechr(i):
58 if i > 255:
58 if i > 255:
59 return 'u'
59 return 'u'
60 c = chr(i)
60 c = chr(i)
61 if c in _repquotefixedmap:
61 if c in _repquotefixedmap:
62 return _repquotefixedmap[c]
62 return _repquotefixedmap[c]
63 if c.isalpha():
63 if c.isalpha():
64 return 'x'
64 return 'x'
65 if c.isdigit():
65 if c.isdigit():
66 return 'n'
66 return 'n'
67 return 'o'
67 return 'o'
68 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
68 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
69
69
70 def repquote(m):
70 def repquote(m):
71 t = m.group('text')
71 t = m.group('text')
72 t = t.translate(_repquotett)
72 t = t.translate(_repquotett)
73 return m.group('quote') + t + m.group('quote')
73 return m.group('quote') + t + m.group('quote')
74
74
75 def reppython(m):
75 def reppython(m):
76 comment = m.group('comment')
76 comment = m.group('comment')
77 if comment:
77 if comment:
78 l = len(comment.rstrip())
78 l = len(comment.rstrip())
79 return "#" * l + comment[l:]
79 return "#" * l + comment[l:]
80 return repquote(m)
80 return repquote(m)
81
81
82 def repcomment(m):
82 def repcomment(m):
83 return m.group(1) + "#" * len(m.group(2))
83 return m.group(1) + "#" * len(m.group(2))
84
84
85 def repccomment(m):
85 def repccomment(m):
86 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
86 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
87 return m.group(1) + t + "*/"
87 return m.group(1) + t + "*/"
88
88
89 def repcallspaces(m):
89 def repcallspaces(m):
90 t = re.sub(r"\n\s+", "\n", m.group(2))
90 t = re.sub(r"\n\s+", "\n", m.group(2))
91 return m.group(1) + t
91 return m.group(1) + t
92
92
93 def repinclude(m):
93 def repinclude(m):
94 return m.group(1) + "<foo>"
94 return m.group(1) + "<foo>"
95
95
96 def rephere(m):
96 def rephere(m):
97 t = re.sub(r"\S", "x", m.group(2))
97 t = re.sub(r"\S", "x", m.group(2))
98 return m.group(1) + t
98 return m.group(1) + t
99
99
100
100
101 testpats = [
101 testpats = [
102 [
102 [
103 (r'\b(push|pop)d\b', "don't use 'pushd' or 'popd', use 'cd'"),
103 (r'\b(push|pop)d\b', "don't use 'pushd' or 'popd', use 'cd'"),
104 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
104 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
105 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
105 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
106 (r'(?<!hg )grep.* -a', "don't use 'grep -a', use in-line python"),
106 (r'(?<!hg )grep.* -a', "don't use 'grep -a', use in-line python"),
107 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
107 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
108 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
108 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
109 (r'echo -n', "don't use 'echo -n', use printf"),
109 (r'echo -n', "don't use 'echo -n', use printf"),
110 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
110 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
111 (r'head -c', "don't use 'head -c', use 'dd'"),
111 (r'head -c', "don't use 'head -c', use 'dd'"),
112 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
112 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
113 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
113 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
114 (r'\bls\b.*-\w*R', "don't use 'ls -R', use 'find'"),
114 (r'\bls\b.*-\w*R', "don't use 'ls -R', use 'find'"),
115 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
115 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
116 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
116 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
117 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
117 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
118 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
118 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
119 (r'\[[^\]]+==', '[ foo == bar ] is a bashism, use [ foo = bar ] instead'),
119 (r'\[[^\]]+==', '[ foo == bar ] is a bashism, use [ foo = bar ] instead'),
120 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
120 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
121 "use egrep for extended grep syntax"),
121 "use egrep for extended grep syntax"),
122 (r'(^|\|\s*)e?grep .*\\S', "don't use \\S in regular expression"),
122 (r'(^|\|\s*)e?grep .*\\S', "don't use \\S in regular expression"),
123 (r'(?<!!)/bin/', "don't use explicit paths for tools"),
123 (r'(?<!!)/bin/', "don't use explicit paths for tools"),
124 (r'#!.*/bash', "don't use bash in shebang, use sh"),
124 (r'#!.*/bash', "don't use bash in shebang, use sh"),
125 (r'[^\n]\Z', "no trailing newline"),
125 (r'[^\n]\Z', "no trailing newline"),
126 (r'export .*=', "don't export and assign at once"),
126 (r'export .*=', "don't export and assign at once"),
127 (r'^source\b', "don't use 'source', use '.'"),
127 (r'^source\b', "don't use 'source', use '.'"),
128 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
128 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
129 (r'\bls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
129 (r'\bls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
130 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
130 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
131 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
131 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
132 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
132 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
133 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
133 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
134 (r'^alias\b.*=', "don't use alias, use a function"),
134 (r'^alias\b.*=', "don't use alias, use a function"),
135 (r'if\s*!', "don't use '!' to negate exit status"),
135 (r'if\s*!', "don't use '!' to negate exit status"),
136 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
136 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
137 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
137 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
138 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
138 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
139 "put a backslash-escaped newline after sed 'i' command"),
139 "put a backslash-escaped newline after sed 'i' command"),
140 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
140 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
141 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
141 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
142 (r'[\s="`\']python\s(?!bindings)', "don't use 'python', use '$PYTHON'"),
142 (r'[\s="`\']python\s(?!bindings)', "don't use 'python', use '$PYTHON'"),
143 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
143 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
144 (r'\butil\.Abort\b', "directly use error.Abort"),
144 (r'\butil\.Abort\b', "directly use error.Abort"),
145 (r'\|&', "don't use |&, use 2>&1"),
145 (r'\|&', "don't use |&, use 2>&1"),
146 (r'\w = +\w', "only one space after = allowed"),
146 (r'\w = +\w', "only one space after = allowed"),
147 (r'\bsed\b.*[^\\]\\n', "don't use 'sed ... \\n', use a \\ and a newline"),
147 (r'\bsed\b.*[^\\]\\n', "don't use 'sed ... \\n', use a \\ and a newline"),
148 (r'env.*-u', "don't use 'env -u VAR', use 'unset VAR'"),
148 (r'env.*-u', "don't use 'env -u VAR', use 'unset VAR'"),
149 (r'cp.* -r ', "don't use 'cp -r', use 'cp -R'"),
149 (r'cp.* -r ', "don't use 'cp -r', use 'cp -R'"),
150 (r'grep.* -[ABC]', "don't use grep's context flags"),
150 (r'grep.* -[ABC]', "don't use grep's context flags"),
151 (r'find.*-printf',
151 (r'find.*-printf',
152 "don't use 'find -printf', it doesn't exist on BSD find(1)"),
152 "don't use 'find -printf', it doesn't exist on BSD find(1)"),
153 (r'\$RANDOM ', "don't use bash-only $RANDOM to generate random values"),
153 (r'\$RANDOM ', "don't use bash-only $RANDOM to generate random values"),
154 ],
154 ],
155 # warnings
155 # warnings
156 [
156 [
157 (r'^function', "don't use 'function', use old style"),
157 (r'^function', "don't use 'function', use old style"),
158 (r'^diff.*-\w*N', "don't use 'diff -N'"),
158 (r'^diff.*-\w*N', "don't use 'diff -N'"),
159 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
159 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
160 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
160 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
161 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
161 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
162 ]
162 ]
163 ]
163 ]
164
164
165 testfilters = [
165 testfilters = [
166 (r"( *)(#([^!][^\n]*\S)?)", repcomment),
166 (r"( *)(#([^!][^\n]*\S)?)", repcomment),
167 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
167 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
168 ]
168 ]
169
169
170 uprefix = r"^ \$ "
170 uprefix = r"^ \$ "
171 utestpats = [
171 utestpats = [
172 [
172 [
173 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
173 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
174 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
174 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
175 "use regex test output patterns instead of sed"),
175 "use regex test output patterns instead of sed"),
176 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
176 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
177 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
177 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
178 (uprefix + r'.*\|\| echo.*(fail|error)',
178 (uprefix + r'.*\|\| echo.*(fail|error)',
179 "explicit exit code checks unnecessary"),
179 "explicit exit code checks unnecessary"),
180 (uprefix + r'set -e', "don't use set -e"),
180 (uprefix + r'set -e', "don't use set -e"),
181 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
181 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
182 (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite "
182 (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite "
183 "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx
183 "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx
184 '# no-msys'), # in test-pull.t which is skipped on windows
184 '# no-msys'), # in test-pull.t which is skipped on windows
185 (r'^ [^$>].*27\.0\.0\.1',
185 (r'^ [^$>].*27\.0\.0\.1',
186 'use $LOCALIP not an explicit loopback address'),
186 'use $LOCALIP not an explicit loopback address'),
187 (r'^ (?![>$] ).*\$LOCALIP.*[^)]$',
187 (r'^ (?![>$] ).*\$LOCALIP.*[^)]$',
188 'mark $LOCALIP output lines with (glob) to help tests in BSD jails'),
188 'mark $LOCALIP output lines with (glob) to help tests in BSD jails'),
189 (r'^ (cat|find): .*: \$ENOENT\$',
189 (r'^ (cat|find): .*: \$ENOENT\$',
190 'use test -f to test for file existence'),
190 'use test -f to test for file existence'),
191 (r'^ diff -[^ -]*p',
191 (r'^ diff -[^ -]*p',
192 "don't use (external) diff with -p for portability"),
192 "don't use (external) diff with -p for portability"),
193 (r' readlink ', 'use readlink.py instead of readlink'),
193 (r' readlink ', 'use readlink.py instead of readlink'),
194 (r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
194 (r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
195 "glob timezone field in diff output for portability"),
195 "glob timezone field in diff output for portability"),
196 (r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
196 (r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
197 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability"),
197 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability"),
198 (r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
198 (r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
199 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability"),
199 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability"),
200 (r'^ @@ -[0-9]+ [+][0-9]+ @@',
200 (r'^ @@ -[0-9]+ [+][0-9]+ @@',
201 "use '@@ -N* +N* @@ (glob)' style chunk header for portability"),
201 "use '@@ -N* +N* @@ (glob)' style chunk header for portability"),
202 (uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
202 (uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
203 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
203 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
204 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)"),
204 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)"),
205 ],
205 ],
206 # warnings
206 # warnings
207 [
207 [
208 (r'^ (?!.*\$LOCALIP)[^*?/\n]* \(glob\)$',
208 (r'^ (?!.*\$LOCALIP)[^*?/\n]* \(glob\)$',
209 "glob match with no glob string (?, *, /, and $LOCALIP)"),
209 "glob match with no glob string (?, *, /, and $LOCALIP)"),
210 ]
210 ]
211 ]
211 ]
212
212
213 # transform plain test rules to unified test's
213 # transform plain test rules to unified test's
214 for i in [0, 1]:
214 for i in [0, 1]:
215 for tp in testpats[i]:
215 for tp in testpats[i]:
216 p = tp[0]
216 p = tp[0]
217 m = tp[1]
217 m = tp[1]
218 if p.startswith(r'^'):
218 if p.startswith(r'^'):
219 p = r"^ [$>] (%s)" % p[1:]
219 p = r"^ [$>] (%s)" % p[1:]
220 else:
220 else:
221 p = r"^ [$>] .*(%s)" % p
221 p = r"^ [$>] .*(%s)" % p
222 utestpats[i].append((p, m) + tp[2:])
222 utestpats[i].append((p, m) + tp[2:])
223
223
224 # don't transform the following rules:
224 # don't transform the following rules:
225 # " > \t" and " \t" should be allowed in unified tests
225 # " > \t" and " \t" should be allowed in unified tests
226 testpats[0].append((r'^( *)\t', "don't use tabs to indent"))
226 testpats[0].append((r'^( *)\t', "don't use tabs to indent"))
227 utestpats[0].append((r'^( ?)\t', "don't use tabs to indent"))
227 utestpats[0].append((r'^( ?)\t', "don't use tabs to indent"))
228
228
229 utestfilters = [
229 utestfilters = [
230 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
230 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
231 (r"( +)(#([^!][^\n]*\S)?)", repcomment),
231 (r"( +)(#([^!][^\n]*\S)?)", repcomment),
232 ]
232 ]
233
233
234 # common patterns to check *.py
234 # common patterns to check *.py
235 commonpypats = [
235 commonpypats = [
236 [
236 [
237 (r'\\$', 'Use () to wrap long lines in Python, not \\'),
237 (r'\\$', 'Use () to wrap long lines in Python, not \\'),
238 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
238 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
239 "tuple parameter unpacking not available in Python 3+"),
239 "tuple parameter unpacking not available in Python 3+"),
240 (r'lambda\s*\(.*,.*\)',
240 (r'lambda\s*\(.*,.*\)',
241 "tuple parameter unpacking not available in Python 3+"),
241 "tuple parameter unpacking not available in Python 3+"),
242 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
242 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
243 (r'(?<!\.)\breduce\s*\(.*', "reduce is not available in Python 3+"),
243 (r'(?<!\.)\breduce\s*\(.*', "reduce is not available in Python 3+"),
244 (r'\bdict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
244 (r'\bdict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
245 'dict-from-generator'),
245 'dict-from-generator'),
246 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
246 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
247 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
247 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
248 (r'^\s*\t', "don't use tabs"),
248 (r'^\s*\t', "don't use tabs"),
249 (r'\S;\s*\n', "semicolon"),
249 (r'\S;\s*\n', "semicolon"),
250 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
250 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
251 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
251 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
252 (r'(\w|\)),\w', "missing whitespace after ,"),
252 (r'(\w|\)),\w', "missing whitespace after ,"),
253 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
253 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
254 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
254 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
255 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
255 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
256 ((
256 ((
257 # a line ending with a colon, potentially with trailing comments
257 # a line ending with a colon, potentially with trailing comments
258 r':([ \t]*#[^\n]*)?\n'
258 r':([ \t]*#[^\n]*)?\n'
259 # one that is not a pass and not only a comment
259 # one that is not a pass and not only a comment
260 r'(?P<indent>[ \t]+)[^#][^\n]+\n'
260 r'(?P<indent>[ \t]+)[^#][^\n]+\n'
261 # more lines at the same indent level
261 # more lines at the same indent level
262 r'((?P=indent)[^\n]+\n)*'
262 r'((?P=indent)[^\n]+\n)*'
263 # a pass at the same indent level, which is bogus
263 # a pass at the same indent level, which is bogus
264 r'(?P=indent)pass[ \t\n#]'
264 r'(?P=indent)pass[ \t\n#]'
265 ), 'omit superfluous pass'),
265 ), 'omit superfluous pass'),
266 (r'[^\n]\Z', "no trailing newline"),
266 (r'[^\n]\Z', "no trailing newline"),
267 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
267 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
268 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
268 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
269 # "don't use underbars in identifiers"),
269 # "don't use underbars in identifiers"),
270 (r'^\s+(self\.)?[A-Za-z][a-z0-9]+[A-Z]\w* = ',
270 (r'^\s+(self\.)?[A-Za-z][a-z0-9]+[A-Z]\w* = ',
271 "don't use camelcase in identifiers", r'#.*camelcase-required'),
271 "don't use camelcase in identifiers", r'#.*camelcase-required'),
272 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
272 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
273 "linebreak after :"),
273 "linebreak after :"),
274 (r'class\s[^( \n]+:', "old-style class, use class foo(object)",
274 (r'class\s[^( \n]+:', "old-style class, use class foo(object)",
275 r'#.*old-style'),
275 r'#.*old-style'),
276 (r'class\s[^( \n]+\(\):',
276 (r'class\s[^( \n]+\(\):',
277 "class foo() creates old style object, use class foo(object)",
277 "class foo() creates old style object, use class foo(object)",
278 r'#.*old-style'),
278 r'#.*old-style'),
279 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
279 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
280 if k not in ('print', 'exec')),
280 if k not in ('print', 'exec')),
281 "Python keyword is not a function"),
281 "Python keyword is not a function"),
282 (r',]', "unneeded trailing ',' in list"),
282 (r',]', "unneeded trailing ',' in list"),
283 # (r'class\s[A-Z][^\(]*\((?!Exception)',
283 # (r'class\s[A-Z][^\(]*\((?!Exception)',
284 # "don't capitalize non-exception classes"),
284 # "don't capitalize non-exception classes"),
285 # (r'in range\(', "use xrange"),
285 # (r'in range\(', "use xrange"),
286 # (r'^\s*print\s+', "avoid using print in core and extensions"),
286 # (r'^\s*print\s+', "avoid using print in core and extensions"),
287 (r'[\x80-\xff]', "non-ASCII character literal"),
287 (r'[\x80-\xff]', "non-ASCII character literal"),
288 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
288 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
289 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
289 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
290 "gratuitous whitespace after Python keyword"),
290 "gratuitous whitespace after Python keyword"),
291 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
291 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
292 # (r'\s\s=', "gratuitous whitespace before ="),
292 # (r'\s\s=', "gratuitous whitespace before ="),
293 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
293 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
294 "missing whitespace around operator"),
294 "missing whitespace around operator"),
295 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
295 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
296 "missing whitespace around operator"),
296 "missing whitespace around operator"),
297 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
297 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
298 "missing whitespace around operator"),
298 "missing whitespace around operator"),
299 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
299 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
300 "wrong whitespace around ="),
300 "wrong whitespace around ="),
301 (r'\([^()]*( =[^=]|[^<>!=]= )',
301 (r'\([^()]*( =[^=]|[^<>!=]= )',
302 "no whitespace around = for named parameters"),
302 "no whitespace around = for named parameters"),
303 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
303 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
304 "don't use old-style two-argument raise, use Exception(message)"),
304 "don't use old-style two-argument raise, use Exception(message)"),
305 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
305 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
306 (r' [=!]=\s+(True|False|None)',
306 (r' [=!]=\s+(True|False|None)',
307 "comparison with singleton, use 'is' or 'is not' instead"),
307 "comparison with singleton, use 'is' or 'is not' instead"),
308 (r'^\s*(while|if) [01]:',
308 (r'^\s*(while|if) [01]:',
309 "use True/False for constant Boolean expression"),
309 "use True/False for constant Boolean expression"),
310 (r'^\s*if False(:| +and)', 'Remove code instead of using `if False`'),
310 (r'^\s*if False(:| +and)', 'Remove code instead of using `if False`'),
311 (r'(?:(?<!def)\s+|\()hasattr\(',
311 (r'(?:(?<!def)\s+|\()hasattr\(',
312 'hasattr(foo, bar) is broken on py2, use util.safehasattr(foo, bar) '
312 'hasattr(foo, bar) is broken on py2, use util.safehasattr(foo, bar) '
313 'instead', r'#.*hasattr-py3-only'),
313 'instead', r'#.*hasattr-py3-only'),
314 (r'opener\([^)]*\).read\(',
314 (r'opener\([^)]*\).read\(',
315 "use opener.read() instead"),
315 "use opener.read() instead"),
316 (r'opener\([^)]*\).write\(',
316 (r'opener\([^)]*\).write\(',
317 "use opener.write() instead"),
317 "use opener.write() instead"),
318 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
318 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
319 (r'\.debug\(\_', "don't mark debug messages for translation"),
319 (r'\.debug\(\_', "don't mark debug messages for translation"),
320 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
320 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
321 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
321 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
322 (r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
322 (r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
323 'legacy exception syntax; use "as" instead of ","'),
323 'legacy exception syntax; use "as" instead of ","'),
324 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
324 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
325 (r'\bdef\s+__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
325 (r'\bdef\s+__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
326 (r'os\.path\.join\(.*, *(""|\'\')\)',
326 (r'os\.path\.join\(.*, *(""|\'\')\)',
327 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
327 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
328 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
328 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
329 # XXX only catch mutable arguments on the first line of the definition
329 # XXX only catch mutable arguments on the first line of the definition
330 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
330 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
331 (r'\butil\.Abort\b', "directly use error.Abort"),
331 (r'\butil\.Abort\b', "directly use error.Abort"),
332 (r'^@(\w*\.)?cachefunc', "module-level @cachefunc is risky, please avoid"),
332 (r'^@(\w*\.)?cachefunc', "module-level @cachefunc is risky, please avoid"),
333 (r'^import Queue', "don't use Queue, use pycompat.queue.Queue + "
333 (r'^import Queue', "don't use Queue, use pycompat.queue.Queue + "
334 "pycompat.queue.Empty"),
334 "pycompat.queue.Empty"),
335 (r'^import cStringIO', "don't use cStringIO.StringIO, use util.stringio"),
335 (r'^import cStringIO', "don't use cStringIO.StringIO, use util.stringio"),
336 (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
336 (r'^import urllib', "don't use urllib, use util.urlreq/util.urlerr"),
337 (r'^import SocketServer', "don't use SockerServer, use util.socketserver"),
337 (r'^import SocketServer', "don't use SockerServer, use util.socketserver"),
338 (r'^import urlparse', "don't use urlparse, use util.urlreq"),
338 (r'^import urlparse', "don't use urlparse, use util.urlreq"),
339 (r'^import xmlrpclib', "don't use xmlrpclib, use util.xmlrpclib"),
339 (r'^import xmlrpclib', "don't use xmlrpclib, use util.xmlrpclib"),
340 (r'^import cPickle', "don't use cPickle, use util.pickle"),
340 (r'^import cPickle', "don't use cPickle, use util.pickle"),
341 (r'^import pickle', "don't use pickle, use util.pickle"),
341 (r'^import pickle', "don't use pickle, use util.pickle"),
342 (r'^import httplib', "don't use httplib, use util.httplib"),
342 (r'^import httplib', "don't use httplib, use util.httplib"),
343 (r'^import BaseHTTPServer', "use util.httpserver instead"),
343 (r'^import BaseHTTPServer', "use util.httpserver instead"),
344 (r'^(from|import) mercurial\.(cext|pure|cffi)',
344 (r'^(from|import) mercurial\.(cext|pure|cffi)',
345 "use mercurial.policy.importmod instead"),
345 "use mercurial.policy.importmod instead"),
346 (r'\.next\(\)', "don't use .next(), use next(...)"),
346 (r'\.next\(\)', "don't use .next(), use next(...)"),
347 (r'([a-z]*).revision\(\1\.node\(',
347 (r'([a-z]*).revision\(\1\.node\(',
348 "don't convert rev to node before passing to revision(nodeorrev)"),
348 "don't convert rev to node before passing to revision(nodeorrev)"),
349 (r'platform\.system\(\)', "don't use platform.system(), use pycompat"),
349 (r'platform\.system\(\)', "don't use platform.system(), use pycompat"),
350
350
351 ],
351 ],
352 # warnings
352 # warnings
353 [
353 [
354 ]
354 ]
355 ]
355 ]
356
356
357 # patterns to check normal *.py files
357 # patterns to check normal *.py files
358 pypats = [
358 pypats = [
359 [
359 [
360 # Ideally, these should be placed in "commonpypats" for
360 # Ideally, these should be placed in "commonpypats" for
361 # consistency of coding rules in Mercurial source tree.
361 # consistency of coding rules in Mercurial source tree.
362 # But on the other hand, these are not so seriously required for
362 # But on the other hand, these are not so seriously required for
363 # python code fragments embedded in test scripts. Fixing test
363 # python code fragments embedded in test scripts. Fixing test
364 # scripts for these patterns requires many changes, and has less
364 # scripts for these patterns requires many changes, and has less
365 # profit than effort.
365 # profit than effort.
366 (r'.{81}', "line too long"),
366 (r'.{81}', "line too long"),
367 (r'raise Exception', "don't raise generic exceptions"),
367 (r'raise Exception', "don't raise generic exceptions"),
368 (r'[\s\(](open|file)\([^)]*\)\.read\(',
368 (r'[\s\(](open|file)\([^)]*\)\.read\(',
369 "use util.readfile() instead"),
369 "use util.readfile() instead"),
370 (r'[\s\(](open|file)\([^)]*\)\.write\(',
370 (r'[\s\(](open|file)\([^)]*\)\.write\(',
371 "use util.writefile() instead"),
371 "use util.writefile() instead"),
372 (r'^[\s\(]*(open(er)?|file)\([^)]*\)(?!\.close\(\))',
372 (r'^[\s\(]*(open(er)?|file)\([^)]*\)(?!\.close\(\))',
373 "always assign an opened file to a variable, and close it afterwards"),
373 "always assign an opened file to a variable, and close it afterwards"),
374 (r'[\s\(](open|file)\([^)]*\)\.(?!close\(\))',
374 (r'[\s\(](open|file)\([^)]*\)\.(?!close\(\))',
375 "always assign an opened file to a variable, and close it afterwards"),
375 "always assign an opened file to a variable, and close it afterwards"),
376 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
376 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
377 (r'^import atexit', "don't use atexit, use ui.atexit"),
377 (r'^import atexit', "don't use atexit, use ui.atexit"),
378
378
379 # rules depending on implementation of repquote()
379 # rules depending on implementation of repquote()
380 (r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
380 (r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
381 'string join across lines with no space'),
381 'string join across lines with no space'),
382 (r'''(?x)ui\.(status|progress|write|note|warn)\(
382 (r'''(?x)ui\.(status|progress|write|note|warn)\(
383 [ \t\n#]*
383 [ \t\n#]*
384 (?# any strings/comments might precede a string, which
384 (?# any strings/comments might precede a string, which
385 # contains translatable message)
385 # contains translatable message)
386 ((['"]|\'\'\'|""")[ \npq%bAPMxno]*(['"]|\'\'\'|""")[ \t\n#]+)*
386 ((['"]|\'\'\'|""")[ \npq%bAPMxno]*(['"]|\'\'\'|""")[ \t\n#]+)*
387 (?# sequence consisting of below might precede translatable message
387 (?# sequence consisting of below might precede translatable message
388 # - formatting string: "% 10s", "%05d", "% -3.2f", "%*s", "%%" ...
388 # - formatting string: "% 10s", "%05d", "% -3.2f", "%*s", "%%" ...
389 # - escaped character: "\\", "\n", "\0" ...
389 # - escaped character: "\\", "\n", "\0" ...
390 # - character other than '%', 'b' as '\', and 'x' as alphabet)
390 # - character other than '%', 'b' as '\', and 'x' as alphabet)
391 (['"]|\'\'\'|""")
391 (['"]|\'\'\'|""")
392 ((%([ n]?[PM]?([np]+|A))?x)|%%|b[bnx]|[ \nnpqAPMo])*x
392 ((%([ n]?[PM]?([np]+|A))?x)|%%|b[bnx]|[ \nnpqAPMo])*x
393 (?# this regexp can't use [^...] style,
393 (?# this regexp can't use [^...] style,
394 # because _preparepats forcibly adds "\n" into [^...],
394 # because _preparepats forcibly adds "\n" into [^...],
395 # even though this regexp wants match it against "\n")''',
395 # even though this regexp wants match it against "\n")''',
396 "missing _() in ui message (use () to hide false-positives)"),
396 "missing _() in ui message (use () to hide false-positives)"),
397 ] + commonpypats[0],
397 ] + commonpypats[0],
398 # warnings
398 # warnings
399 [
399 [
400 # rules depending on implementation of repquote()
400 # rules depending on implementation of repquote()
401 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
401 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
402 ] + commonpypats[1]
402 ] + commonpypats[1]
403 ]
403 ]
404
404
405 # common filters to convert *.py
405 # common filters to convert *.py
406 commonpyfilters = [
406 commonpyfilters = [
407 (r"""(?msx)(?P<comment>\#.*?$)|
407 (r"""(?msx)(?P<comment>\#.*?$)|
408 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
408 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
409 (?P<text>(([^\\]|\\.)*?))
409 (?P<text>(([^\\]|\\.)*?))
410 (?P=quote))""", reppython),
410 (?P=quote))""", reppython),
411 ]
411 ]
412
412
413 # filters to convert normal *.py files
413 # filters to convert normal *.py files
414 pyfilters = [
414 pyfilters = [
415 ] + commonpyfilters
415 ] + commonpyfilters
416
416
417 # non-filter patterns
417 # non-filter patterns
418 pynfpats = [
418 pynfpats = [
419 [
419 [
420 (r'pycompat\.osname\s*[=!]=\s*[\'"]nt[\'"]', "use pycompat.iswindows"),
420 (r'pycompat\.osname\s*[=!]=\s*[\'"]nt[\'"]', "use pycompat.iswindows"),
421 (r'pycompat\.osname\s*[=!]=\s*[\'"]posix[\'"]', "use pycompat.isposix"),
421 (r'pycompat\.osname\s*[=!]=\s*[\'"]posix[\'"]', "use pycompat.isposix"),
422 (r'pycompat\.sysplatform\s*[!=]=\s*[\'"]darwin[\'"]',
422 (r'pycompat\.sysplatform\s*[!=]=\s*[\'"]darwin[\'"]',
423 "use pycompat.isdarwin"),
423 "use pycompat.isdarwin"),
424 ],
424 ],
425 # warnings
425 # warnings
426 [],
426 [],
427 ]
427 ]
428
428
429 # extension non-filter patterns
429 # extension non-filter patterns
430 pyextnfpats = [
430 pyextnfpats = [
431 [(r'^"""\n?[A-Z]', "don't capitalize docstring title")],
431 [(r'^"""\n?[A-Z]', "don't capitalize docstring title")],
432 # warnings
432 # warnings
433 [],
433 [],
434 ]
434 ]
435
435
436 txtfilters = []
436 txtfilters = []
437
437
438 txtpats = [
438 txtpats = [
439 [
439 [
440 (r'\s$', 'trailing whitespace'),
440 (r'\s$', 'trailing whitespace'),
441 ('.. note::[ \n][^\n]', 'add two newlines after note::')
441 ('.. note::[ \n][^\n]', 'add two newlines after note::')
442 ],
442 ],
443 []
443 []
444 ]
444 ]
445
445
446 cpats = [
446 cpats = [
447 [
447 [
448 (r'//', "don't use //-style comments"),
448 (r'//', "don't use //-style comments"),
449 (r'\S\t', "don't use tabs except for indent"),
449 (r'\S\t', "don't use tabs except for indent"),
450 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
450 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
451 (r'.{81}', "line too long"),
451 (r'.{81}', "line too long"),
452 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
452 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
453 (r'return\(', "return is not a function"),
453 (r'return\(', "return is not a function"),
454 (r' ;', "no space before ;"),
454 (r' ;', "no space before ;"),
455 (r'[^;] \)', "no space before )"),
455 (r'[^;] \)', "no space before )"),
456 (r'[)][{]', "space between ) and {"),
456 (r'[)][{]', "space between ) and {"),
457 (r'\w+\* \w+', "use int *foo, not int* foo"),
457 (r'\w+\* \w+', "use int *foo, not int* foo"),
458 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
458 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
459 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
459 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
460 (r'\w,\w', "missing whitespace after ,"),
460 (r'\w,\w', "missing whitespace after ,"),
461 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
461 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
462 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
462 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
463 (r'^#\s+\w', "use #foo, not # foo"),
463 (r'^#\s+\w', "use #foo, not # foo"),
464 (r'[^\n]\Z', "no trailing newline"),
464 (r'[^\n]\Z', "no trailing newline"),
465 (r'^\s*#import\b', "use only #include in standard C code"),
465 (r'^\s*#import\b', "use only #include in standard C code"),
466 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
466 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
467 (r'strcat\(', "don't use strcat"),
467 (r'strcat\(', "don't use strcat"),
468
468
469 # rules depending on implementation of repquote()
469 # rules depending on implementation of repquote()
470 ],
470 ],
471 # warnings
471 # warnings
472 [
472 [
473 # rules depending on implementation of repquote()
473 # rules depending on implementation of repquote()
474 ]
474 ]
475 ]
475 ]
476
476
477 cfilters = [
477 cfilters = [
478 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
478 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
479 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
479 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
480 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
480 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
481 (r'(\()([^)]+\))', repcallspaces),
481 (r'(\()([^)]+\))', repcallspaces),
482 ]
482 ]
483
483
484 inutilpats = [
484 inutilpats = [
485 [
485 [
486 (r'\bui\.', "don't use ui in util"),
486 (r'\bui\.', "don't use ui in util"),
487 ],
487 ],
488 # warnings
488 # warnings
489 []
489 []
490 ]
490 ]
491
491
492 inrevlogpats = [
492 inrevlogpats = [
493 [
493 [
494 (r'\brepo\.', "don't use repo in revlog"),
494 (r'\brepo\.', "don't use repo in revlog"),
495 ],
495 ],
496 # warnings
496 # warnings
497 []
497 []
498 ]
498 ]
499
499
500 webtemplatefilters = []
500 webtemplatefilters = []
501
501
502 webtemplatepats = [
502 webtemplatepats = [
503 [],
503 [],
504 [
504 [
505 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
505 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
506 'follow desc keyword with either firstline or websub'),
506 'follow desc keyword with either firstline or websub'),
507 ]
507 ]
508 ]
508 ]
509
509
510 allfilesfilters = []
510 allfilesfilters = []
511
511
512 allfilespats = [
512 allfilespats = [
513 [
513 [
514 (r'(http|https)://[a-zA-Z0-9./]*selenic.com/',
514 (r'(http|https)://[a-zA-Z0-9./]*selenic.com/',
515 'use mercurial-scm.org domain URL'),
515 'use mercurial-scm.org domain URL'),
516 (r'mercurial@selenic\.com',
516 (r'mercurial@selenic\.com',
517 'use mercurial-scm.org domain for mercurial ML address'),
517 'use mercurial-scm.org domain for mercurial ML address'),
518 (r'mercurial-devel@selenic\.com',
518 (r'mercurial-devel@selenic\.com',
519 'use mercurial-scm.org domain for mercurial-devel ML address'),
519 'use mercurial-scm.org domain for mercurial-devel ML address'),
520 ],
520 ],
521 # warnings
521 # warnings
522 [],
522 [],
523 ]
523 ]
524
524
525 py3pats = [
525 py3pats = [
526 [
526 [
527 (r'os\.environ', "use encoding.environ instead (py3)", r'#.*re-exports'),
527 (r'os\.environ', "use encoding.environ instead (py3)", r'#.*re-exports'),
528 (r'os\.name', "use pycompat.osname instead (py3)"),
528 (r'os\.name', "use pycompat.osname instead (py3)"),
529 (r'os\.getcwd', "use encoding.getcwd instead (py3)", r'#.*re-exports'),
529 (r'os\.getcwd', "use encoding.getcwd instead (py3)", r'#.*re-exports'),
530 (r'os\.sep', "use pycompat.ossep instead (py3)"),
530 (r'os\.sep', "use pycompat.ossep instead (py3)"),
531 (r'os\.pathsep', "use pycompat.ospathsep instead (py3)"),
531 (r'os\.pathsep', "use pycompat.ospathsep instead (py3)"),
532 (r'os\.altsep', "use pycompat.osaltsep instead (py3)"),
532 (r'os\.altsep', "use pycompat.osaltsep instead (py3)"),
533 (r'sys\.platform', "use pycompat.sysplatform instead (py3)"),
533 (r'sys\.platform', "use pycompat.sysplatform instead (py3)"),
534 (r'getopt\.getopt', "use pycompat.getoptb instead (py3)"),
534 (r'getopt\.getopt', "use pycompat.getoptb instead (py3)"),
535 (r'os\.getenv', "use encoding.environ.get instead"),
535 (r'os\.getenv', "use encoding.environ.get instead"),
536 (r'os\.setenv', "modifying the environ dict is not preferred"),
536 (r'os\.setenv', "modifying the environ dict is not preferred"),
537 (r'(?<!pycompat\.)xrange', "use pycompat.xrange instead (py3)"),
537 (r'(?<!pycompat\.)xrange', "use pycompat.xrange instead (py3)"),
538 ],
538 ],
539 # warnings
539 # warnings
540 [],
540 [],
541 ]
541 ]
542
542
543 checks = [
543 checks = [
544 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
544 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
545 ('python', r'.*\.(py|cgi)$', r'^#!.*python', [], pynfpats),
545 ('python', r'.*\.(py|cgi)$', r'^#!.*python', [], pynfpats),
546 ('python', r'.*hgext.*\.py$', '', [], pyextnfpats),
546 ('python', r'.*hgext.*\.py$', '', [], pyextnfpats),
547 ('python 3', r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
547 ('python 3', r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
548 '', pyfilters, py3pats),
548 '', pyfilters, py3pats),
549 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
549 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
550 ('c', r'.*\.[ch]$', '', cfilters, cpats),
550 ('c', r'.*\.[ch]$', '', cfilters, cpats),
551 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
551 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
552 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
552 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
553 pyfilters, inrevlogpats),
553 pyfilters, inrevlogpats),
554 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
554 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
555 inutilpats),
555 inutilpats),
556 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
556 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
557 ('web template', r'mercurial/templates/.*\.tmpl', '',
557 ('web template', r'mercurial/templates/.*\.tmpl', '',
558 webtemplatefilters, webtemplatepats),
558 webtemplatefilters, webtemplatepats),
559 ('all except for .po', r'.*(?<!\.po)$', '',
559 ('all except for .po', r'.*(?<!\.po)$', '',
560 allfilesfilters, allfilespats),
560 allfilesfilters, allfilespats),
561 ]
561 ]
562
562
563 def _preparepats():
563 def _preparepats():
564 def preparefailandwarn(failandwarn):
564 def preparefailandwarn(failandwarn):
565 for pats in failandwarn:
565 for pats in failandwarn:
566 for i, pseq in enumerate(pats):
566 for i, pseq in enumerate(pats):
567 # fix-up regexes for multi-line searches
567 # fix-up regexes for multi-line searches
568 p = pseq[0]
568 p = pseq[0]
569 # \s doesn't match \n (done in two steps)
569 # \s doesn't match \n (done in two steps)
570 # first, we replace \s that appears in a set already
570 # first, we replace \s that appears in a set already
571 p = re.sub(r'\[\\s', r'[ \\t', p)
571 p = re.sub(r'\[\\s', r'[ \\t', p)
572 # now we replace other \s instances.
572 # now we replace other \s instances.
573 p = re.sub(r'(?<!(\\|\[))\\s', r'[ \\t]', p)
573 p = re.sub(r'(?<!(\\|\[))\\s', r'[ \\t]', p)
574 # [^...] doesn't match newline
574 # [^...] doesn't match newline
575 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
575 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
576
576
577 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
577 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
578
578
579 def preparefilters(filters):
579 def preparefilters(filters):
580 for i, flt in enumerate(filters):
580 for i, flt in enumerate(filters):
581 filters[i] = re.compile(flt[0]), flt[1]
581 filters[i] = re.compile(flt[0]), flt[1]
582
582
583 for cs in (checks,):
583 for cs in (checks,):
584 for c in cs:
584 for c in cs:
585 failandwarn = c[-1]
585 failandwarn = c[-1]
586 preparefailandwarn(failandwarn)
586 preparefailandwarn(failandwarn)
587
587
588 filters = c[-2]
588 filters = c[-2]
589 preparefilters(filters)
589 preparefilters(filters)
590
590
591 class norepeatlogger(object):
591 class norepeatlogger(object):
592 def __init__(self):
592 def __init__(self):
593 self._lastseen = None
593 self._lastseen = None
594
594
595 def log(self, fname, lineno, line, msg, blame):
595 def log(self, fname, lineno, line, msg, blame):
596 """print error related a to given line of a given file.
596 """print error related a to given line of a given file.
597
597
598 The faulty line will also be printed but only once in the case
598 The faulty line will also be printed but only once in the case
599 of multiple errors.
599 of multiple errors.
600
600
601 :fname: filename
601 :fname: filename
602 :lineno: line number
602 :lineno: line number
603 :line: actual content of the line
603 :line: actual content of the line
604 :msg: error message
604 :msg: error message
605 """
605 """
606 msgid = fname, lineno, line
606 msgid = fname, lineno, line
607 if msgid != self._lastseen:
607 if msgid != self._lastseen:
608 if blame:
608 if blame:
609 print("%s:%d (%s):" % (fname, lineno, blame))
609 print("%s:%d (%s):" % (fname, lineno, blame))
610 else:
610 else:
611 print("%s:%d:" % (fname, lineno))
611 print("%s:%d:" % (fname, lineno))
612 print(" > %s" % line)
612 print(" > %s" % line)
613 self._lastseen = msgid
613 self._lastseen = msgid
614 print(" " + msg)
614 print(" " + msg)
615
615
616 _defaultlogger = norepeatlogger()
616 _defaultlogger = norepeatlogger()
617
617
618 def getblame(f):
618 def getblame(f):
619 lines = []
619 lines = []
620 for l in os.popen('hg annotate -un %s' % f):
620 for l in os.popen('hg annotate -un %s' % f):
621 start, line = l.split(':', 1)
621 start, line = l.split(':', 1)
622 user, rev = start.split()
622 user, rev = start.split()
623 lines.append((line[1:-1], user, rev))
623 lines.append((line[1:-1], user, rev))
624 return lines
624 return lines
625
625
626 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
626 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
627 blame=False, debug=False, lineno=True):
627 blame=False, debug=False, lineno=True):
628 """checks style and portability of a given file
628 """checks style and portability of a given file
629
629
630 :f: filepath
630 :f: filepath
631 :logfunc: function used to report error
631 :logfunc: function used to report error
632 logfunc(filename, linenumber, linecontent, errormessage)
632 logfunc(filename, linenumber, linecontent, errormessage)
633 :maxerr: number of error to display before aborting.
633 :maxerr: number of error to display before aborting.
634 Set to false (default) to report all errors
634 Set to false (default) to report all errors
635
635
636 return True if no error is found, False otherwise.
636 return True if no error is found, False otherwise.
637 """
637 """
638 result = True
638 result = True
639
639
640 try:
640 try:
641 with opentext(f) as fp:
641 with opentext(f) as fp:
642 try:
642 try:
643 pre = fp.read()
643 pre = fp.read()
644 except UnicodeDecodeError as e:
644 except UnicodeDecodeError as e:
645 print("%s while reading %s" % (e, f))
645 print("%s while reading %s" % (e, f))
646 return result
646 return result
647 except IOError as e:
647 except IOError as e:
648 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
648 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
649 return result
649 return result
650
650
651 # context information shared while single checkfile() invocation
651 # context information shared while single checkfile() invocation
652 context = {'blamecache': None}
652 context = {'blamecache': None}
653
653
654 for name, match, magic, filters, pats in checks:
654 for name, match, magic, filters, pats in checks:
655 if debug:
655 if debug:
656 print(name, f)
656 print(name, f)
657 if not (re.match(match, f) or (magic and re.search(magic, pre))):
657 if not (re.match(match, f) or (magic and re.search(magic, pre))):
658 if debug:
658 if debug:
659 print("Skipping %s for %s it doesn't match %s" % (
659 print("Skipping %s for %s it doesn't match %s" % (
660 name, match, f))
660 name, match, f))
661 continue
661 continue
662 if "no-" "check-code" in pre:
662 if "no-" "check-code" in pre:
663 # If you're looking at this line, it's because a file has:
663 # If you're looking at this line, it's because a file has:
664 # no- check- code
664 # no- check- code
665 # but the reason to output skipping is to make life for
665 # but the reason to output skipping is to make life for
666 # tests easier. So, instead of writing it with a normal
666 # tests easier. So, instead of writing it with a normal
667 # spelling, we write it with the expected spelling from
667 # spelling, we write it with the expected spelling from
668 # tests/test-check-code.t
668 # tests/test-check-code.t
669 print("Skipping %s it has no-che?k-code (glob)" % f)
669 print("Skipping %s it has no-che?k-code (glob)" % f)
670 return "Skip" # skip checking this file
670 return "Skip" # skip checking this file
671
671
672 fc = _checkfiledata(name, f, pre, filters, pats, context,
672 fc = _checkfiledata(name, f, pre, filters, pats, context,
673 logfunc, maxerr, warnings, blame, debug, lineno)
673 logfunc, maxerr, warnings, blame, debug, lineno)
674 if fc:
674 if fc:
675 result = False
675 result = False
676
676
677 return result
677 return result
678
678
679 def _checkfiledata(name, f, filedata, filters, pats, context,
679 def _checkfiledata(name, f, filedata, filters, pats, context,
680 logfunc, maxerr, warnings, blame, debug, lineno):
680 logfunc, maxerr, warnings, blame, debug, lineno,
681 offset=None):
681 """Execute actual error check for file data
682 """Execute actual error check for file data
682
683
683 :name: of the checking category
684 :name: of the checking category
684 :f: filepath
685 :f: filepath
685 :filedata: content of a file
686 :filedata: content of a file
686 :filters: to be applied before checking
687 :filters: to be applied before checking
687 :pats: to detect errors
688 :pats: to detect errors
688 :context: a dict of information shared while single checkfile() invocation
689 :context: a dict of information shared while single checkfile() invocation
689 Valid keys: 'blamecache'.
690 Valid keys: 'blamecache'.
690 :logfunc: function used to report error
691 :logfunc: function used to report error
691 logfunc(filename, linenumber, linecontent, errormessage)
692 logfunc(filename, linenumber, linecontent, errormessage)
692 :maxerr: number of error to display before aborting, or False to
693 :maxerr: number of error to display before aborting, or False to
693 report all errors
694 report all errors
694 :warnings: whether warning level checks should be applied
695 :warnings: whether warning level checks should be applied
695 :blame: whether blame information should be displayed at error reporting
696 :blame: whether blame information should be displayed at error reporting
696 :debug: whether debug information should be displayed
697 :debug: whether debug information should be displayed
697 :lineno: whether lineno should be displayed at error reporting
698 :lineno: whether lineno should be displayed at error reporting
699 :offset: line number offset of 'filedata' in 'f' for checking
700 an embedded code fragment, or None (offset=0 is different
701 from offset=None)
698
702
699 returns number of detected errors.
703 returns number of detected errors.
700 """
704 """
701 blamecache = context['blamecache']
705 blamecache = context['blamecache']
706 if offset is None:
707 lineoffset = 0
708 else:
709 lineoffset = offset
702
710
703 fc = 0
711 fc = 0
704 pre = post = filedata
712 pre = post = filedata
705
713
706 if True: # TODO: get rid of this redundant 'if' block
714 if True: # TODO: get rid of this redundant 'if' block
707 for p, r in filters:
715 for p, r in filters:
708 post = re.sub(p, r, post)
716 post = re.sub(p, r, post)
709 nerrs = len(pats[0]) # nerr elements are errors
717 nerrs = len(pats[0]) # nerr elements are errors
710 if warnings:
718 if warnings:
711 pats = pats[0] + pats[1]
719 pats = pats[0] + pats[1]
712 else:
720 else:
713 pats = pats[0]
721 pats = pats[0]
714 # print post # uncomment to show filtered version
722 # print post # uncomment to show filtered version
715
723
716 if debug:
724 if debug:
717 print("Checking %s for %s" % (name, f))
725 print("Checking %s for %s" % (name, f))
718
726
719 prelines = None
727 prelines = None
720 errors = []
728 errors = []
721 for i, pat in enumerate(pats):
729 for i, pat in enumerate(pats):
722 if len(pat) == 3:
730 if len(pat) == 3:
723 p, msg, ignore = pat
731 p, msg, ignore = pat
724 else:
732 else:
725 p, msg = pat
733 p, msg = pat
726 ignore = None
734 ignore = None
727 if i >= nerrs:
735 if i >= nerrs:
728 msg = "warning: " + msg
736 msg = "warning: " + msg
729
737
730 pos = 0
738 pos = 0
731 n = 0
739 n = 0
732 for m in p.finditer(post):
740 for m in p.finditer(post):
733 if prelines is None:
741 if prelines is None:
734 prelines = pre.splitlines()
742 prelines = pre.splitlines()
735 postlines = post.splitlines(True)
743 postlines = post.splitlines(True)
736
744
737 start = m.start()
745 start = m.start()
738 while n < len(postlines):
746 while n < len(postlines):
739 step = len(postlines[n])
747 step = len(postlines[n])
740 if pos + step > start:
748 if pos + step > start:
741 break
749 break
742 pos += step
750 pos += step
743 n += 1
751 n += 1
744 l = prelines[n]
752 l = prelines[n]
745
753
746 if ignore and re.search(ignore, l, re.MULTILINE):
754 if ignore and re.search(ignore, l, re.MULTILINE):
747 if debug:
755 if debug:
748 print("Skipping %s for %s:%s (ignore pattern)" % (
756 print("Skipping %s for %s:%s (ignore pattern)" % (
749 name, f, n))
757 name, f, (n + lineoffset)))
750 continue
758 continue
751 bd = ""
759 bd = ""
752 if blame:
760 if blame:
753 bd = 'working directory'
761 bd = 'working directory'
754 if blamecache is None:
762 if blamecache is None:
755 blamecache = getblame(f)
763 blamecache = getblame(f)
756 context['blamecache'] = blamecache
764 context['blamecache'] = blamecache
757 if n < len(blamecache):
765 if (n + lineoffset) < len(blamecache):
758 bl, bu, br = blamecache[n]
766 bl, bu, br = blamecache[(n + lineoffset)]
759 if bl == l:
767 if offset is None and bl == l:
760 bd = '%s@%s' % (bu, br)
768 bd = '%s@%s' % (bu, br)
769 elif offset is not None and bl.endswith(l):
770 # "offset is not None" means "checking
771 # embedded code fragment". In this case,
772 # "l" does not have information about the
773 # beginning of an *original* line in the
774 # file (e.g. ' > ').
775 # Therefore, use "str.endswith()", and
776 # show "maybe" for a little loose
777 # examination.
778 bd = '%s@%s, maybe' % (bu, br)
761
779
762 errors.append((f, lineno and n + 1, l, msg, bd))
780 errors.append((f, lineno and (n + lineoffset + 1), l, msg, bd))
763
781
764 errors.sort()
782 errors.sort()
765 for e in errors:
783 for e in errors:
766 logfunc(*e)
784 logfunc(*e)
767 fc += 1
785 fc += 1
768 if maxerr and fc >= maxerr:
786 if maxerr and fc >= maxerr:
769 print(" (too many errors, giving up)")
787 print(" (too many errors, giving up)")
770 break
788 break
771
789
772 return fc
790 return fc
773
791
774 def main():
792 def main():
775 parser = optparse.OptionParser("%prog [options] [files | -]")
793 parser = optparse.OptionParser("%prog [options] [files | -]")
776 parser.add_option("-w", "--warnings", action="store_true",
794 parser.add_option("-w", "--warnings", action="store_true",
777 help="include warning-level checks")
795 help="include warning-level checks")
778 parser.add_option("-p", "--per-file", type="int",
796 parser.add_option("-p", "--per-file", type="int",
779 help="max warnings per file")
797 help="max warnings per file")
780 parser.add_option("-b", "--blame", action="store_true",
798 parser.add_option("-b", "--blame", action="store_true",
781 help="use annotate to generate blame info")
799 help="use annotate to generate blame info")
782 parser.add_option("", "--debug", action="store_true",
800 parser.add_option("", "--debug", action="store_true",
783 help="show debug information")
801 help="show debug information")
784 parser.add_option("", "--nolineno", action="store_false",
802 parser.add_option("", "--nolineno", action="store_false",
785 dest='lineno', help="don't show line numbers")
803 dest='lineno', help="don't show line numbers")
786
804
787 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
805 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
788 lineno=True)
806 lineno=True)
789 (options, args) = parser.parse_args()
807 (options, args) = parser.parse_args()
790
808
791 if len(args) == 0:
809 if len(args) == 0:
792 check = glob.glob("*")
810 check = glob.glob("*")
793 elif args == ['-']:
811 elif args == ['-']:
794 # read file list from stdin
812 # read file list from stdin
795 check = sys.stdin.read().splitlines()
813 check = sys.stdin.read().splitlines()
796 else:
814 else:
797 check = args
815 check = args
798
816
799 _preparepats()
817 _preparepats()
800
818
801 ret = 0
819 ret = 0
802 for f in check:
820 for f in check:
803 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
821 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
804 blame=options.blame, debug=options.debug,
822 blame=options.blame, debug=options.debug,
805 lineno=options.lineno):
823 lineno=options.lineno):
806 ret = 1
824 ret = 1
807 return ret
825 return ret
808
826
809 if __name__ == "__main__":
827 if __name__ == "__main__":
810 sys.exit(main())
828 sys.exit(main())
General Comments 0
You need to be logged in to leave comments. Login now