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) |