##// END OF EJS Templates
check-code: drop ban of BaseException...
Pierre-Yves David -
r25180:3ff2a5dc default
parent child Browse files
Show More
@@ -1,573 +1,572 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() not available in Python 2.4"),
242 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
243 (r'(?<!def)\s+(format)\(',
243 (r'(?<!def)\s+(format)\(',
244 "format not available in Python 2.4", 'no-py24'),
244 "format not available in Python 2.4", 'no-py24'),
245 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
245 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
246 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
246 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
247 "gratuitous whitespace after Python keyword"),
247 "gratuitous whitespace after Python keyword"),
248 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
248 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
249 # (r'\s\s=', "gratuitous whitespace before ="),
249 # (r'\s\s=', "gratuitous whitespace before ="),
250 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
250 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
251 "missing whitespace around operator"),
251 "missing whitespace around operator"),
252 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
252 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
253 "missing whitespace around operator"),
253 "missing whitespace around operator"),
254 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
254 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
255 "missing whitespace around operator"),
255 "missing whitespace around operator"),
256 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
256 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
257 "wrong whitespace around ="),
257 "wrong whitespace around ="),
258 (r'\([^()]*( =[^=]|[^<>!=]= )',
258 (r'\([^()]*( =[^=]|[^<>!=]= )',
259 "no whitespace around = for named parameters"),
259 "no whitespace around = for named parameters"),
260 (r'raise Exception', "don't raise generic exceptions"),
260 (r'raise Exception', "don't raise generic exceptions"),
261 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
261 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
262 "don't use old-style two-argument raise, use Exception(message)"),
262 "don't use old-style two-argument raise, use Exception(message)"),
263 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
263 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
264 (r' [=!]=\s+(True|False|None)',
264 (r' [=!]=\s+(True|False|None)',
265 "comparison with singleton, use 'is' or 'is not' instead"),
265 "comparison with singleton, use 'is' or 'is not' instead"),
266 (r'^\s*(while|if) [01]:',
266 (r'^\s*(while|if) [01]:',
267 "use True/False for constant Boolean expression"),
267 "use True/False for constant Boolean expression"),
268 (r'(?:(?<!def)\s+|\()hasattr',
268 (r'(?:(?<!def)\s+|\()hasattr',
269 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
269 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
270 (r'opener\([^)]*\).read\(',
270 (r'opener\([^)]*\).read\(',
271 "use opener.read() instead"),
271 "use opener.read() instead"),
272 (r'BaseException', 'not in Python 2.4, use Exception'),
273 (r'opener\([^)]*\).write\(',
272 (r'opener\([^)]*\).write\(',
274 "use opener.write() instead"),
273 "use opener.write() instead"),
275 (r'[\s\(](open|file)\([^)]*\)\.read\(',
274 (r'[\s\(](open|file)\([^)]*\)\.read\(',
276 "use util.readfile() instead"),
275 "use util.readfile() instead"),
277 (r'[\s\(](open|file)\([^)]*\)\.write\(',
276 (r'[\s\(](open|file)\([^)]*\)\.write\(',
278 "use util.writefile() instead"),
277 "use util.writefile() instead"),
279 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
278 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
280 "always assign an opened file to a variable, and close it afterwards"),
279 "always assign an opened file to a variable, and close it afterwards"),
281 (r'[\s\(](open|file)\([^)]*\)\.',
280 (r'[\s\(](open|file)\([^)]*\)\.',
282 "always assign an opened file to a variable, and close it afterwards"),
281 "always assign an opened file to a variable, and close it afterwards"),
283 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
282 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
284 (r'\.debug\(\_', "don't mark debug messages for translation"),
283 (r'\.debug\(\_', "don't mark debug messages for translation"),
285 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
284 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
286 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
285 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
287 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
286 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
288 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
287 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
289 "missing _() in ui message (use () to hide false-positives)"),
288 "missing _() in ui message (use () to hide false-positives)"),
290 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
289 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
291 (r'\b__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
290 (r'\b__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
292 (r'os\.path\.join\(.*, *(""|\'\')\)',
291 (r'os\.path\.join\(.*, *(""|\'\')\)',
293 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
292 "use pathutil.normasprefix(path) instead of os.path.join(path, '')"),
294 ],
293 ],
295 # warnings
294 # warnings
296 [
295 [
297 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
296 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
298 ]
297 ]
299 ]
298 ]
300
299
301 pyfilters = [
300 pyfilters = [
302 (r"""(?msx)(?P<comment>\#.*?$)|
301 (r"""(?msx)(?P<comment>\#.*?$)|
303 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
302 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
304 (?P<text>(([^\\]|\\.)*?))
303 (?P<text>(([^\\]|\\.)*?))
305 (?P=quote))""", reppython),
304 (?P=quote))""", reppython),
306 ]
305 ]
307
306
308 txtfilters = []
307 txtfilters = []
309
308
310 txtpats = [
309 txtpats = [
311 [
310 [
312 ('\s$', 'trailing whitespace'),
311 ('\s$', 'trailing whitespace'),
313 ('.. note::[ \n][^\n]', 'add two newlines after note::')
312 ('.. note::[ \n][^\n]', 'add two newlines after note::')
314 ],
313 ],
315 []
314 []
316 ]
315 ]
317
316
318 cpats = [
317 cpats = [
319 [
318 [
320 (r'//', "don't use //-style comments"),
319 (r'//', "don't use //-style comments"),
321 (r'^ ', "don't use spaces to indent"),
320 (r'^ ', "don't use spaces to indent"),
322 (r'\S\t', "don't use tabs except for indent"),
321 (r'\S\t', "don't use tabs except for indent"),
323 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
322 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
324 (r'.{81}', "line too long"),
323 (r'.{81}', "line too long"),
325 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
324 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
326 (r'return\(', "return is not a function"),
325 (r'return\(', "return is not a function"),
327 (r' ;', "no space before ;"),
326 (r' ;', "no space before ;"),
328 (r'[^;] \)', "no space before )"),
327 (r'[^;] \)', "no space before )"),
329 (r'[)][{]', "space between ) and {"),
328 (r'[)][{]', "space between ) and {"),
330 (r'\w+\* \w+', "use int *foo, not int* foo"),
329 (r'\w+\* \w+', "use int *foo, not int* foo"),
331 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
330 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
332 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
331 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
333 (r'\w,\w', "missing whitespace after ,"),
332 (r'\w,\w', "missing whitespace after ,"),
334 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
333 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
335 (r'^#\s+\w', "use #foo, not # foo"),
334 (r'^#\s+\w', "use #foo, not # foo"),
336 (r'[^\n]\Z', "no trailing newline"),
335 (r'[^\n]\Z', "no trailing newline"),
337 (r'^\s*#import\b', "use only #include in standard C code"),
336 (r'^\s*#import\b', "use only #include in standard C code"),
338 ],
337 ],
339 # warnings
338 # warnings
340 []
339 []
341 ]
340 ]
342
341
343 cfilters = [
342 cfilters = [
344 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
343 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
345 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
344 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
346 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
345 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
347 (r'(\()([^)]+\))', repcallspaces),
346 (r'(\()([^)]+\))', repcallspaces),
348 ]
347 ]
349
348
350 inutilpats = [
349 inutilpats = [
351 [
350 [
352 (r'\bui\.', "don't use ui in util"),
351 (r'\bui\.', "don't use ui in util"),
353 ],
352 ],
354 # warnings
353 # warnings
355 []
354 []
356 ]
355 ]
357
356
358 inrevlogpats = [
357 inrevlogpats = [
359 [
358 [
360 (r'\brepo\.', "don't use repo in revlog"),
359 (r'\brepo\.', "don't use repo in revlog"),
361 ],
360 ],
362 # warnings
361 # warnings
363 []
362 []
364 ]
363 ]
365
364
366 webtemplatefilters = []
365 webtemplatefilters = []
367
366
368 webtemplatepats = [
367 webtemplatepats = [
369 [],
368 [],
370 [
369 [
371 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
370 (r'{desc(\|(?!websub|firstline)[^\|]*)+}',
372 'follow desc keyword with either firstline or websub'),
371 'follow desc keyword with either firstline or websub'),
373 ]
372 ]
374 ]
373 ]
375
374
376 checks = [
375 checks = [
377 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
376 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
378 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
377 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
379 ('c', r'.*\.[ch]$', '', cfilters, cpats),
378 ('c', r'.*\.[ch]$', '', cfilters, cpats),
380 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
379 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
381 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
380 ('layering violation repo in revlog', r'mercurial/revlog\.py', '',
382 pyfilters, inrevlogpats),
381 pyfilters, inrevlogpats),
383 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
382 ('layering violation ui in util', r'mercurial/util\.py', '', pyfilters,
384 inutilpats),
383 inutilpats),
385 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
384 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
386 ('web template', r'mercurial/templates/.*\.tmpl', '',
385 ('web template', r'mercurial/templates/.*\.tmpl', '',
387 webtemplatefilters, webtemplatepats),
386 webtemplatefilters, webtemplatepats),
388 ]
387 ]
389
388
390 def _preparepats():
389 def _preparepats():
391 for c in checks:
390 for c in checks:
392 failandwarn = c[-1]
391 failandwarn = c[-1]
393 for pats in failandwarn:
392 for pats in failandwarn:
394 for i, pseq in enumerate(pats):
393 for i, pseq in enumerate(pats):
395 # fix-up regexes for multi-line searches
394 # fix-up regexes for multi-line searches
396 p = pseq[0]
395 p = pseq[0]
397 # \s doesn't match \n
396 # \s doesn't match \n
398 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
397 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
399 # [^...] doesn't match newline
398 # [^...] doesn't match newline
400 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
399 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
401
400
402 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
401 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
403 filters = c[3]
402 filters = c[3]
404 for i, flt in enumerate(filters):
403 for i, flt in enumerate(filters):
405 filters[i] = re.compile(flt[0]), flt[1]
404 filters[i] = re.compile(flt[0]), flt[1]
406 _preparepats()
405 _preparepats()
407
406
408 class norepeatlogger(object):
407 class norepeatlogger(object):
409 def __init__(self):
408 def __init__(self):
410 self._lastseen = None
409 self._lastseen = None
411
410
412 def log(self, fname, lineno, line, msg, blame):
411 def log(self, fname, lineno, line, msg, blame):
413 """print error related a to given line of a given file.
412 """print error related a to given line of a given file.
414
413
415 The faulty line will also be printed but only once in the case
414 The faulty line will also be printed but only once in the case
416 of multiple errors.
415 of multiple errors.
417
416
418 :fname: filename
417 :fname: filename
419 :lineno: line number
418 :lineno: line number
420 :line: actual content of the line
419 :line: actual content of the line
421 :msg: error message
420 :msg: error message
422 """
421 """
423 msgid = fname, lineno, line
422 msgid = fname, lineno, line
424 if msgid != self._lastseen:
423 if msgid != self._lastseen:
425 if blame:
424 if blame:
426 print "%s:%d (%s):" % (fname, lineno, blame)
425 print "%s:%d (%s):" % (fname, lineno, blame)
427 else:
426 else:
428 print "%s:%d:" % (fname, lineno)
427 print "%s:%d:" % (fname, lineno)
429 print " > %s" % line
428 print " > %s" % line
430 self._lastseen = msgid
429 self._lastseen = msgid
431 print " " + msg
430 print " " + msg
432
431
433 _defaultlogger = norepeatlogger()
432 _defaultlogger = norepeatlogger()
434
433
435 def getblame(f):
434 def getblame(f):
436 lines = []
435 lines = []
437 for l in os.popen('hg annotate -un %s' % f):
436 for l in os.popen('hg annotate -un %s' % f):
438 start, line = l.split(':', 1)
437 start, line = l.split(':', 1)
439 user, rev = start.split()
438 user, rev = start.split()
440 lines.append((line[1:-1], user, rev))
439 lines.append((line[1:-1], user, rev))
441 return lines
440 return lines
442
441
443 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
442 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
444 blame=False, debug=False, lineno=True):
443 blame=False, debug=False, lineno=True):
445 """checks style and portability of a given file
444 """checks style and portability of a given file
446
445
447 :f: filepath
446 :f: filepath
448 :logfunc: function used to report error
447 :logfunc: function used to report error
449 logfunc(filename, linenumber, linecontent, errormessage)
448 logfunc(filename, linenumber, linecontent, errormessage)
450 :maxerr: number of error to display before aborting.
449 :maxerr: number of error to display before aborting.
451 Set to false (default) to report all errors
450 Set to false (default) to report all errors
452
451
453 return True if no error is found, False otherwise.
452 return True if no error is found, False otherwise.
454 """
453 """
455 blamecache = None
454 blamecache = None
456 result = True
455 result = True
457
456
458 try:
457 try:
459 fp = open(f)
458 fp = open(f)
460 except IOError, e:
459 except IOError, e:
461 print "Skipping %s, %s" % (f, str(e).split(':', 1)[0])
460 print "Skipping %s, %s" % (f, str(e).split(':', 1)[0])
462 return result
461 return result
463 pre = post = fp.read()
462 pre = post = fp.read()
464 fp.close()
463 fp.close()
465
464
466 for name, match, magic, filters, pats in checks:
465 for name, match, magic, filters, pats in checks:
467 if debug:
466 if debug:
468 print name, f
467 print name, f
469 fc = 0
468 fc = 0
470 if not (re.match(match, f) or (magic and re.search(magic, f))):
469 if not (re.match(match, f) or (magic and re.search(magic, f))):
471 if debug:
470 if debug:
472 print "Skipping %s for %s it doesn't match %s" % (
471 print "Skipping %s for %s it doesn't match %s" % (
473 name, match, f)
472 name, match, f)
474 continue
473 continue
475 if "no-" "check-code" in pre:
474 if "no-" "check-code" in pre:
476 print "Skipping %s it has no-" "check-code" % f
475 print "Skipping %s it has no-" "check-code" % f
477 return "Skip" # skip checking this file
476 return "Skip" # skip checking this file
478 for p, r in filters:
477 for p, r in filters:
479 post = re.sub(p, r, post)
478 post = re.sub(p, r, post)
480 nerrs = len(pats[0]) # nerr elements are errors
479 nerrs = len(pats[0]) # nerr elements are errors
481 if warnings:
480 if warnings:
482 pats = pats[0] + pats[1]
481 pats = pats[0] + pats[1]
483 else:
482 else:
484 pats = pats[0]
483 pats = pats[0]
485 # print post # uncomment to show filtered version
484 # print post # uncomment to show filtered version
486
485
487 if debug:
486 if debug:
488 print "Checking %s for %s" % (name, f)
487 print "Checking %s for %s" % (name, f)
489
488
490 prelines = None
489 prelines = None
491 errors = []
490 errors = []
492 for i, pat in enumerate(pats):
491 for i, pat in enumerate(pats):
493 if len(pat) == 3:
492 if len(pat) == 3:
494 p, msg, ignore = pat
493 p, msg, ignore = pat
495 else:
494 else:
496 p, msg = pat
495 p, msg = pat
497 ignore = None
496 ignore = None
498 if i >= nerrs:
497 if i >= nerrs:
499 msg = "warning: " + msg
498 msg = "warning: " + msg
500
499
501 pos = 0
500 pos = 0
502 n = 0
501 n = 0
503 for m in p.finditer(post):
502 for m in p.finditer(post):
504 if prelines is None:
503 if prelines is None:
505 prelines = pre.splitlines()
504 prelines = pre.splitlines()
506 postlines = post.splitlines(True)
505 postlines = post.splitlines(True)
507
506
508 start = m.start()
507 start = m.start()
509 while n < len(postlines):
508 while n < len(postlines):
510 step = len(postlines[n])
509 step = len(postlines[n])
511 if pos + step > start:
510 if pos + step > start:
512 break
511 break
513 pos += step
512 pos += step
514 n += 1
513 n += 1
515 l = prelines[n]
514 l = prelines[n]
516
515
517 if ignore and re.search(ignore, l, re.MULTILINE):
516 if ignore and re.search(ignore, l, re.MULTILINE):
518 if debug:
517 if debug:
519 print "Skipping %s for %s:%s (ignore pattern)" % (
518 print "Skipping %s for %s:%s (ignore pattern)" % (
520 name, f, n)
519 name, f, n)
521 continue
520 continue
522 bd = ""
521 bd = ""
523 if blame:
522 if blame:
524 bd = 'working directory'
523 bd = 'working directory'
525 if not blamecache:
524 if not blamecache:
526 blamecache = getblame(f)
525 blamecache = getblame(f)
527 if n < len(blamecache):
526 if n < len(blamecache):
528 bl, bu, br = blamecache[n]
527 bl, bu, br = blamecache[n]
529 if bl == l:
528 if bl == l:
530 bd = '%s@%s' % (bu, br)
529 bd = '%s@%s' % (bu, br)
531
530
532 errors.append((f, lineno and n + 1, l, msg, bd))
531 errors.append((f, lineno and n + 1, l, msg, bd))
533 result = False
532 result = False
534
533
535 errors.sort()
534 errors.sort()
536 for e in errors:
535 for e in errors:
537 logfunc(*e)
536 logfunc(*e)
538 fc += 1
537 fc += 1
539 if maxerr and fc >= maxerr:
538 if maxerr and fc >= maxerr:
540 print " (too many errors, giving up)"
539 print " (too many errors, giving up)"
541 break
540 break
542
541
543 return result
542 return result
544
543
545 if __name__ == "__main__":
544 if __name__ == "__main__":
546 parser = optparse.OptionParser("%prog [options] [files]")
545 parser = optparse.OptionParser("%prog [options] [files]")
547 parser.add_option("-w", "--warnings", action="store_true",
546 parser.add_option("-w", "--warnings", action="store_true",
548 help="include warning-level checks")
547 help="include warning-level checks")
549 parser.add_option("-p", "--per-file", type="int",
548 parser.add_option("-p", "--per-file", type="int",
550 help="max warnings per file")
549 help="max warnings per file")
551 parser.add_option("-b", "--blame", action="store_true",
550 parser.add_option("-b", "--blame", action="store_true",
552 help="use annotate to generate blame info")
551 help="use annotate to generate blame info")
553 parser.add_option("", "--debug", action="store_true",
552 parser.add_option("", "--debug", action="store_true",
554 help="show debug information")
553 help="show debug information")
555 parser.add_option("", "--nolineno", action="store_false",
554 parser.add_option("", "--nolineno", action="store_false",
556 dest='lineno', help="don't show line numbers")
555 dest='lineno', help="don't show line numbers")
557
556
558 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
557 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
559 lineno=True)
558 lineno=True)
560 (options, args) = parser.parse_args()
559 (options, args) = parser.parse_args()
561
560
562 if len(args) == 0:
561 if len(args) == 0:
563 check = glob.glob("*")
562 check = glob.glob("*")
564 else:
563 else:
565 check = args
564 check = args
566
565
567 ret = 0
566 ret = 0
568 for f in check:
567 for f in check:
569 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
568 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
570 blame=options.blame, debug=options.debug,
569 blame=options.blame, debug=options.debug,
571 lineno=options.lineno):
570 lineno=options.lineno):
572 ret = 1
571 ret = 1
573 sys.exit(ret)
572 sys.exit(ret)
General Comments 0
You need to be logged in to leave comments. Login now