##// END OF EJS Templates
check-code: added a check for calls to the builtin cmp function
Renato Cunha -
r11764:16723af5 default
parent child Browse files
Show More
@@ -1,264 +1,265 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
10 import re, glob, os
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*def\s*\w+\s*\(.*,\s*\(',
73 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
74 "tuple parameter unpacking not available in Python 3+"),
74 "tuple parameter unpacking not available in Python 3+"),
75 (r'lambda\s*\(.*,.*\)',
75 (r'lambda\s*\(.*,.*\)',
76 "tuple parameter unpacking not available in Python 3+"),
76 "tuple parameter unpacking not available in Python 3+"),
77 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
77 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
78 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
78 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
79 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
79 (r'^\s*\t', "don't use tabs"),
80 (r'^\s*\t', "don't use tabs"),
80 (r'\S;\s*\n', "semicolon"),
81 (r'\S;\s*\n', "semicolon"),
81 (r'\w,\w', "missing whitespace after ,"),
82 (r'\w,\w', "missing whitespace after ,"),
82 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
83 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
83 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
84 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
84 (r'.{85}', "line too long"),
85 (r'.{85}', "line too long"),
85 (r'.{81}', "warning: line over 80 characters"),
86 (r'.{81}', "warning: line over 80 characters"),
86 (r'[^\n]\Z', "no trailing newline"),
87 (r'[^\n]\Z', "no trailing newline"),
87 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
88 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
88 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
89 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
89 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
90 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
90 "linebreak after :"),
91 "linebreak after :"),
91 (r'class\s[^(]:', "old-style class, use class foo(object)"),
92 (r'class\s[^(]:', "old-style class, use class foo(object)"),
92 (r'^\s+del\(', "del isn't a function"),
93 (r'^\s+del\(', "del isn't a function"),
93 (r'^\s+except\(', "except isn't a function"),
94 (r'^\s+except\(', "except isn't a function"),
94 (r',]', "unneeded trailing ',' in list"),
95 (r',]', "unneeded trailing ',' in list"),
95 # (r'class\s[A-Z][^\(]*\((?!Exception)',
96 # (r'class\s[A-Z][^\(]*\((?!Exception)',
96 # "don't capitalize non-exception classes"),
97 # "don't capitalize non-exception classes"),
97 # (r'in range\(', "use xrange"),
98 # (r'in range\(', "use xrange"),
98 # (r'^\s*print\s+', "avoid using print in core and extensions"),
99 # (r'^\s*print\s+', "avoid using print in core and extensions"),
99 (r'[\x80-\xff]', "non-ASCII character literal"),
100 (r'[\x80-\xff]', "non-ASCII character literal"),
100 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
101 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
101 (r'^\s*with\s+', "with not available in Python 2.4"),
102 (r'^\s*with\s+', "with not available in Python 2.4"),
102 (r'(?<!def)\s+(any|all|format)\(',
103 (r'(?<!def)\s+(any|all|format)\(',
103 "any/all/format not available in Python 2.4"),
104 "any/all/format not available in Python 2.4"),
104 (r'(?<!def)\s+(callable)\(',
105 (r'(?<!def)\s+(callable)\(',
105 "callable not available in Python 3, use hasattr(f, '__call__')"),
106 "callable not available in Python 3, use hasattr(f, '__call__')"),
106 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
107 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
107 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
108 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
108 # (r'\s\s=', "gratuitous whitespace before ="),
109 # (r'\s\s=', "gratuitous whitespace before ="),
109 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
110 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
110 "missing whitespace around operator"),
111 "missing whitespace around operator"),
111 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s',
112 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s',
112 "missing whitespace around operator"),
113 "missing whitespace around operator"),
113 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
114 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
114 "missing whitespace around operator"),
115 "missing whitespace around operator"),
115 (r'[^+=*!<>&| -](\s=|=\s)[^= ]',
116 (r'[^+=*!<>&| -](\s=|=\s)[^= ]',
116 "wrong whitespace around ="),
117 "wrong whitespace around ="),
117 (r'raise Exception', "don't raise generic exceptions"),
118 (r'raise Exception', "don't raise generic exceptions"),
118 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
119 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
119 "warning: unwrapped ui message"),
120 "warning: unwrapped ui message"),
120 ]
121 ]
121
122
122 pyfilters = [
123 pyfilters = [
123 (r"""(?msx)(?P<comment>\#.*?$)|
124 (r"""(?msx)(?P<comment>\#.*?$)|
124 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
125 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
125 (?P<text>(([^\\]|\\.)*?))
126 (?P<text>(([^\\]|\\.)*?))
126 (?P=quote))""", reppython),
127 (?P=quote))""", reppython),
127 ]
128 ]
128
129
129 cpats = [
130 cpats = [
130 (r'//', "don't use //-style comments"),
131 (r'//', "don't use //-style comments"),
131 (r'^ ', "don't use spaces to indent"),
132 (r'^ ', "don't use spaces to indent"),
132 (r'\S\t', "don't use tabs except for indent"),
133 (r'\S\t', "don't use tabs except for indent"),
133 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
134 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
134 (r'.{85}', "line too long"),
135 (r'.{85}', "line too long"),
135 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
136 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
136 (r'return\(', "return is not a function"),
137 (r'return\(', "return is not a function"),
137 (r' ;', "no space before ;"),
138 (r' ;', "no space before ;"),
138 (r'\w+\* \w+', "use int *foo, not int* foo"),
139 (r'\w+\* \w+', "use int *foo, not int* foo"),
139 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
140 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
140 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
141 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
141 (r'\w,\w', "missing whitespace after ,"),
142 (r'\w,\w', "missing whitespace after ,"),
142 (r'\w[+/*]\w', "missing whitespace in expression"),
143 (r'\w[+/*]\w', "missing whitespace in expression"),
143 (r'^#\s+\w', "use #foo, not # foo"),
144 (r'^#\s+\w', "use #foo, not # foo"),
144 (r'[^\n]\Z', "no trailing newline"),
145 (r'[^\n]\Z', "no trailing newline"),
145 ]
146 ]
146
147
147 cfilters = [
148 cfilters = [
148 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
149 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
149 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
150 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
150 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
151 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
151 (r'(\()([^)]+\))', repcallspaces),
152 (r'(\()([^)]+\))', repcallspaces),
152 ]
153 ]
153
154
154 checks = [
155 checks = [
155 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
156 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
156 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
157 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
157 ('c', r'.*\.c$', cfilters, cpats),
158 ('c', r'.*\.c$', cfilters, cpats),
158 ]
159 ]
159
160
160 class norepeatlogger(object):
161 class norepeatlogger(object):
161 def __init__(self):
162 def __init__(self):
162 self._lastseen = None
163 self._lastseen = None
163
164
164 def log(self, fname, lineno, line, msg, blame):
165 def log(self, fname, lineno, line, msg, blame):
165 """print error related a to given line of a given file.
166 """print error related a to given line of a given file.
166
167
167 The faulty line will also be printed but only once in the case
168 The faulty line will also be printed but only once in the case
168 of multiple errors.
169 of multiple errors.
169
170
170 :fname: filename
171 :fname: filename
171 :lineno: line number
172 :lineno: line number
172 :line: actual content of the line
173 :line: actual content of the line
173 :msg: error message
174 :msg: error message
174 """
175 """
175 msgid = fname, lineno, line
176 msgid = fname, lineno, line
176 if msgid != self._lastseen:
177 if msgid != self._lastseen:
177 if blame:
178 if blame:
178 print "%s:%d (%s):" % (fname, lineno, blame)
179 print "%s:%d (%s):" % (fname, lineno, blame)
179 else:
180 else:
180 print "%s:%d:" % (fname, lineno)
181 print "%s:%d:" % (fname, lineno)
181 print " > %s" % line
182 print " > %s" % line
182 self._lastseen = msgid
183 self._lastseen = msgid
183 print " " + msg
184 print " " + msg
184
185
185 _defaultlogger = norepeatlogger()
186 _defaultlogger = norepeatlogger()
186
187
187 def getblame(f):
188 def getblame(f):
188 lines = []
189 lines = []
189 for l in os.popen('hg annotate -un %s' % f):
190 for l in os.popen('hg annotate -un %s' % f):
190 start, line = l.split(':', 1)
191 start, line = l.split(':', 1)
191 user, rev = start.split()
192 user, rev = start.split()
192 lines.append((line[1:-1], user, rev))
193 lines.append((line[1:-1], user, rev))
193 return lines
194 return lines
194
195
195 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
196 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
196 blame=False):
197 blame=False):
197 """checks style and portability of a given file
198 """checks style and portability of a given file
198
199
199 :f: filepath
200 :f: filepath
200 :logfunc: function used to report error
201 :logfunc: function used to report error
201 logfunc(filename, linenumber, linecontent, errormessage)
202 logfunc(filename, linenumber, linecontent, errormessage)
202 :maxerr: number of error to display before arborting.
203 :maxerr: number of error to display before arborting.
203 Set to None (default) to report all errors
204 Set to None (default) to report all errors
204
205
205 return True if no error is found, False otherwise.
206 return True if no error is found, False otherwise.
206 """
207 """
207 blamecache = None
208 blamecache = None
208 result = True
209 result = True
209 for name, match, filters, pats in checks:
210 for name, match, filters, pats in checks:
210 fc = 0
211 fc = 0
211 if not re.match(match, f):
212 if not re.match(match, f):
212 continue
213 continue
213 pre = post = open(f).read()
214 pre = post = open(f).read()
214 if "no-" + "check-code" in pre:
215 if "no-" + "check-code" in pre:
215 break
216 break
216 for p, r in filters:
217 for p, r in filters:
217 post = re.sub(p, r, post)
218 post = re.sub(p, r, post)
218 # print post # uncomment to show filtered version
219 # print post # uncomment to show filtered version
219 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
220 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
220 for n, l in z:
221 for n, l in z:
221 if "check-code" + "-ignore" in l[0]:
222 if "check-code" + "-ignore" in l[0]:
222 continue
223 continue
223 for p, msg in pats:
224 for p, msg in pats:
224 if not warnings and msg.startswith("warning"):
225 if not warnings and msg.startswith("warning"):
225 continue
226 continue
226 if re.search(p, l[1]):
227 if re.search(p, l[1]):
227 bd = ""
228 bd = ""
228 if blame:
229 if blame:
229 bd = 'working directory'
230 bd = 'working directory'
230 if not blamecache:
231 if not blamecache:
231 blamecache = getblame(f)
232 blamecache = getblame(f)
232 if n < len(blamecache):
233 if n < len(blamecache):
233 bl, bu, br = blamecache[n]
234 bl, bu, br = blamecache[n]
234 if bl == l[0]:
235 if bl == l[0]:
235 bd = '%s@%s' % (bu, br)
236 bd = '%s@%s' % (bu, br)
236 logfunc(f, n + 1, l[0], msg, bd)
237 logfunc(f, n + 1, l[0], msg, bd)
237 fc += 1
238 fc += 1
238 result = False
239 result = False
239 if maxerr is not None and fc >= maxerr:
240 if maxerr is not None and fc >= maxerr:
240 print " (too many errors, giving up)"
241 print " (too many errors, giving up)"
241 break
242 break
242 break
243 break
243 return result
244 return result
244
245
245 if __name__ == "__main__":
246 if __name__ == "__main__":
246 parser = optparse.OptionParser("%prog [options] [files]")
247 parser = optparse.OptionParser("%prog [options] [files]")
247 parser.add_option("-w", "--warnings", action="store_true",
248 parser.add_option("-w", "--warnings", action="store_true",
248 help="include warning-level checks")
249 help="include warning-level checks")
249 parser.add_option("-p", "--per-file", type="int",
250 parser.add_option("-p", "--per-file", type="int",
250 help="max warnings per file")
251 help="max warnings per file")
251 parser.add_option("-b", "--blame", action="store_true",
252 parser.add_option("-b", "--blame", action="store_true",
252 help="use annotate to generate blame info")
253 help="use annotate to generate blame info")
253
254
254 parser.set_defaults(per_file=15, warnings=False, blame=False)
255 parser.set_defaults(per_file=15, warnings=False, blame=False)
255 (options, args) = parser.parse_args()
256 (options, args) = parser.parse_args()
256
257
257 if len(args) == 0:
258 if len(args) == 0:
258 check = glob.glob("*")
259 check = glob.glob("*")
259 else:
260 else:
260 check = args
261 check = args
261
262
262 for f in check:
263 for f in check:
263 checkfile(f, maxerr=options.per_file, warnings=options.warnings,
264 checkfile(f, maxerr=options.per_file, warnings=options.warnings,
264 blame=options.blame)
265 blame=options.blame)
General Comments 0
You need to be logged in to leave comments. Login now