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