comparison contrib/check-code.py @ 11604:c5d40818b270

check-code: add --blame switch
author Matt Mackall <mpm@selenic.com>
date Fri, 16 Jul 2010 13:26:39 -0500
parents ba2520dd1e29
children dad185761392
comparison
equal deleted inserted replaced
11603:2eab5025f804 11604:c5d40818b270
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 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)
158 158
159 class norepeatlogger(object): 159 class norepeatlogger(object):
160 def __init__(self): 160 def __init__(self):
161 self._lastseen = None 161 self._lastseen = None
162 162
163 def log(self, fname, lineno, line, msg): 163 def log(self, fname, lineno, line, msg, blame):
164 """print error related a to given line of a given file. 164 """print error related a to given line of a given file.
165 165
166 The faulty line will also be printed but only once in the case 166 The faulty line will also be printed but only once in the case
167 of multiple errors. 167 of multiple errors.
168 168
171 :line: actual content of the line 171 :line: actual content of the line
172 :msg: error message 172 :msg: error message
173 """ 173 """
174 msgid = fname, lineno, line 174 msgid = fname, lineno, line
175 if msgid != self._lastseen: 175 if msgid != self._lastseen:
176 print "%s:%d:" % (fname, lineno) 176 if blame:
177 print "%s:%d (%s):" % (fname, lineno, blame)
178 else:
179 print "%s:%d:" % (fname, lineno)
177 print " > %s" % line 180 print " > %s" % line
178 self._lastseen = msgid 181 self._lastseen = msgid
179 print " " + msg 182 print " " + msg
180 183
181 _defaultlogger = norepeatlogger() 184 _defaultlogger = norepeatlogger()
182 185
183 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False): 186 def getblame(f):
187 lines = []
188 for l in os.popen('hg annotate -un %s' % f):
189 start, line = l.split(':', 1)
190 user, rev = start.split()
191 lines.append((line[1:-1], user, rev))
192 return lines
193
194 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
195 blame=False):
184 """checks style and portability of a given file 196 """checks style and portability of a given file
185 197
186 :f: filepath 198 :f: filepath
187 :logfunc: function used to report error 199 :logfunc: function used to report error
188 logfunc(filename, linenumber, linecontent, errormessage) 200 logfunc(filename, linenumber, linecontent, errormessage)
189 :maxerr: number of error to display before arborting. 201 :maxerr: number of error to display before arborting.
190 Set to None (default) to report all errors 202 Set to None (default) to report all errors
191 203
192 return True if no error is found, False otherwise. 204 return True if no error is found, False otherwise.
193 """ 205 """
206 blamecache = None
194 result = True 207 result = True
195 for name, match, filters, pats in checks: 208 for name, match, filters, pats in checks:
196 fc = 0 209 fc = 0
197 if not re.match(match, f): 210 if not re.match(match, f):
198 continue 211 continue
208 continue 221 continue
209 for p, msg in pats: 222 for p, msg in pats:
210 if not warnings and msg.startswith("warning"): 223 if not warnings and msg.startswith("warning"):
211 continue 224 continue
212 if re.search(p, l[1]): 225 if re.search(p, l[1]):
213 logfunc(f, n + 1, l[0], msg) 226 bd = ""
227 if blame:
228 bd = 'working directory'
229 if not blamecache:
230 blamecache = getblame(f)
231 if n < len(blamecache):
232 bl, bu, br = blamecache[n]
233 if bl == l[0]:
234 bd = '%s@%s' % (bu, br)
235 logfunc(f, n + 1, l[0], msg, bd)
214 fc += 1 236 fc += 1
215 result = False 237 result = False
216 if maxerr is not None and fc >= maxerr: 238 if maxerr is not None and fc >= maxerr:
217 print " (too many errors, giving up)" 239 print " (too many errors, giving up)"
218 break 240 break
219 break 241 break
220 return result 242 return result
221 243
222
223 if __name__ == "__main__": 244 if __name__ == "__main__":
224 parser = optparse.OptionParser("%prog [options] [files]") 245 parser = optparse.OptionParser("%prog [options] [files]")
225 parser.add_option("-w", "--warnings", action="store_true", 246 parser.add_option("-w", "--warnings", action="store_true",
226 help="include warning-level checks") 247 help="include warning-level checks")
227 parser.add_option("-p", "--per-file", type="int", 248 parser.add_option("-p", "--per-file", type="int",
228 help="max warnings per file") 249 help="max warnings per file")
229 250 parser.add_option("-b", "--blame", action="store_true",
230 parser.set_defaults(per_file=15, warnings=False) 251 help="use annotate to generate blame info")
252
253 parser.set_defaults(per_file=15, warnings=False, blame=False)
231 (options, args) = parser.parse_args() 254 (options, args) = parser.parse_args()
232 255
233 if len(args) == 0: 256 if len(args) == 0:
234 check = glob.glob("*") 257 check = glob.glob("*")
235 else: 258 else:
236 check = args 259 check = args
237 260
238 for f in check: 261 for f in check:
239 checkfile(f, maxerr=options.per_file, warnings=options.warnings) 262 checkfile(f, maxerr=options.per_file, warnings=options.warnings,
263 blame=options.blame)