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