##// END OF EJS Templates
check-code: add /= to operator list
Sune Foldager -
r14303:e2be0bba default
parent child Browse files
Show More
@@ -1,377 +1,377 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 [
46 46 (r'(pushd|popd)', "don't use 'pushd' or 'popd', use 'cd'"),
47 47 (r'\W\$?\(\([^\)]*\)\)', "don't use (()) or $(()), use 'expr'"),
48 48 (r'^function', "don't use 'function', use old style"),
49 49 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
50 50 (r'echo.*\\n', "don't use 'echo \\n', use printf"),
51 51 (r'echo -n', "don't use 'echo -n', use printf"),
52 52 (r'^diff.*-\w*N', "don't use 'diff -N'"),
53 53 (r'(^| )wc[^|]*$', "filter wc output"),
54 54 (r'head -c', "don't use 'head -c', use 'dd'"),
55 55 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
56 56 (r'printf.*\\\d\d\d', "don't use 'printf \NNN', use Python"),
57 57 (r'printf.*\\x', "don't use printf \\x, use Python"),
58 58 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
59 59 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
60 60 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
61 61 "use egrep for extended grep syntax"),
62 62 (r'/bin/', "don't use explicit paths for tools"),
63 63 (r'\$PWD', "don't use $PWD, use `pwd`"),
64 64 (r'[^\n]\Z', "no trailing newline"),
65 65 (r'export.*=', "don't export and assign at once"),
66 66 ('^([^"\']|("[^"]*")|(\'[^\']*\'))*\\^', "^ must be quoted"),
67 67 (r'^source\b', "don't use 'source', use '.'"),
68 68 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
69 69 (r'ls\s+[^|-]+\s+-', "options to 'ls' must come before filenames"),
70 70 (r'[^>]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
71 71 ],
72 72 # warnings
73 73 []
74 74 ]
75 75
76 76 testfilters = [
77 77 (r"( *)(#([^\n]*\S)?)", repcomment),
78 78 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
79 79 ]
80 80
81 81 uprefix = r"^ \$ "
82 82 uprefixc = r"^ > "
83 83 utestpats = [
84 84 [
85 85 (r'^(\S| $ ).*(\S\s+|^\s+)\n', "trailing whitespace on non-output"),
86 86 (uprefix + r'.*\|\s*sed', "use regex test output patterns instead of sed"),
87 87 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
88 88 (uprefix + r'.*\$\?', "explicit exit code checks unnecessary"),
89 89 (uprefix + r'.*\|\| echo.*(fail|error)',
90 90 "explicit exit code checks unnecessary"),
91 91 (uprefix + r'set -e', "don't use set -e"),
92 92 (uprefixc + r'( *)\t', "don't use tabs to indent"),
93 93 ],
94 94 # warnings
95 95 []
96 96 ]
97 97
98 98 for i in [0, 1]:
99 99 for p, m in testpats[i]:
100 100 if p.startswith('^'):
101 101 p = uprefix + p[1:]
102 102 else:
103 103 p = uprefix + p
104 104 utestpats[i].append((p, m))
105 105
106 106 utestfilters = [
107 107 (r"( *)(#([^\n]*\S)?)", repcomment),
108 108 ]
109 109
110 110 pypats = [
111 111 [
112 112 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
113 113 "tuple parameter unpacking not available in Python 3+"),
114 114 (r'lambda\s*\(.*,.*\)',
115 115 "tuple parameter unpacking not available in Python 3+"),
116 116 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
117 117 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
118 118 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
119 119 (r'^\s*\t', "don't use tabs"),
120 120 (r'\S;\s*\n', "semicolon"),
121 121 (r'\w,\w', "missing whitespace after ,"),
122 122 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
123 123 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
124 124 (r'.{85}', "line too long"),
125 125 (r'[^\n]\Z', "no trailing newline"),
126 126 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
127 127 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
128 128 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
129 129 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
130 130 "linebreak after :"),
131 131 (r'class\s[^(]:', "old-style class, use class foo(object)"),
132 132 (r'\b(%s)\(' % '|'.join(keyword.kwlist),
133 133 "Python keyword is not a function"),
134 134 (r',]', "unneeded trailing ',' in list"),
135 135 # (r'class\s[A-Z][^\(]*\((?!Exception)',
136 136 # "don't capitalize non-exception classes"),
137 137 # (r'in range\(', "use xrange"),
138 138 # (r'^\s*print\s+', "avoid using print in core and extensions"),
139 139 (r'[\x80-\xff]', "non-ASCII character literal"),
140 140 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
141 141 (r'^\s*with\s+', "with not available in Python 2.4"),
142 142 (r'\.isdisjoint\(', "set.isdisjoint not available in Python 2.4"),
143 143 (r'^\s*except.* as .*:', "except as not available in Python 2.4"),
144 144 (r'^\s*os\.path\.relpath', "relpath not available in Python 2.4"),
145 145 (r'(?<!def)\s+(any|all|format)\(',
146 146 "any/all/format not available in Python 2.4"),
147 147 (r'(?<!def)\s+(callable)\(',
148 148 "callable not available in Python 3, use hasattr(f, '__call__')"),
149 149 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
150 150 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
151 151 "gratuitous whitespace after Python keyword"),
152 152 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
153 153 # (r'\s\s=', "gratuitous whitespace before ="),
154 154 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
155 155 "missing whitespace around operator"),
156 156 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s',
157 157 "missing whitespace around operator"),
158 158 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
159 159 "missing whitespace around operator"),
160 (r'[^+=*!<>&| -](\s=|=\s)[^= ]',
160 (r'[^+=*/!<>&| -](\s=|=\s)[^= ]',
161 161 "wrong whitespace around ="),
162 162 (r'raise Exception', "don't raise generic exceptions"),
163 163 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
164 164 (r' [=!]=\s+(True|False|None)',
165 165 "comparison with singleton, use 'is' or 'is not' instead"),
166 166 (r'opener\([^)]*\).read\(',
167 167 "use opener.read() instead"),
168 168 (r'opener\([^)]*\).write\(',
169 169 "use opener.write() instead"),
170 170 (r'[\s\(](open|file)\([^)]*\)\.read\(',
171 171 "use util.readfile() instead"),
172 172 (r'[\s\(](open|file)\([^)]*\)\.write\(',
173 173 "use util.readfile() instead"),
174 174 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
175 175 "always assign an opened file to a variable, and close it afterwards"),
176 176 (r'[\s\(](open|file)\([^)]*\)\.',
177 177 "always assign an opened file to a variable, and close it afterwards"),
178 178 ],
179 179 # warnings
180 180 [
181 181 (r'.{81}', "warning: line over 80 characters"),
182 182 (r'^\s*except:$', "warning: naked except clause"),
183 183 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
184 184 "warning: unwrapped ui message"),
185 185 ]
186 186 ]
187 187
188 188 pyfilters = [
189 189 (r"""(?msx)(?P<comment>\#.*?$)|
190 190 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
191 191 (?P<text>(([^\\]|\\.)*?))
192 192 (?P=quote))""", reppython),
193 193 ]
194 194
195 195 cpats = [
196 196 [
197 197 (r'//', "don't use //-style comments"),
198 198 (r'^ ', "don't use spaces to indent"),
199 199 (r'\S\t', "don't use tabs except for indent"),
200 200 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
201 201 (r'.{85}', "line too long"),
202 202 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
203 203 (r'return\(', "return is not a function"),
204 204 (r' ;', "no space before ;"),
205 205 (r'\w+\* \w+', "use int *foo, not int* foo"),
206 206 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
207 207 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
208 208 (r'\w,\w', "missing whitespace after ,"),
209 209 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
210 210 (r'^#\s+\w', "use #foo, not # foo"),
211 211 (r'[^\n]\Z', "no trailing newline"),
212 212 (r'^\s*#import\b', "use only #include in standard C code"),
213 213 ],
214 214 # warnings
215 215 []
216 216 ]
217 217
218 218 cfilters = [
219 219 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
220 220 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
221 221 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
222 222 (r'(\()([^)]+\))', repcallspaces),
223 223 ]
224 224
225 225 inutilpats = [
226 226 [
227 227 (r'\bui\.', "don't use ui in util"),
228 228 ],
229 229 # warnings
230 230 []
231 231 ]
232 232
233 233 inrevlogpats = [
234 234 [
235 235 (r'\brepo\.', "don't use repo in revlog"),
236 236 ],
237 237 # warnings
238 238 []
239 239 ]
240 240
241 241 checks = [
242 242 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
243 243 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
244 244 ('c', r'.*\.c$', cfilters, cpats),
245 245 ('unified test', r'.*\.t$', utestfilters, utestpats),
246 246 ('layering violation repo in revlog', r'mercurial/revlog\.py', pyfilters,
247 247 inrevlogpats),
248 248 ('layering violation ui in util', r'mercurial/util\.py', pyfilters,
249 249 inutilpats),
250 250 ]
251 251
252 252 class norepeatlogger(object):
253 253 def __init__(self):
254 254 self._lastseen = None
255 255
256 256 def log(self, fname, lineno, line, msg, blame):
257 257 """print error related a to given line of a given file.
258 258
259 259 The faulty line will also be printed but only once in the case
260 260 of multiple errors.
261 261
262 262 :fname: filename
263 263 :lineno: line number
264 264 :line: actual content of the line
265 265 :msg: error message
266 266 """
267 267 msgid = fname, lineno, line
268 268 if msgid != self._lastseen:
269 269 if blame:
270 270 print "%s:%d (%s):" % (fname, lineno, blame)
271 271 else:
272 272 print "%s:%d:" % (fname, lineno)
273 273 print " > %s" % line
274 274 self._lastseen = msgid
275 275 print " " + msg
276 276
277 277 _defaultlogger = norepeatlogger()
278 278
279 279 def getblame(f):
280 280 lines = []
281 281 for l in os.popen('hg annotate -un %s' % f):
282 282 start, line = l.split(':', 1)
283 283 user, rev = start.split()
284 284 lines.append((line[1:-1], user, rev))
285 285 return lines
286 286
287 287 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
288 288 blame=False, debug=False):
289 289 """checks style and portability of a given file
290 290
291 291 :f: filepath
292 292 :logfunc: function used to report error
293 293 logfunc(filename, linenumber, linecontent, errormessage)
294 294 :maxerr: number of error to display before arborting.
295 295 Set to None (default) to report all errors
296 296
297 297 return True if no error is found, False otherwise.
298 298 """
299 299 blamecache = None
300 300 result = True
301 301 for name, match, filters, pats in checks:
302 302 if debug:
303 303 print name, f
304 304 fc = 0
305 305 if not re.match(match, f):
306 306 if debug:
307 307 print "Skipping %s for %s it doesn't match %s" % (
308 308 name, match, f)
309 309 continue
310 310 fp = open(f)
311 311 pre = post = fp.read()
312 312 fp.close()
313 313 if "no-" + "check-code" in pre:
314 314 if debug:
315 315 print "Skipping %s for %s it has no- and check-code" % (
316 316 name, f)
317 317 break
318 318 for p, r in filters:
319 319 post = re.sub(p, r, post)
320 320 if warnings:
321 321 pats = pats[0] + pats[1]
322 322 else:
323 323 pats = pats[0]
324 324 # print post # uncomment to show filtered version
325 325 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
326 326 if debug:
327 327 print "Checking %s for %s" % (name, f)
328 328 for n, l in z:
329 329 if "check-code" + "-ignore" in l[0]:
330 330 if debug:
331 331 print "Skipping %s for %s:%s (check-code -ignore)" % (
332 332 name, f, n)
333 333 continue
334 334 for p, msg in pats:
335 335 if re.search(p, l[1]):
336 336 bd = ""
337 337 if blame:
338 338 bd = 'working directory'
339 339 if not blamecache:
340 340 blamecache = getblame(f)
341 341 if n < len(blamecache):
342 342 bl, bu, br = blamecache[n]
343 343 if bl == l[0]:
344 344 bd = '%s@%s' % (bu, br)
345 345 logfunc(f, n + 1, l[0], msg, bd)
346 346 fc += 1
347 347 result = False
348 348 if maxerr is not None and fc >= maxerr:
349 349 print " (too many errors, giving up)"
350 350 break
351 351 return result
352 352
353 353 if __name__ == "__main__":
354 354 parser = optparse.OptionParser("%prog [options] [files]")
355 355 parser.add_option("-w", "--warnings", action="store_true",
356 356 help="include warning-level checks")
357 357 parser.add_option("-p", "--per-file", type="int",
358 358 help="max warnings per file")
359 359 parser.add_option("-b", "--blame", action="store_true",
360 360 help="use annotate to generate blame info")
361 361 parser.add_option("", "--debug", action="store_true",
362 362 help="show debug information")
363 363
364 364 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False)
365 365 (options, args) = parser.parse_args()
366 366
367 367 if len(args) == 0:
368 368 check = glob.glob("*")
369 369 else:
370 370 check = args
371 371
372 372 for f in check:
373 373 ret = 0
374 374 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
375 375 blame=options.blame, debug=options.debug):
376 376 ret = 1
377 377 sys.exit(ret)
General Comments 0
You need to be logged in to leave comments. Login now