##// END OF EJS Templates
check-code: add test for callable
Martin Geisler -
r11522:eaa7666a stable
parent child Browse files
Show More
@@ -1,231 +1,233 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
10 import re, glob
11 import optparse
11 import optparse
12
12
13 def repquote(m):
13 def repquote(m):
14 t = re.sub(r"\w", "x", m.group('text'))
14 t = re.sub(r"\w", "x", m.group('text'))
15 t = re.sub(r"[^\sx]", "o", t)
15 t = re.sub(r"[^\sx]", "o", t)
16 return m.group('quote') + t + m.group('quote')
16 return m.group('quote') + t + m.group('quote')
17
17
18 def reppython(m):
18 def reppython(m):
19 comment = m.group('comment')
19 comment = m.group('comment')
20 if comment:
20 if comment:
21 return "#" * len(comment)
21 return "#" * len(comment)
22 return repquote(m)
22 return repquote(m)
23
23
24 def repcomment(m):
24 def repcomment(m):
25 return m.group(1) + "#" * len(m.group(2))
25 return m.group(1) + "#" * len(m.group(2))
26
26
27 def repccomment(m):
27 def repccomment(m):
28 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
28 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
29 return m.group(1) + t + "*/"
29 return m.group(1) + t + "*/"
30
30
31 def repcallspaces(m):
31 def repcallspaces(m):
32 t = re.sub(r"\n\s+", "\n", m.group(2))
32 t = re.sub(r"\n\s+", "\n", m.group(2))
33 return m.group(1) + t
33 return m.group(1) + t
34
34
35 def repinclude(m):
35 def repinclude(m):
36 return m.group(1) + "<foo>"
36 return m.group(1) + "<foo>"
37
37
38 def rephere(m):
38 def rephere(m):
39 t = re.sub(r"\S", "x", m.group(2))
39 t = re.sub(r"\S", "x", m.group(2))
40 return m.group(1) + t
40 return m.group(1) + t
41
41
42
42
43 testpats = [
43 testpats = [
44 (r'(pushd|popd)', "don't use 'pushd' or 'popd', use 'cd'"),
44 (r'(pushd|popd)', "don't use 'pushd' or 'popd', use 'cd'"),
45 (r'\W\$?\(\([^\)]*\)\)', "don't use (()) or $(()), use 'expr'"),
45 (r'\W\$?\(\([^\)]*\)\)', "don't use (()) or $(()), use 'expr'"),
46 (r'^function', "don't use 'function', use old style"),
46 (r'^function', "don't use 'function', use old style"),
47 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
47 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
48 (r'echo.*\\n', "don't use 'echo \\n', use printf"),
48 (r'echo.*\\n', "don't use 'echo \\n', use printf"),
49 (r'^diff.*-\w*N', "don't use 'diff -N'"),
49 (r'^diff.*-\w*N', "don't use 'diff -N'"),
50 (r'(^| )wc[^|]*$', "filter wc output"),
50 (r'(^| )wc[^|]*$', "filter wc output"),
51 (r'head -c', "don't use 'head -c', use 'dd'"),
51 (r'head -c', "don't use 'head -c', use 'dd'"),
52 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
52 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
53 (r'printf.*\\\d\d\d', "don't use 'printf \NNN', use Python"),
53 (r'printf.*\\\d\d\d', "don't use 'printf \NNN', use Python"),
54 (r'printf.*\\x', "don't use printf \\x, use Python"),
54 (r'printf.*\\x', "don't use printf \\x, use Python"),
55 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
55 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
56 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
56 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
57 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
57 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
58 "use egrep for extended grep syntax"),
58 "use egrep for extended grep syntax"),
59 (r'/bin/', "don't use explicit paths for tools"),
59 (r'/bin/', "don't use explicit paths for tools"),
60 (r'\$PWD', "don't use $PWD, use `pwd`"),
60 (r'\$PWD', "don't use $PWD, use `pwd`"),
61 (r'[^\n]\Z', "no trailing newline"),
61 (r'[^\n]\Z', "no trailing newline"),
62 (r'export.*=', "don't export and assign at once"),
62 (r'export.*=', "don't export and assign at once"),
63 ('^([^"\']|("[^"]*")|(\'[^\']*\'))*\\^', "^ must be quoted"),
63 ('^([^"\']|("[^"]*")|(\'[^\']*\'))*\\^', "^ must be quoted"),
64 (r'^source\b', "don't use 'source', use '.'"),
64 (r'^source\b', "don't use 'source', use '.'"),
65 ]
65 ]
66
66
67 testfilters = [
67 testfilters = [
68 (r"( *)(#([^\n]*\S)?)", repcomment),
68 (r"( *)(#([^\n]*\S)?)", repcomment),
69 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
69 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
70 ]
70 ]
71
71
72 pypats = [
72 pypats = [
73 (r'^\s*\t', "don't use tabs"),
73 (r'^\s*\t', "don't use tabs"),
74 (r'\S;\s*\n', "semicolon"),
74 (r'\S;\s*\n', "semicolon"),
75 (r'\w,\w', "missing whitespace after ,"),
75 (r'\w,\w', "missing whitespace after ,"),
76 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
76 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
77 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
77 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
78 (r'.{85}', "line too long"),
78 (r'.{85}', "line too long"),
79 (r'[^\n]\Z', "no trailing newline"),
79 (r'[^\n]\Z', "no trailing newline"),
80 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
80 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
81 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
81 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
82 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
82 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
83 "linebreak after :"),
83 "linebreak after :"),
84 (r'class\s[^(]:', "old-style class, use class foo(object)"),
84 (r'class\s[^(]:', "old-style class, use class foo(object)"),
85 (r'^\s+del\(', "del isn't a function"),
85 (r'^\s+del\(', "del isn't a function"),
86 (r'^\s+except\(', "except isn't a function"),
86 (r'^\s+except\(', "except isn't a function"),
87 (r',]', "unneeded trailing ',' in list"),
87 (r',]', "unneeded trailing ',' in list"),
88 # (r'class\s[A-Z][^\(]*\((?!Exception)',
88 # (r'class\s[A-Z][^\(]*\((?!Exception)',
89 # "don't capitalize non-exception classes"),
89 # "don't capitalize non-exception classes"),
90 # (r'in range\(', "use xrange"),
90 # (r'in range\(', "use xrange"),
91 # (r'^\s*print\s+', "avoid using print in core and extensions"),
91 # (r'^\s*print\s+', "avoid using print in core and extensions"),
92 (r'[\x80-\xff]', "non-ASCII character literal"),
92 (r'[\x80-\xff]', "non-ASCII character literal"),
93 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
93 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
94 (r'^\s*with\s+', "with not available in Python 2.4"),
94 (r'^\s*with\s+', "with not available in Python 2.4"),
95 (r'(?<!def)\s+(any|all|format)\(',
95 (r'(?<!def)\s+(any|all|format)\(',
96 "any/all/format not available in Python 2.4"),
96 "any/all/format not available in Python 2.4"),
97 (r'(?<!def)\s+(callable)\(',
98 "callable not available in Python 3, use hasattr(f, '__call__')"),
97 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
99 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
98 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
100 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
99 # (r'\s\s=', "gratuitous whitespace before ="),
101 # (r'\s\s=', "gratuitous whitespace before ="),
100 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
102 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
101 "missing whitespace around operator"),
103 "missing whitespace around operator"),
102 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s',
104 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s',
103 "missing whitespace around operator"),
105 "missing whitespace around operator"),
104 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
106 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
105 "missing whitespace around operator"),
107 "missing whitespace around operator"),
106 (r'[^+=*!<>&| -](\s=|=\s)[^= ]',
108 (r'[^+=*!<>&| -](\s=|=\s)[^= ]',
107 "wrong whitespace around ="),
109 "wrong whitespace around ="),
108 (r'raise Exception', "don't raise generic exceptions"),
110 (r'raise Exception', "don't raise generic exceptions"),
109 (r'ui\.(status|progress|write|note)\([\'\"]x',
111 (r'ui\.(status|progress|write|note)\([\'\"]x',
110 "warning: unwrapped ui message"),
112 "warning: unwrapped ui message"),
111 ]
113 ]
112
114
113 pyfilters = [
115 pyfilters = [
114 (r"""(?msx)(?P<comment>\#.*?$)|
116 (r"""(?msx)(?P<comment>\#.*?$)|
115 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
117 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
116 (?P<text>(([^\\]|\\.)*?))
118 (?P<text>(([^\\]|\\.)*?))
117 (?P=quote))""", reppython),
119 (?P=quote))""", reppython),
118 ]
120 ]
119
121
120 cpats = [
122 cpats = [
121 (r'//', "don't use //-style comments"),
123 (r'//', "don't use //-style comments"),
122 (r'^ ', "don't use spaces to indent"),
124 (r'^ ', "don't use spaces to indent"),
123 (r'\S\t', "don't use tabs except for indent"),
125 (r'\S\t', "don't use tabs except for indent"),
124 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
126 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
125 (r'.{85}', "line too long"),
127 (r'.{85}', "line too long"),
126 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
128 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
127 (r'return\(', "return is not a function"),
129 (r'return\(', "return is not a function"),
128 (r' ;', "no space before ;"),
130 (r' ;', "no space before ;"),
129 (r'\w+\* \w+', "use int *foo, not int* foo"),
131 (r'\w+\* \w+', "use int *foo, not int* foo"),
130 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
132 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
131 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
133 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
132 (r'\w,\w', "missing whitespace after ,"),
134 (r'\w,\w', "missing whitespace after ,"),
133 (r'\w[+/*]\w', "missing whitespace in expression"),
135 (r'\w[+/*]\w', "missing whitespace in expression"),
134 (r'^#\s+\w', "use #foo, not # foo"),
136 (r'^#\s+\w', "use #foo, not # foo"),
135 (r'[^\n]\Z', "no trailing newline"),
137 (r'[^\n]\Z', "no trailing newline"),
136 ]
138 ]
137
139
138 cfilters = [
140 cfilters = [
139 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
141 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
140 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
142 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
141 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
143 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
142 (r'(\()([^)]+\))', repcallspaces),
144 (r'(\()([^)]+\))', repcallspaces),
143 ]
145 ]
144
146
145 checks = [
147 checks = [
146 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
148 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
147 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
149 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
148 ('c', r'.*\.c$', cfilters, cpats),
150 ('c', r'.*\.c$', cfilters, cpats),
149 ]
151 ]
150
152
151 class norepeatlogger(object):
153 class norepeatlogger(object):
152 def __init__(self):
154 def __init__(self):
153 self._lastseen = None
155 self._lastseen = None
154
156
155 def log(self, fname, lineno, line, msg):
157 def log(self, fname, lineno, line, msg):
156 """print error related a to given line of a given file.
158 """print error related a to given line of a given file.
157
159
158 The faulty line will also be printed but only once in the case
160 The faulty line will also be printed but only once in the case
159 of multiple errors.
161 of multiple errors.
160
162
161 :fname: filename
163 :fname: filename
162 :lineno: line number
164 :lineno: line number
163 :line: actual content of the line
165 :line: actual content of the line
164 :msg: error message
166 :msg: error message
165 """
167 """
166 msgid = fname, lineno, line
168 msgid = fname, lineno, line
167 if msgid != self._lastseen:
169 if msgid != self._lastseen:
168 print "%s:%d:" % (fname, lineno)
170 print "%s:%d:" % (fname, lineno)
169 print " > %s" % line
171 print " > %s" % line
170 self._lastseen = msgid
172 self._lastseen = msgid
171 print " " + msg
173 print " " + msg
172
174
173 _defaultlogger = norepeatlogger()
175 _defaultlogger = norepeatlogger()
174
176
175 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False):
177 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False):
176 """checks style and portability of a given file
178 """checks style and portability of a given file
177
179
178 :f: filepath
180 :f: filepath
179 :logfunc: function used to report error
181 :logfunc: function used to report error
180 logfunc(filename, linenumber, linecontent, errormessage)
182 logfunc(filename, linenumber, linecontent, errormessage)
181 :maxerr: number of error to display before arborting.
183 :maxerr: number of error to display before arborting.
182 Set to None (default) to report all errors
184 Set to None (default) to report all errors
183
185
184 return True if no error is found, False otherwise.
186 return True if no error is found, False otherwise.
185 """
187 """
186 result = True
188 result = True
187 for name, match, filters, pats in checks:
189 for name, match, filters, pats in checks:
188 fc = 0
190 fc = 0
189 if not re.match(match, f):
191 if not re.match(match, f):
190 continue
192 continue
191 pre = post = open(f).read()
193 pre = post = open(f).read()
192 if "no-" + "check-code" in pre:
194 if "no-" + "check-code" in pre:
193 break
195 break
194 for p, r in filters:
196 for p, r in filters:
195 post = re.sub(p, r, post)
197 post = re.sub(p, r, post)
196 # print post # uncomment to show filtered version
198 # print post # uncomment to show filtered version
197 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
199 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
198 for n, l in z:
200 for n, l in z:
199 if "check-code" + "-ignore" in l[0]:
201 if "check-code" + "-ignore" in l[0]:
200 continue
202 continue
201 for p, msg in pats:
203 for p, msg in pats:
202 if not warnings and msg.startswith("warning"):
204 if not warnings and msg.startswith("warning"):
203 continue
205 continue
204 if re.search(p, l[1]):
206 if re.search(p, l[1]):
205 logfunc(f, n + 1, l[0], msg)
207 logfunc(f, n + 1, l[0], msg)
206 fc += 1
208 fc += 1
207 result = False
209 result = False
208 if maxerr is not None and fc >= maxerr:
210 if maxerr is not None and fc >= maxerr:
209 print " (too many errors, giving up)"
211 print " (too many errors, giving up)"
210 break
212 break
211 break
213 break
212 return result
214 return result
213
215
214
216
215 if __name__ == "__main__":
217 if __name__ == "__main__":
216 parser = optparse.OptionParser("%prog [options] [files]")
218 parser = optparse.OptionParser("%prog [options] [files]")
217 parser.add_option("-w", "--warnings", action="store_true",
219 parser.add_option("-w", "--warnings", action="store_true",
218 help="include warning-level checks")
220 help="include warning-level checks")
219 parser.add_option("-p", "--per-file", type="int",
221 parser.add_option("-p", "--per-file", type="int",
220 help="max warnings per file")
222 help="max warnings per file")
221
223
222 parser.set_defaults(per_file=15, warnings=False)
224 parser.set_defaults(per_file=15, warnings=False)
223 (options, args) = parser.parse_args()
225 (options, args) = parser.parse_args()
224
226
225 if len(args) == 0:
227 if len(args) == 0:
226 check = glob.glob("*")
228 check = glob.glob("*")
227 else:
229 else:
228 check = args
230 check = args
229
231
230 for f in check:
232 for f in check:
231 checkfile(f, maxerr=options.per_file, warnings=options.warnings)
233 checkfile(f, maxerr=options.per_file, warnings=options.warnings)
General Comments 0
You need to be logged in to leave comments. Login now