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