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