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