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