##// END OF EJS Templates
check-code: reintroduce str.format() ban for 3.x porting...
Matt Mackall -
r25212:a39c35e8 default
parent child Browse files
Show More
@@ -1,568 +1,569 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, # no-py24)
17 (you can append a short comment and match this, like: #re-raises, # no-py24)
18 * change the pattern to a warning and list the exception in test-check-code-hg
18 * change the pattern to a warning and list the exception in test-check-code-hg
19 * ONLY use no--check-code for skipping entire files from external sources
19 * ONLY use no--check-code for skipping entire files from external sources
20 """
20 """
21
21
22 import re, glob, os, sys
22 import re, glob, os, sys
23 import keyword
23 import keyword
24 import optparse
24 import optparse
25 try:
25 try:
26 import re2
26 import re2
27 except ImportError:
27 except ImportError:
28 re2 = None
28 re2 = None
29
29
30 def compilere(pat, multiline=False):
30 def compilere(pat, multiline=False):
31 if multiline:
31 if multiline:
32 pat = '(?m)' + pat
32 pat = '(?m)' + pat
33 if re2:
33 if re2:
34 try:
34 try:
35 return re2.compile(pat)
35 return re2.compile(pat)
36 except re2.error:
36 except re2.error:
37 pass
37 pass
38 return re.compile(pat)
38 return re.compile(pat)
39
39
40 def repquote(m):
40 def repquote(m):
41 fromc = '.:'
41 fromc = '.:'
42 tochr = 'pq'
42 tochr = 'pq'
43 def encodechr(i):
43 def encodechr(i):
44 if i > 255:
44 if i > 255:
45 return 'u'
45 return 'u'
46 c = chr(i)
46 c = chr(i)
47 if c in ' \n':
47 if c in ' \n':
48 return c
48 return c
49 if c.isalpha():
49 if c.isalpha():
50 return 'x'
50 return 'x'
51 if c.isdigit():
51 if c.isdigit():
52 return 'n'
52 return 'n'
53 try:
53 try:
54 return tochr[fromc.find(c)]
54 return tochr[fromc.find(c)]
55 except (ValueError, IndexError):
55 except (ValueError, IndexError):
56 return 'o'
56 return 'o'
57 t = m.group('text')
57 t = m.group('text')
58 tt = ''.join(encodechr(i) for i in xrange(256))
58 tt = ''.join(encodechr(i) for i in xrange(256))
59 t = t.translate(tt)
59 t = t.translate(tt)
60 return m.group('quote') + t + m.group('quote')
60 return m.group('quote') + t + m.group('quote')
61
61
62 def reppython(m):
62 def reppython(m):
63 comment = m.group('comment')
63 comment = m.group('comment')
64 if comment:
64 if comment:
65 l = len(comment.rstrip())
65 l = len(comment.rstrip())
66 return "#" * l + comment[l:]
66 return "#" * l + comment[l:]
67 return repquote(m)
67 return repquote(m)
68
68
69 def repcomment(m):
69 def repcomment(m):
70 return m.group(1) + "#" * len(m.group(2))
70 return m.group(1) + "#" * len(m.group(2))
71
71
72 def repccomment(m):
72 def repccomment(m):
73 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
73 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
74 return m.group(1) + t + "*/"
74 return m.group(1) + t + "*/"
75
75
76 def repcallspaces(m):
76 def repcallspaces(m):
77 t = re.sub(r"\n\s+", "\n", m.group(2))
77 t = re.sub(r"\n\s+", "\n", m.group(2))
78 return m.group(1) + t
78 return m.group(1) + t
79
79
80 def repinclude(m):
80 def repinclude(m):
81 return m.group(1) + "<foo>"
81 return m.group(1) + "<foo>"
82
82
83 def rephere(m):
83 def rephere(m):
84 t = re.sub(r"\S", "x", m.group(2))
84 t = re.sub(r"\S", "x", m.group(2))
85 return m.group(1) + t
85 return m.group(1) + t
86
86
87
87
88 testpats = [
88 testpats = [
89 [
89 [
90 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
90 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
91 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
91 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
92 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
92 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
93 (r'(?<!hg )grep.*-a', "don't use 'grep -a', use in-line python"),
93 (r'(?<!hg )grep.*-a', "don't use 'grep -a', use in-line python"),
94 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
94 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
95 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
95 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
96 (r'echo -n', "don't use 'echo -n', use printf"),
96 (r'echo -n', "don't use 'echo -n', use printf"),
97 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
97 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
98 (r'head -c', "don't use 'head -c', use 'dd'"),
98 (r'head -c', "don't use 'head -c', use 'dd'"),
99 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
99 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
100 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
100 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
101 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
101 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
102 (r'printf.*[^\\]\\([1-9]|0\d)', "don't use 'printf \NNN', use Python"),
102 (r'printf.*[^\\]\\([1-9]|0\d)', "don't use 'printf \NNN', use Python"),
103 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
103 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
104 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
104 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
105 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
105 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
106 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
106 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
107 "use egrep for extended grep syntax"),
107 "use egrep for extended grep syntax"),
108 (r'/bin/', "don't use explicit paths for tools"),
108 (r'/bin/', "don't use explicit paths for tools"),
109 (r'[^\n]\Z', "no trailing newline"),
109 (r'[^\n]\Z', "no trailing newline"),
110 (r'export.*=', "don't export and assign at once"),
110 (r'export.*=', "don't export and assign at once"),
111 (r'^source\b', "don't use 'source', use '.'"),
111 (r'^source\b', "don't use 'source', use '.'"),
112 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
112 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
113 (r'ls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
113 (r'ls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
114 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
114 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
115 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
115 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
116 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
116 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
117 (r'^alias\b.*=', "don't use alias, use a function"),
117 (r'^alias\b.*=', "don't use alias, use a function"),
118 (r'if\s*!', "don't use '!' to negate exit status"),
118 (r'if\s*!', "don't use '!' to negate exit status"),
119 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
119 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
120 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
120 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
121 (r'^( *)\t', "don't use tabs to indent"),
121 (r'^( *)\t', "don't use tabs to indent"),
122 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
122 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
123 "put a backslash-escaped newline after sed 'i' command"),
123 "put a backslash-escaped newline after sed 'i' command"),
124 (r'^diff *-\w*u.*$\n(^ \$ |^$)', "prefix diff -u with cmp"),
124 (r'^diff *-\w*u.*$\n(^ \$ |^$)', "prefix diff -u with cmp"),
125 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py")
125 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py")
126 ],
126 ],
127 # warnings
127 # warnings
128 [
128 [
129 (r'^function', "don't use 'function', use old style"),
129 (r'^function', "don't use 'function', use old style"),
130 (r'^diff.*-\w*N', "don't use 'diff -N'"),
130 (r'^diff.*-\w*N', "don't use 'diff -N'"),
131 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
131 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
132 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
132 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
133 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
133 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
134 ]
134 ]
135 ]
135 ]
136
136
137 testfilters = [
137 testfilters = [
138 (r"( *)(#([^\n]*\S)?)", repcomment),
138 (r"( *)(#([^\n]*\S)?)", repcomment),
139 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
139 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
140 ]
140 ]
141
141
142 winglobmsg = "use (glob) to match Windows paths too"
142 winglobmsg = "use (glob) to match Windows paths too"
143 uprefix = r"^ \$ "
143 uprefix = r"^ \$ "
144 utestpats = [
144 utestpats = [
145 [
145 [
146 (r'^(\S.*|| [$>] .*)[ \t]\n', "trailing whitespace on non-output"),
146 (r'^(\S.*|| [$>] .*)[ \t]\n', "trailing whitespace on non-output"),
147 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
147 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
148 "use regex test output patterns instead of sed"),
148 "use regex test output patterns instead of sed"),
149 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
149 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
150 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
150 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
151 (uprefix + r'.*\|\| echo.*(fail|error)',
151 (uprefix + r'.*\|\| echo.*(fail|error)',
152 "explicit exit code checks unnecessary"),
152 "explicit exit code checks unnecessary"),
153 (uprefix + r'set -e', "don't use set -e"),
153 (uprefix + r'set -e', "don't use set -e"),
154 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
154 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
155 (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite "
155 (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite "
156 "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx
156 "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx
157 '# no-msys'), # in test-pull.t which is skipped on windows
157 '# no-msys'), # in test-pull.t which is skipped on windows
158 (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg),
158 (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg),
159 (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$',
159 (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$',
160 winglobmsg),
160 winglobmsg),
161 (r'^ pulling from \$TESTTMP/.*[^)]$', winglobmsg,
161 (r'^ pulling from \$TESTTMP/.*[^)]$', winglobmsg,
162 '\$TESTTMP/unix-repo$'), # in test-issue1802.t which skipped on windows
162 '\$TESTTMP/unix-repo$'), # in test-issue1802.t which skipped on windows
163 (r'^ reverting (?!subrepo ).*/.*[^)]$', winglobmsg),
163 (r'^ reverting (?!subrepo ).*/.*[^)]$', winglobmsg),
164 (r'^ cloning subrepo \S+/.*[^)]$', winglobmsg),
164 (r'^ cloning subrepo \S+/.*[^)]$', winglobmsg),
165 (r'^ pushing to \$TESTTMP/.*[^)]$', winglobmsg),
165 (r'^ pushing to \$TESTTMP/.*[^)]$', winglobmsg),
166 (r'^ pushing subrepo \S+/\S+ to.*[^)]$', winglobmsg),
166 (r'^ pushing subrepo \S+/\S+ to.*[^)]$', winglobmsg),
167 (r'^ moving \S+/.*[^)]$', winglobmsg),
167 (r'^ moving \S+/.*[^)]$', winglobmsg),
168 (r'^ no changes made to subrepo since.*/.*[^)]$', winglobmsg),
168 (r'^ no changes made to subrepo since.*/.*[^)]$', winglobmsg),
169 (r'^ .*: largefile \S+ not available from file:.*/.*[^)]$', winglobmsg),
169 (r'^ .*: largefile \S+ not available from file:.*/.*[^)]$', winglobmsg),
170 (r'^ .*file://\$TESTTMP',
170 (r'^ .*file://\$TESTTMP',
171 'write "file:/*/$TESTTMP" + (glob) to match on windows too'),
171 'write "file:/*/$TESTTMP" + (glob) to match on windows too'),
172 (r'^ (cat|find): .*: No such file or directory',
172 (r'^ (cat|find): .*: No such file or directory',
173 'use test -f to test for file existence'),
173 'use test -f to test for file existence'),
174 ],
174 ],
175 # warnings
175 # warnings
176 [
176 [
177 (r'^ [^*?/\n]* \(glob\)$',
177 (r'^ [^*?/\n]* \(glob\)$',
178 "glob match with no glob character (?*/)"),
178 "glob match with no glob character (?*/)"),
179 ]
179 ]
180 ]
180 ]
181
181
182 for i in [0, 1]:
182 for i in [0, 1]:
183 for tp in testpats[i]:
183 for tp in testpats[i]:
184 p = tp[0]
184 p = tp[0]
185 m = tp[1]
185 m = tp[1]
186 if p.startswith(r'^'):
186 if p.startswith(r'^'):
187 p = r"^ [$>] (%s)" % p[1:]
187 p = r"^ [$>] (%s)" % p[1:]
188 else:
188 else:
189 p = r"^ [$>] .*(%s)" % p
189 p = r"^ [$>] .*(%s)" % p
190 utestpats[i].append((p, m) + tp[2:])
190 utestpats[i].append((p, m) + tp[2:])
191
191
192 utestfilters = [
192 utestfilters = [
193 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
193 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
194 (r"( *)(#([^\n]*\S)?)", repcomment),
194 (r"( *)(#([^\n]*\S)?)", repcomment),
195 ]
195 ]
196
196
197 pypats = [
197 pypats = [
198 [
198 [
199 (r'\([^)]*\*\w[^()]+\w+=', "can't pass varargs with keyword in Py2.5"),
199 (r'\([^)]*\*\w[^()]+\w+=', "can't pass varargs with keyword in Py2.5"),
200 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
200 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
201 "tuple parameter unpacking not available in Python 3+"),
201 "tuple parameter unpacking not available in Python 3+"),
202 (r'lambda\s*\(.*,.*\)',
202 (r'lambda\s*\(.*,.*\)',
203 "tuple parameter unpacking not available in Python 3+"),
203 "tuple parameter unpacking not available in Python 3+"),
204 (r'import (.+,[^.]+\.[^.]+|[^.]+\.[^.]+,)',
204 (r'import (.+,[^.]+\.[^.]+|[^.]+\.[^.]+,)',
205 '2to3 can\'t always rewrite "import qux, foo.bar", '
205 '2to3 can\'t always rewrite "import qux, foo.bar", '
206 'use "import foo.bar" on its own line instead.'),
206 'use "import foo.bar" on its own line instead.'),
207 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
207 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
208 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
208 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
209 (r'dict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
209 (r'dict\(.*=', 'dict() is different in Py2 and 3 and is slower than {}',
210 'dict-from-generator'),
210 'dict-from-generator'),
211 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
211 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
212 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
212 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
213 (r'^\s*\t', "don't use tabs"),
213 (r'^\s*\t', "don't use tabs"),
214 (r'\S;\s*\n', "semicolon"),
214 (r'\S;\s*\n', "semicolon"),
215 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
215 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
216 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
216 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
217 (r'(\w|\)),\w', "missing whitespace after ,"),
217 (r'(\w|\)),\w', "missing whitespace after ,"),
218 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
218 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
219 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
219 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
220 (r'.{81}', "line too long"),
220 (r'.{81}', "line too long"),
221 (r' x+[xo][\'"]\n\s+[\'"]x', 'string join across lines with no space'),
221 (r' x+[xo][\'"]\n\s+[\'"]x', 'string join across lines with no space'),
222 (r'[^\n]\Z', "no trailing newline"),
222 (r'[^\n]\Z', "no trailing newline"),
223 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
223 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
224 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
224 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
225 # "don't use underbars in identifiers"),
225 # "don't use underbars in identifiers"),
226 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
226 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
227 "don't use camelcase in identifiers"),
227 "don't use camelcase in identifiers"),
228 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
228 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
229 "linebreak after :"),
229 "linebreak after :"),
230 (r'class\s[^( \n]+:', "old-style class, use class foo(object)"),
230 (r'class\s[^( \n]+:', "old-style class, use class foo(object)"),
231 (r'class\s[^( \n]+\(\):',
231 (r'class\s[^( \n]+\(\):',
232 "class foo() creates old style object, use class foo(object)"),
232 "class foo() creates old style object, use class foo(object)"),
233 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
233 (r'\b(%s)\(' % '|'.join(k for k in keyword.kwlist
234 if k not in ('print', 'exec')),
234 if k not in ('print', 'exec')),
235 "Python keyword is not a function"),
235 "Python keyword is not a function"),
236 (r',]', "unneeded trailing ',' in list"),
236 (r',]', "unneeded trailing ',' in list"),
237 # (r'class\s[A-Z][^\(]*\((?!Exception)',
237 # (r'class\s[A-Z][^\(]*\((?!Exception)',
238 # "don't capitalize non-exception classes"),
238 # "don't capitalize non-exception classes"),
239 # (r'in range\(', "use xrange"),
239 # (r'in range\(', "use xrange"),
240 # (r'^\s*print\s+', "avoid using print in core and extensions"),
240 # (r'^\s*print\s+', "avoid using print in core and extensions"),
241 (r'[\x80-\xff]', "non-ASCII character literal"),
241 (r'[\x80-\xff]', "non-ASCII character literal"),
242 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
242 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
243 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
243 "gratuitous whitespace after Python keyword"),
244 "gratuitous whitespace after Python keyword"),
244 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
245 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
245 # (r'\s\s=', "gratuitous whitespace before ="),
246 # (r'\s\s=', "gratuitous whitespace before ="),
246 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
247 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
247 "missing whitespace around operator"),
248 "missing whitespace around operator"),
248 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
249 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
249 "missing whitespace around operator"),
250 "missing whitespace around operator"),
250 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
251 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
251 "missing whitespace around operator"),
252 "missing whitespace around operator"),
252 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
253 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
253 "wrong whitespace around ="),
254 "wrong whitespace around ="),
254 (r'\([^()]*( =[^=]|[^<>!=]= )',
255 (r'\([^()]*( =[^=]|[^<>!=]= )',
255 "no whitespace around = for named parameters"),
256 "no whitespace around = for named parameters"),
256 (r'raise Exception', "don't raise generic exceptions"),
257 (r'raise Exception', "don't raise generic exceptions"),
257 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
258 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
258 "don't use old-style two-argument raise, use Exception(message)"),
259 "don't use old-style two-argument raise, use Exception(message)"),
259 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
260 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
260 (r' [=!]=\s+(True|False|None)',
261 (r' [=!]=\s+(True|False|None)',
261 "comparison with singleton, use 'is' or 'is not' instead"),
262 "comparison with singleton, use 'is' or 'is not' instead"),
262 (r'^\s*(while|if) [01]:',
263 (r'^\s*(while|if) [01]:',
263 "use True/False for constant Boolean expression"),
264 "use True/False for constant Boolean expression"),
264 (r'(?:(?<!def)\s+|\()hasattr',
265 (r'(?:(?<!def)\s+|\()hasattr',
265 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
266 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
266 (r'opener\([^)]*\).read\(',
267 (r'opener\([^)]*\).read\(',
267 "use opener.read() instead"),
268 "use opener.read() instead"),
268 (r'opener\([^)]*\).write\(',
269 (r'opener\([^)]*\).write\(',
269 "use opener.write() instead"),
270 "use opener.write() instead"),
270 (r'[\s\(](open|file)\([^)]*\)\.read\(',
271 (r'[\s\(](open|file)\([^)]*\)\.read\(',
271 "use util.readfile() instead"),
272 "use util.readfile() instead"),
272 (r'[\s\(](open|file)\([^)]*\)\.write\(',
273 (r'[\s\(](open|file)\([^)]*\)\.write\(',
273 "use util.writefile() instead"),
274 "use util.writefile() instead"),
274 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
275 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
275 "always assign an opened file to a variable, and close it afterwards"),
276 "always assign an opened file to a variable, and close it afterwards"),
276 (r'[\s\(](open|file)\([^)]*\)\.',
277 (r'[\s\(](open|file)\([^)]*\)\.',
277 "always assign an opened file to a variable, and close it afterwards"),
278 "always assign an opened file to a variable, and close it afterwards"),
278 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
279 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
279 (r'\.debug\(\_', "don't mark debug messages for translation"),
280 (r'\.debug\(\_', "don't mark debug messages for translation"),
280 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
281 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
281 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
282 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
282 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
283 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
283 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
284 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
284 "missing _() in ui message (use () to hide false-positives)"),
285 "missing _() in ui message (use () to hide false-positives)"),
285 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
286 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
286 (r'\b__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
287 (r'\b__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
287 (r'os\.path\.join\(.*, *(""|\'\')\)',
288 (r'os\.path\.join\(.*, *(""|\'\')\)',
288 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
289 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
289 ],
290 ],
290 # warnings
291 # warnings
291 [
292 [
292 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
293 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
293 ]
294 ]
294 ]
295 ]
295
296
296 pyfilters = [
297 pyfilters = [
297 (r"""(?msx)(?P<comment>\#.*?$)|
298 (r"""(?msx)(?P<comment>\#.*?$)|
298 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
299 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
299 (?P<text>(([^\\]|\\.)*?))
300 (?P<text>(([^\\]|\\.)*?))
300 (?P=quote))""", reppython),
301 (?P=quote))""", reppython),
301 ]
302 ]
302
303
303 txtfilters = []
304 txtfilters = []
304
305
305 txtpats = [
306 txtpats = [
306 [
307 [
307 ('\s$', 'trailing whitespace'),
308 ('\s$', 'trailing whitespace'),
308 ('.. note::[ \n][^\n]', 'add two newlines after note::')
309 ('.. note::[ \n][^\n]', 'add two newlines after note::')
309 ],
310 ],
310 []
311 []
311 ]
312 ]
312
313
313 cpats = [
314 cpats = [
314 [
315 [
315 (r'//', "don't use //-style comments"),
316 (r'//', "don't use //-style comments"),
316 (r'^ ', "don't use spaces to indent"),
317 (r'^ ', "don't use spaces to indent"),
317 (r'\S\t', "don't use tabs except for indent"),
318 (r'\S\t', "don't use tabs except for indent"),
318 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
319 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
319 (r'.{81}', "line too long"),
320 (r'.{81}', "line too long"),
320 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
321 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
321 (r'return\(', "return is not a function"),
322 (r'return\(', "return is not a function"),
322 (r' ;', "no space before ;"),
323 (r' ;', "no space before ;"),
323 (r'[^;] \)', "no space before )"),
324 (r'[^;] \)', "no space before )"),
324 (r'[)][{]', "space between ) and {"),
325 (r'[)][{]', "space between ) and {"),
325 (r'\w+\* \w+', "use int *foo, not int* foo"),
326 (r'\w+\* \w+', "use int *foo, not int* foo"),
326 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
327 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
327 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
328 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
328 (r'\w,\w', "missing whitespace after ,"),
329 (r'\w,\w', "missing whitespace after ,"),
329 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
330 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
330 (r'^#\s+\w', "use #foo, not # foo"),
331 (r'^#\s+\w', "use #foo, not # foo"),
331 (r'[^\n]\Z', "no trailing newline"),
332 (r'[^\n]\Z', "no trailing newline"),
332 (r'^\s*#import\b', "use only #include in standard C code"),
333 (r'^\s*#import\b', "use only #include in standard C code"),
333 ],
334 ],
334 # warnings
335 # warnings
335 []
336 []
336 ]
337 ]
337
338
338 cfilters = [
339 cfilters = [
339 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
340 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
340 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
341 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
341 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
342 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
342 (r'(\()([^)]+\))', repcallspaces),
343 (r'(\()([^)]+\))', repcallspaces),
343 ]
344 ]
344
345
345 inutilpats = [
346 inutilpats = [
346 [
347 [
347 (r'\bui\.', "don't use ui in util"),
348 (r'\bui\.', "don't use ui in util"),
348 ],
349 ],
349 # warnings
350 # warnings
350 []
351 []
351 ]
352 ]
352
353
353 inrevlogpats = [
354 inrevlogpats = [
354 [
355 [
355 (r'\brepo\.', "don't use repo in revlog"),
356 (r'\brepo\.', "don't use repo in revlog"),
356 ],
357 ],
357 # warnings
358 # warnings
358 []
359 []
359 ]
360 ]
360
361
361 webtemplatefilters = []
362 webtemplatefilters = []
362
363
363 webtemplatepats = [
364 webtemplatepats = [
364 [],
365 [],
365 [
366 [
366 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
367 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
367 'follow desc keyword with either firstline or websub'),
368 'follow desc keyword with either firstline or websub'),
368 ]
369 ]
369 ]
370 ]
370
371
371 checks = [
372 checks = [
372 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
373 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
373 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
374 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
374 ('c', r'.*\.[ch]$', '', cfilters, cpats),
375 ('c', r'.*\.[ch]$', '', cfilters, cpats),
375 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
376 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
376 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
377 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
377 pyfilters, inrevlogpats),
378 pyfilters, inrevlogpats),
378 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
379 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
379 inutilpats),
380 inutilpats),
380 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
381 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
381 ('web template', r'mercurial/templates/.*\.tmpl', '',
382 ('web template', r'mercurial/templates/.*\.tmpl', '',
382 webtemplatefilters, webtemplatepats),
383 webtemplatefilters, webtemplatepats),
383 ]
384 ]
384
385
385 def _preparepats():
386 def _preparepats():
386 for c in checks:
387 for c in checks:
387 failandwarn = c[-1]
388 failandwarn = c[-1]
388 for pats in failandwarn:
389 for pats in failandwarn:
389 for i, pseq in enumerate(pats):
390 for i, pseq in enumerate(pats):
390 # fix-up regexes for multi-line searches
391 # fix-up regexes for multi-line searches
391 p = pseq[0]
392 p = pseq[0]
392 # \s doesn't match \n
393 # \s doesn't match \n
393 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
394 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
394 # [^...] doesn't match newline
395 # [^...] doesn't match newline
395 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
396 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
396
397
397 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
398 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
398 filters = c[3]
399 filters = c[3]
399 for i, flt in enumerate(filters):
400 for i, flt in enumerate(filters):
400 filters[i] = re.compile(flt[0]), flt[1]
401 filters[i] = re.compile(flt[0]), flt[1]
401 _preparepats()
402 _preparepats()
402
403
403 class norepeatlogger(object):
404 class norepeatlogger(object):
404 def __init__(self):
405 def __init__(self):
405 self._lastseen = None
406 self._lastseen = None
406
407
407 def log(self, fname, lineno, line, msg, blame):
408 def log(self, fname, lineno, line, msg, blame):
408 """print error related a to given line of a given file.
409 """print error related a to given line of a given file.
409
410
410 The faulty line will also be printed but only once in the case
411 The faulty line will also be printed but only once in the case
411 of multiple errors.
412 of multiple errors.
412
413
413 :fname: filename
414 :fname: filename
414 :lineno: line number
415 :lineno: line number
415 :line: actual content of the line
416 :line: actual content of the line
416 :msg: error message
417 :msg: error message
417 """
418 """
418 msgid = fname, lineno, line
419 msgid = fname, lineno, line
419 if msgid != self._lastseen:
420 if msgid != self._lastseen:
420 if blame:
421 if blame:
421 print "%s:%d (%s):" % (fname, lineno, blame)
422 print "%s:%d (%s):" % (fname, lineno, blame)
422 else:
423 else:
423 print "%s:%d:" % (fname, lineno)
424 print "%s:%d:" % (fname, lineno)
424 print " > %s" % line
425 print " > %s" % line
425 self._lastseen = msgid
426 self._lastseen = msgid
426 print " " + msg
427 print " " + msg
427
428
428 _defaultlogger = norepeatlogger()
429 _defaultlogger = norepeatlogger()
429
430
430 def getblame(f):
431 def getblame(f):
431 lines = []
432 lines = []
432 for l in os.popen('hg annotate -un %s' % f):
433 for l in os.popen('hg annotate -un %s' % f):
433 start, line = l.split(':', 1)
434 start, line = l.split(':', 1)
434 user, rev = start.split()
435 user, rev = start.split()
435 lines.append((line[1:-1], user, rev))
436 lines.append((line[1:-1], user, rev))
436 return lines
437 return lines
437
438
438 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
439 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
439 blame=False, debug=False, lineno=True):
440 blame=False, debug=False, lineno=True):
440 """checks style and portability of a given file
441 """checks style and portability of a given file
441
442
442 :f: filepath
443 :f: filepath
443 :logfunc: function used to report error
444 :logfunc: function used to report error
444 logfunc(filename, linenumber, linecontent, errormessage)
445 logfunc(filename, linenumber, linecontent, errormessage)
445 :maxerr: number of error to display before aborting.
446 :maxerr: number of error to display before aborting.
446 Set to false (default) to report all errors
447 Set to false (default) to report all errors
447
448
448 return True if no error is found, False otherwise.
449 return True if no error is found, False otherwise.
449 """
450 """
450 blamecache = None
451 blamecache = None
451 result = True
452 result = True
452
453
453 try:
454 try:
454 fp = open(f)
455 fp = open(f)
455 except IOError, e:
456 except IOError, e:
456 print "Skipping %s, %s" % (f, str(e).split(':', 1)[0])
457 print "Skipping %s, %s" % (f, str(e).split(':', 1)[0])
457 return result
458 return result
458 pre = post = fp.read()
459 pre = post = fp.read()
459 fp.close()
460 fp.close()
460
461
461 for name, match, magic, filters, pats in checks:
462 for name, match, magic, filters, pats in checks:
462 if debug:
463 if debug:
463 print name, f
464 print name, f
464 fc = 0
465 fc = 0
465 if not (re.match(match, f) or (magic and re.search(magic, f))):
466 if not (re.match(match, f) or (magic and re.search(magic, f))):
466 if debug:
467 if debug:
467 print "Skipping %s for %s it doesn't match %s" % (
468 print "Skipping %s for %s it doesn't match %s" % (
468 name, match, f)
469 name, match, f)
469 continue
470 continue
470 if "no-" "check-code" in pre:
471 if "no-" "check-code" in pre:
471 print "Skipping %s it has no-" "check-code" % f
472 print "Skipping %s it has no-" "check-code" % f
472 return "Skip" # skip checking this file
473 return "Skip" # skip checking this file
473 for p, r in filters:
474 for p, r in filters:
474 post = re.sub(p, r, post)
475 post = re.sub(p, r, post)
475 nerrs = len(pats[0]) # nerr elements are errors
476 nerrs = len(pats[0]) # nerr elements are errors
476 if warnings:
477 if warnings:
477 pats = pats[0] + pats[1]
478 pats = pats[0] + pats[1]
478 else:
479 else:
479 pats = pats[0]
480 pats = pats[0]
480 # print post # uncomment to show filtered version
481 # print post # uncomment to show filtered version
481
482
482 if debug:
483 if debug:
483 print "Checking %s for %s" % (name, f)
484 print "Checking %s for %s" % (name, f)
484
485
485 prelines = None
486 prelines = None
486 errors = []
487 errors = []
487 for i, pat in enumerate(pats):
488 for i, pat in enumerate(pats):
488 if len(pat) == 3:
489 if len(pat) == 3:
489 p, msg, ignore = pat
490 p, msg, ignore = pat
490 else:
491 else:
491 p, msg = pat
492 p, msg = pat
492 ignore = None
493 ignore = None
493 if i >= nerrs:
494 if i >= nerrs:
494 msg = "warning: " + msg
495 msg = "warning: " + msg
495
496
496 pos = 0
497 pos = 0
497 n = 0
498 n = 0
498 for m in p.finditer(post):
499 for m in p.finditer(post):
499 if prelines is None:
500 if prelines is None:
500 prelines = pre.splitlines()
501 prelines = pre.splitlines()
501 postlines = post.splitlines(True)
502 postlines = post.splitlines(True)
502
503
503 start = m.start()
504 start = m.start()
504 while n < len(postlines):
505 while n < len(postlines):
505 step = len(postlines[n])
506 step = len(postlines[n])
506 if pos + step > start:
507 if pos + step > start:
507 break
508 break
508 pos += step
509 pos += step
509 n += 1
510 n += 1
510 l = prelines[n]
511 l = prelines[n]
511
512
512 if ignore and re.search(ignore, l, re.MULTILINE):
513 if ignore and re.search(ignore, l, re.MULTILINE):
513 if debug:
514 if debug:
514 print "Skipping %s for %s:%s (ignore pattern)" % (
515 print "Skipping %s for %s:%s (ignore pattern)" % (
515 name, f, n)
516 name, f, n)
516 continue
517 continue
517 bd = ""
518 bd = ""
518 if blame:
519 if blame:
519 bd = 'working directory'
520 bd = 'working directory'
520 if not blamecache:
521 if not blamecache:
521 blamecache = getblame(f)
522 blamecache = getblame(f)
522 if n < len(blamecache):
523 if n < len(blamecache):
523 bl, bu, br = blamecache[n]
524 bl, bu, br = blamecache[n]
524 if bl == l:
525 if bl == l:
525 bd = '%s@%s' % (bu, br)
526 bd = '%s@%s' % (bu, br)
526
527
527 errors.append((f, lineno and n + 1, l, msg, bd))
528 errors.append((f, lineno and n + 1, l, msg, bd))
528 result = False
529 result = False
529
530
530 errors.sort()
531 errors.sort()
531 for e in errors:
532 for e in errors:
532 logfunc(*e)
533 logfunc(*e)
533 fc += 1
534 fc += 1
534 if maxerr and fc >= maxerr:
535 if maxerr and fc >= maxerr:
535 print " (too many errors, giving up)"
536 print " (too many errors, giving up)"
536 break
537 break
537
538
538 return result
539 return result
539
540
540 if __name__ == "__main__":
541 if __name__ == "__main__":
541 parser = optparse.OptionParser("%prog [options] [files]")
542 parser = optparse.OptionParser("%prog [options] [files]")
542 parser.add_option("-w", "--warnings", action="store_true",
543 parser.add_option("-w", "--warnings", action="store_true",
543 help="include warning-level checks")
544 help="include warning-level checks")
544 parser.add_option("-p", "--per-file", type="int",
545 parser.add_option("-p", "--per-file", type="int",
545 help="max warnings per file")
546 help="max warnings per file")
546 parser.add_option("-b", "--blame", action="store_true",
547 parser.add_option("-b", "--blame", action="store_true",
547 help="use annotate to generate blame info")
548 help="use annotate to generate blame info")
548 parser.add_option("", "--debug", action="store_true",
549 parser.add_option("", "--debug", action="store_true",
549 help="show debug information")
550 help="show debug information")
550 parser.add_option("", "--nolineno", action="store_false",
551 parser.add_option("", "--nolineno", action="store_false",
551 dest='lineno', help="don't show line numbers")
552 dest='lineno', help="don't show line numbers")
552
553
553 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
554 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
554 lineno=True)
555 lineno=True)
555 (options, args) = parser.parse_args()
556 (options, args) = parser.parse_args()
556
557
557 if len(args) == 0:
558 if len(args) == 0:
558 check = glob.glob("*")
559 check = glob.glob("*")
559 else:
560 else:
560 check = args
561 check = args
561
562
562 ret = 0
563 ret = 0
563 for f in check:
564 for f in check:
564 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
565 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
565 blame=options.blame, debug=options.debug,
566 blame=options.blame, debug=options.debug,
566 lineno=options.lineno):
567 lineno=options.lineno):
567 ret = 1
568 ret = 1
568 sys.exit(ret)
569 sys.exit(ret)
General Comments 0
You need to be logged in to leave comments. Login now