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