##// END OF EJS Templates
check-code: single check for Python keywords used as a function...
Thomas Arendsen Hein -
r13076:a861c715 default
parent child Browse files
Show More
@@ -1,306 +1,303 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # check-code - a style and portability checker for Mercurial
4 4 #
5 5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 import re, glob, os, sys
11 11 import keyword
12 12 import optparse
13 13
14 14 def repquote(m):
15 15 t = re.sub(r"\w", "x", m.group('text'))
16 16 t = re.sub(r"[^\sx]", "o", t)
17 17 return m.group('quote') + t + m.group('quote')
18 18
19 19 def reppython(m):
20 20 comment = m.group('comment')
21 21 if comment:
22 22 return "#" * len(comment)
23 23 return repquote(m)
24 24
25 25 def repcomment(m):
26 26 return m.group(1) + "#" * len(m.group(2))
27 27
28 28 def repccomment(m):
29 29 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
30 30 return m.group(1) + t + "*/"
31 31
32 32 def repcallspaces(m):
33 33 t = re.sub(r"\n\s+", "\n", m.group(2))
34 34 return m.group(1) + t
35 35
36 36 def repinclude(m):
37 37 return m.group(1) + "<foo>"
38 38
39 39 def rephere(m):
40 40 t = re.sub(r"\S", "x", m.group(2))
41 41 return m.group(1) + t
42 42
43 43
44 44 testpats = [
45 45 (r'(pushd|popd)', "don't use 'pushd' or 'popd', use 'cd'"),
46 46 (r'\W\$?\(\([^\)]*\)\)', "don't use (()) or $(()), use 'expr'"),
47 47 (r'^function', "don't use 'function', use old style"),
48 48 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
49 49 (r'echo.*\\n', "don't use 'echo \\n', use printf"),
50 50 (r'echo -n', "don't use 'echo -n', use printf"),
51 51 (r'^diff.*-\w*N', "don't use 'diff -N'"),
52 52 (r'(^| )wc[^|]*$', "filter wc output"),
53 53 (r'head -c', "don't use 'head -c', use 'dd'"),
54 54 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
55 55 (r'printf.*\\\d\d\d', "don't use 'printf \NNN', use Python"),
56 56 (r'printf.*\\x', "don't use printf \\x, use Python"),
57 57 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
58 58 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
59 59 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
60 60 "use egrep for extended grep syntax"),
61 61 (r'/bin/', "don't use explicit paths for tools"),
62 62 (r'\$PWD', "don't use $PWD, use `pwd`"),
63 63 (r'[^\n]\Z', "no trailing newline"),
64 64 (r'export.*=', "don't export and assign at once"),
65 65 ('^([^"\']|("[^"]*")|(\'[^\']*\'))*\\^', "^ must be quoted"),
66 66 (r'^source\b', "don't use 'source', use '.'"),
67 67 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
68 68 (r'ls\s+[^-]+\s+-', "options to 'ls' must come before filenames"),
69 69 ]
70 70
71 71 testfilters = [
72 72 (r"( *)(#([^\n]*\S)?)", repcomment),
73 73 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
74 74 ]
75 75
76 76 uprefix = r"^ \$ "
77 77 uprefixc = r"^ > "
78 78 utestpats = [
79 79 (r'^(\S| $ ).*(\S\s+|^\s+)\n', "trailing whitespace on non-output"),
80 80 (uprefix + r'.*\|\s*sed', "use regex test output patterns instead of sed"),
81 81 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
82 82 (uprefix + r'.*\$\?', "explicit exit code checks unnecessary"),
83 83 (uprefix + r'.*\|\| echo.*(fail|error)',
84 84 "explicit exit code checks unnecessary"),
85 85 (uprefix + r'set -e', "don't use set -e"),
86 86 (uprefixc + r'( *)\t', "don't use tabs to indent"),
87 87 ]
88 88
89 89 for p, m in testpats:
90 90 if p.startswith('^'):
91 91 p = uprefix + p[1:]
92 92 else:
93 93 p = uprefix + p
94 94 utestpats.append((p, m))
95 95
96 96 utestfilters = [
97 97 (r"( *)(#([^\n]*\S)?)", repcomment),
98 98 ]
99 99
100 100 pypats = [
101 101 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
102 102 "tuple parameter unpacking not available in Python 3+"),
103 103 (r'lambda\s*\(.*,.*\)',
104 104 "tuple parameter unpacking not available in Python 3+"),
105 105 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
106 106 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
107 107 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
108 108 (r'^\s*\t', "don't use tabs"),
109 109 (r'\S;\s*\n', "semicolon"),
110 110 (r'\w,\w', "missing whitespace after ,"),
111 111 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
112 112 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
113 113 (r'.{85}', "line too long"),
114 114 (r'.{81}', "warning: line over 80 characters"),
115 115 (r'[^\n]\Z', "no trailing newline"),
116 116 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
117 117 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
118 118 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
119 119 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
120 120 "linebreak after :"),
121 121 (r'class\s[^(]:', "old-style class, use class foo(object)"),
122 (r'^\s+del\(', "del isn't a function"),
123 (r'\band\(', "and isn't a function"),
124 (r'\bor\(', "or isn't a function"),
125 (r'\bnot\(', "not isn't a function"),
126 (r'^\s+except\(', "except isn't a function"),
122 (r'\b(%s)\(' % '|'.join(keyword.kwlist),
123 "Python keyword is not a function"),
127 124 (r',]', "unneeded trailing ',' in list"),
128 125 # (r'class\s[A-Z][^\(]*\((?!Exception)',
129 126 # "don't capitalize non-exception classes"),
130 127 # (r'in range\(', "use xrange"),
131 128 # (r'^\s*print\s+', "avoid using print in core and extensions"),
132 129 (r'[\x80-\xff]', "non-ASCII character literal"),
133 130 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
134 131 (r'^\s*with\s+', "with not available in Python 2.4"),
135 132 (r'(?<!def)\s+(any|all|format)\(',
136 133 "any/all/format not available in Python 2.4"),
137 134 (r'(?<!def)\s+(callable)\(',
138 135 "callable not available in Python 3, use hasattr(f, '__call__')"),
139 136 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
140 137 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
141 138 "gratuitous whitespace after Python keyword"),
142 139 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
143 140 # (r'\s\s=', "gratuitous whitespace before ="),
144 141 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
145 142 "missing whitespace around operator"),
146 143 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s',
147 144 "missing whitespace around operator"),
148 145 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
149 146 "missing whitespace around operator"),
150 147 (r'[^+=*!<>&| -](\s=|=\s)[^= ]',
151 148 "wrong whitespace around ="),
152 149 (r'raise Exception', "don't raise generic exceptions"),
153 150 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
154 151 "warning: unwrapped ui message"),
155 152 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
156 153 (r' [=!]=\s+(True|False|None)',
157 154 "comparison with singleton, use 'is' or 'is not' instead"),
158 155 ]
159 156
160 157 pyfilters = [
161 158 (r"""(?msx)(?P<comment>\#.*?$)|
162 159 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
163 160 (?P<text>(([^\\]|\\.)*?))
164 161 (?P=quote))""", reppython),
165 162 ]
166 163
167 164 cpats = [
168 165 (r'//', "don't use //-style comments"),
169 166 (r'^ ', "don't use spaces to indent"),
170 167 (r'\S\t', "don't use tabs except for indent"),
171 168 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
172 169 (r'.{85}', "line too long"),
173 170 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
174 171 (r'return\(', "return is not a function"),
175 172 (r' ;', "no space before ;"),
176 173 (r'\w+\* \w+', "use int *foo, not int* foo"),
177 174 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
178 175 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
179 176 (r'\w,\w', "missing whitespace after ,"),
180 177 (r'\w[+/*]\w', "missing whitespace in expression"),
181 178 (r'^#\s+\w', "use #foo, not # foo"),
182 179 (r'[^\n]\Z', "no trailing newline"),
183 180 ]
184 181
185 182 cfilters = [
186 183 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
187 184 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
188 185 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
189 186 (r'(\()([^)]+\))', repcallspaces),
190 187 ]
191 188
192 189 checks = [
193 190 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
194 191 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
195 192 ('c', r'.*\.c$', cfilters, cpats),
196 193 ('unified test', r'.*\.t$', utestfilters, utestpats),
197 194 ]
198 195
199 196 class norepeatlogger(object):
200 197 def __init__(self):
201 198 self._lastseen = None
202 199
203 200 def log(self, fname, lineno, line, msg, blame):
204 201 """print error related a to given line of a given file.
205 202
206 203 The faulty line will also be printed but only once in the case
207 204 of multiple errors.
208 205
209 206 :fname: filename
210 207 :lineno: line number
211 208 :line: actual content of the line
212 209 :msg: error message
213 210 """
214 211 msgid = fname, lineno, line
215 212 if msgid != self._lastseen:
216 213 if blame:
217 214 print "%s:%d (%s):" % (fname, lineno, blame)
218 215 else:
219 216 print "%s:%d:" % (fname, lineno)
220 217 print " > %s" % line
221 218 self._lastseen = msgid
222 219 print " " + msg
223 220
224 221 _defaultlogger = norepeatlogger()
225 222
226 223 def getblame(f):
227 224 lines = []
228 225 for l in os.popen('hg annotate -un %s' % f):
229 226 start, line = l.split(':', 1)
230 227 user, rev = start.split()
231 228 lines.append((line[1:-1], user, rev))
232 229 return lines
233 230
234 231 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
235 232 blame=False):
236 233 """checks style and portability of a given file
237 234
238 235 :f: filepath
239 236 :logfunc: function used to report error
240 237 logfunc(filename, linenumber, linecontent, errormessage)
241 238 :maxerr: number of error to display before arborting.
242 239 Set to None (default) to report all errors
243 240
244 241 return True if no error is found, False otherwise.
245 242 """
246 243 blamecache = None
247 244 result = True
248 245 for name, match, filters, pats in checks:
249 246 fc = 0
250 247 if not re.match(match, f):
251 248 continue
252 249 pre = post = open(f).read()
253 250 if "no-" + "check-code" in pre:
254 251 break
255 252 for p, r in filters:
256 253 post = re.sub(p, r, post)
257 254 # print post # uncomment to show filtered version
258 255 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
259 256 for n, l in z:
260 257 if "check-code" + "-ignore" in l[0]:
261 258 continue
262 259 for p, msg in pats:
263 260 if not warnings and msg.startswith("warning"):
264 261 continue
265 262 if re.search(p, l[1]):
266 263 bd = ""
267 264 if blame:
268 265 bd = 'working directory'
269 266 if not blamecache:
270 267 blamecache = getblame(f)
271 268 if n < len(blamecache):
272 269 bl, bu, br = blamecache[n]
273 270 if bl == l[0]:
274 271 bd = '%s@%s' % (bu, br)
275 272 logfunc(f, n + 1, l[0], msg, bd)
276 273 fc += 1
277 274 result = False
278 275 if maxerr is not None and fc >= maxerr:
279 276 print " (too many errors, giving up)"
280 277 break
281 278 break
282 279 return result
283 280
284 281 if __name__ == "__main__":
285 282 parser = optparse.OptionParser("%prog [options] [files]")
286 283 parser.add_option("-w", "--warnings", action="store_true",
287 284 help="include warning-level checks")
288 285 parser.add_option("-p", "--per-file", type="int",
289 286 help="max warnings per file")
290 287 parser.add_option("-b", "--blame", action="store_true",
291 288 help="use annotate to generate blame info")
292 289
293 290 parser.set_defaults(per_file=15, warnings=False, blame=False)
294 291 (options, args) = parser.parse_args()
295 292
296 293 if len(args) == 0:
297 294 check = glob.glob("*")
298 295 else:
299 296 check = args
300 297
301 298 for f in check:
302 299 ret = 0
303 300 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
304 301 blame=options.blame):
305 302 ret = 1
306 303 sys.exit(ret)
General Comments 0
You need to be logged in to leave comments. Login now