OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2003-2010 Sylvain Thenault (thenault@gmail.com). |
| 2 # Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE). |
| 3 # http://www.logilab.fr/ -- mailto:contact@logilab.fr |
| 4 # |
| 5 # This program is free software; you can redistribute it and/or modify it under |
| 6 # the terms of the GNU General Public License as published by the Free Software |
| 7 # Foundation; either version 2 of the License, or (at your option) any later |
| 8 # version. |
| 9 # |
| 10 # This program is distributed in the hope that it will be useful, but WITHOUT |
| 11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| 12 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details |
| 13 # |
| 14 # You should have received a copy of the GNU General Public License along with |
| 15 # this program; if not, write to the Free Software Foundation, Inc., |
| 16 # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
| 17 """some various utilities and helper classes, most of them used in the |
| 18 main pylint class |
| 19 """ |
| 20 |
| 21 import sys |
| 22 from os import linesep |
| 23 from os.path import dirname, basename, splitext, exists, isdir, join, normpath |
| 24 |
| 25 from logilab.common.modutils import modpath_from_file, get_module_files, \ |
| 26 file_from_modpath |
| 27 from logilab.common.textutils import normalize_text |
| 28 from logilab.common.configuration import rest_format_section |
| 29 from logilab.common.ureports import Section |
| 30 |
| 31 from logilab.astng import nodes, Module |
| 32 |
| 33 from pylint.checkers import EmptyReport |
| 34 |
| 35 |
| 36 class UnknownMessage(Exception): |
| 37 """raised when a unregistered message id is encountered""" |
| 38 |
| 39 |
| 40 MSG_TYPES = { |
| 41 'I' : 'info', |
| 42 'C' : 'convention', |
| 43 'R' : 'refactor', |
| 44 'W' : 'warning', |
| 45 'E' : 'error', |
| 46 'F' : 'fatal' |
| 47 } |
| 48 MSG_TYPES_LONG = dict([(v, k) for k, v in MSG_TYPES.iteritems()]) |
| 49 |
| 50 MSG_TYPES_STATUS = { |
| 51 'I' : 0, |
| 52 'C' : 16, |
| 53 'R' : 8, |
| 54 'W' : 4, |
| 55 'E' : 2, |
| 56 'F' : 1 |
| 57 } |
| 58 |
| 59 _MSG_ORDER = 'EWRCIF' |
| 60 |
| 61 def sort_msgs(msgids): |
| 62 """sort message identifiers according to their category first""" |
| 63 msgs = {} |
| 64 for msg in msgids: |
| 65 msgs.setdefault(msg[0], []).append(msg) |
| 66 result = [] |
| 67 for m_id in _MSG_ORDER: |
| 68 if m_id in msgs: |
| 69 result.extend( sorted(msgs[m_id]) ) |
| 70 return result |
| 71 |
| 72 def get_module_and_frameid(node): |
| 73 """return the module name and the frame id in the module""" |
| 74 frame = node.frame() |
| 75 module, obj = '', [] |
| 76 while frame: |
| 77 if isinstance(frame, Module): |
| 78 module = frame.name |
| 79 else: |
| 80 obj.append(getattr(frame, 'name', '<lambda>')) |
| 81 try: |
| 82 frame = frame.parent.frame() |
| 83 except AttributeError: |
| 84 frame = None |
| 85 obj.reverse() |
| 86 return module, '.'.join(obj) |
| 87 |
| 88 def category_id(id): |
| 89 id = id.upper() |
| 90 if id in MSG_TYPES: |
| 91 return id |
| 92 return MSG_TYPES_LONG.get(id) |
| 93 |
| 94 |
| 95 class Message: |
| 96 def __init__(self, checker, msgid, msg, descr): |
| 97 assert len(msgid) == 5, 'Invalid message id %s' % msgid |
| 98 assert msgid[0] in MSG_TYPES, \ |
| 99 'Bad message type %s in %r' % (msgid[0], msgid) |
| 100 self.msgid = msgid |
| 101 self.msg = msg |
| 102 self.descr = descr |
| 103 self.checker = checker |
| 104 |
| 105 class MessagesHandlerMixIn: |
| 106 """a mix-in class containing all the messages related methods for the main |
| 107 lint class |
| 108 """ |
| 109 |
| 110 def __init__(self): |
| 111 # dictionary of registered messages |
| 112 self._messages = {} |
| 113 self._msgs_state = {} |
| 114 self._module_msgs_state = {} # None |
| 115 self._msgs_by_category = {} |
| 116 self.msg_status = 0 |
| 117 |
| 118 def register_messages(self, checker): |
| 119 """register a dictionary of messages |
| 120 |
| 121 Keys are message ids, values are a 2-uple with the message type and the |
| 122 message itself |
| 123 |
| 124 message ids should be a string of len 4, where the two first characters |
| 125 are the checker id and the two last the message id in this checker |
| 126 """ |
| 127 msgs_dict = checker.msgs |
| 128 chkid = None |
| 129 for msgid, (msg, msgdescr) in msgs_dict.items(): |
| 130 # avoid duplicate / malformed ids |
| 131 assert msgid not in self._messages, \ |
| 132 'Message id %r is already defined' % msgid |
| 133 assert chkid is None or chkid == msgid[1:3], \ |
| 134 'Inconsistent checker part in message id %r' % msgid |
| 135 chkid = msgid[1:3] |
| 136 self._messages[msgid] = Message(checker, msgid, msg, msgdescr) |
| 137 self._msgs_by_category.setdefault(msgid[0], []).append(msgid) |
| 138 |
| 139 def get_message_help(self, msgid, checkerref=False): |
| 140 """return the help string for the given message id""" |
| 141 msg = self.check_message_id(msgid) |
| 142 desc = normalize_text(' '.join(msg.descr.split()), indent=' ') |
| 143 if checkerref: |
| 144 desc += ' This message belongs to the %s checker.' % \ |
| 145 msg.checker.name |
| 146 title = msg.msg |
| 147 if title != '%s': |
| 148 title = title.splitlines()[0] |
| 149 return ':%s: *%s*\n%s' % (msg.msgid, title, desc) |
| 150 return ':%s:\n%s' % (msg.msgid, desc) |
| 151 |
| 152 def disable(self, msgid, scope='package', line=None): |
| 153 """don't output message of the given id""" |
| 154 assert scope in ('package', 'module') |
| 155 # msgid is a category? |
| 156 catid = category_id(msgid) |
| 157 if catid is not None: |
| 158 for msgid in self._msgs_by_category.get(catid): |
| 159 self.disable(msgid, scope, line) |
| 160 return |
| 161 # msgid is a checker name? |
| 162 if msgid.lower() in self._checkers: |
| 163 for checker in self._checkers[msgid.lower()]: |
| 164 for msgid in checker.msgs: |
| 165 self.disable(msgid, scope, line) |
| 166 return |
| 167 # msgid is report id? |
| 168 if msgid.lower().startswith('rp'): |
| 169 self.disable_report(msgid) |
| 170 return |
| 171 # msgid is a msgid. |
| 172 msg = self.check_message_id(msgid) |
| 173 if scope == 'module': |
| 174 assert line > 0 |
| 175 try: |
| 176 self._module_msgs_state[msg.msgid][line] = False |
| 177 except KeyError: |
| 178 self._module_msgs_state[msg.msgid] = {line: False} |
| 179 if msgid != 'I0011': |
| 180 self.add_message('I0011', line=line, args=msg.msgid) |
| 181 |
| 182 else: |
| 183 msgs = self._msgs_state |
| 184 msgs[msg.msgid] = False |
| 185 # sync configuration object |
| 186 self.config.disable_msg = [mid for mid, val in msgs.items() |
| 187 if not val] |
| 188 |
| 189 def enable(self, msgid, scope='package', line=None): |
| 190 """reenable message of the given id""" |
| 191 assert scope in ('package', 'module') |
| 192 catid = category_id(msgid) |
| 193 # msgid is a category? |
| 194 if catid is not None: |
| 195 for msgid in self._msgs_by_category.get(catid): |
| 196 self.enable(msgid, scope, line) |
| 197 return |
| 198 # msgid is a checker name? |
| 199 if msgid.lower() in self._checkers: |
| 200 for checker in self._checkers[msgid.lower()]: |
| 201 for msgid in checker.msgs: |
| 202 self.enable(msgid, scope, line) |
| 203 return |
| 204 # msgid is report id? |
| 205 if msgid.lower().startswith('rp'): |
| 206 self.enable_report(msgid) |
| 207 return |
| 208 # msgid is a msgid. |
| 209 msg = self.check_message_id(msgid) |
| 210 if scope == 'module': |
| 211 assert line > 0 |
| 212 try: |
| 213 self._module_msgs_state[msg.msgid][line] = True |
| 214 except KeyError: |
| 215 self._module_msgs_state[msg.msgid] = {line: True} |
| 216 self.add_message('I0012', line=line, args=msg.msgid) |
| 217 else: |
| 218 msgs = self._msgs_state |
| 219 msgs[msg.msgid] = True |
| 220 # sync configuration object |
| 221 self.config.enable = [mid for mid, val in msgs.items() if val] |
| 222 |
| 223 def check_message_id(self, msgid): |
| 224 """raise UnknownMessage if the message id is not defined""" |
| 225 msgid = msgid.upper() |
| 226 try: |
| 227 return self._messages[msgid] |
| 228 except KeyError: |
| 229 raise UnknownMessage('No such message id %s' % msgid) |
| 230 |
| 231 def is_message_enabled(self, msgid, line=None): |
| 232 """return true if the message associated to the given message id is |
| 233 enabled |
| 234 """ |
| 235 if line is None: |
| 236 return self._msgs_state.get(msgid, True) |
| 237 try: |
| 238 return self._module_msgs_state[msgid][line] |
| 239 except (KeyError, TypeError): |
| 240 return self._msgs_state.get(msgid, True) |
| 241 |
| 242 def add_message(self, msgid, line=None, node=None, args=None): |
| 243 """add the message corresponding to the given id. |
| 244 |
| 245 If provided, msg is expanded using args |
| 246 |
| 247 astng checkers should provide the node argument, raw checkers should |
| 248 provide the line argument. |
| 249 """ |
| 250 if line is None and node is not None: |
| 251 line = node.fromlineno |
| 252 if hasattr(node, 'col_offset'): |
| 253 col_offset = node.col_offset # XXX measured in bytes for utf-8, divi
de by two for chars? |
| 254 else: |
| 255 col_offset = None |
| 256 # should this message be displayed |
| 257 if not self.is_message_enabled(msgid, line): |
| 258 return |
| 259 # update stats |
| 260 msg_cat = MSG_TYPES[msgid[0]] |
| 261 self.msg_status |= MSG_TYPES_STATUS[msgid[0]] |
| 262 self.stats[msg_cat] += 1 |
| 263 self.stats['by_module'][self.current_name][msg_cat] += 1 |
| 264 try: |
| 265 self.stats['by_msg'][msgid] += 1 |
| 266 except KeyError: |
| 267 self.stats['by_msg'][msgid] = 1 |
| 268 msg = self._messages[msgid].msg |
| 269 # expand message ? |
| 270 if args: |
| 271 msg %= args |
| 272 # get module and object |
| 273 if node is None: |
| 274 module, obj = self.current_name, '' |
| 275 path = self.current_file |
| 276 else: |
| 277 module, obj = get_module_and_frameid(node) |
| 278 path = node.root().file |
| 279 # add the message |
| 280 self.reporter.add_message(msgid, (path, module, obj, line or 1, col_offs
et or 0), msg) |
| 281 |
| 282 def help_message(self, msgids): |
| 283 """display help messages for the given message identifiers""" |
| 284 for msgid in msgids: |
| 285 try: |
| 286 print self.get_message_help(msgid, True) |
| 287 print |
| 288 except UnknownMessage, ex: |
| 289 print ex |
| 290 print |
| 291 continue |
| 292 |
| 293 def print_full_documentation(self): |
| 294 """output a full documentation in ReST format""" |
| 295 by_checker = {} |
| 296 for checker in self.get_checkers(): |
| 297 if checker.name == 'master': |
| 298 prefix = 'Main ' |
| 299 print "Options" |
| 300 print '-------\n' |
| 301 if checker.options: |
| 302 for section, options in checker.options_by_section(): |
| 303 if section is None: |
| 304 title = 'General options' |
| 305 else: |
| 306 title = '%s options' % section.capitalize() |
| 307 print title |
| 308 print '~' * len(title) |
| 309 rest_format_section(sys.stdout, None, options) |
| 310 print |
| 311 else: |
| 312 try: |
| 313 by_checker[checker.name][0] += checker.options_and_values() |
| 314 by_checker[checker.name][1].update(checker.msgs) |
| 315 by_checker[checker.name][2] += checker.reports |
| 316 except KeyError: |
| 317 by_checker[checker.name] = [list(checker.options_and_values(
)), |
| 318 dict(checker.msgs), |
| 319 list(checker.reports)] |
| 320 for checker, (options, msgs, reports) in by_checker.items(): |
| 321 prefix = '' |
| 322 title = '%s checker' % checker |
| 323 print title |
| 324 print '-' * len(title) |
| 325 print |
| 326 if options: |
| 327 title = 'Options' |
| 328 print title |
| 329 print '~' * len(title) |
| 330 rest_format_section(sys.stdout, None, options) |
| 331 print |
| 332 if msgs: |
| 333 title = ('%smessages' % prefix).capitalize() |
| 334 print title |
| 335 print '~' * len(title) |
| 336 for msgid in sort_msgs(msgs.keys()): |
| 337 print self.get_message_help(msgid, False) |
| 338 print |
| 339 if reports: |
| 340 title = ('%sreports' % prefix).capitalize() |
| 341 print title |
| 342 print '~' * len(title) |
| 343 for report in reports: |
| 344 print ':%s: %s' % report[:2] |
| 345 print |
| 346 print |
| 347 |
| 348 def list_messages(self): |
| 349 """output full messages list documentation in ReST format""" |
| 350 msgids = [] |
| 351 for checker in self.get_checkers(): |
| 352 for msgid in checker.msgs.keys(): |
| 353 msgids.append(msgid) |
| 354 msgids.sort() |
| 355 for msgid in msgids: |
| 356 print self.get_message_help(msgid, False) |
| 357 print |
| 358 |
| 359 |
| 360 class ReportsHandlerMixIn: |
| 361 """a mix-in class containing all the reports and stats manipulation |
| 362 related methods for the main lint class |
| 363 """ |
| 364 def __init__(self): |
| 365 self._reports = {} |
| 366 self._reports_state = {} |
| 367 |
| 368 def register_report(self, reportid, r_title, r_cb, checker): |
| 369 """register a report |
| 370 |
| 371 reportid is the unique identifier for the report |
| 372 r_title the report's title |
| 373 r_cb the method to call to make the report |
| 374 checker is the checker defining the report |
| 375 """ |
| 376 reportid = reportid.upper() |
| 377 self._reports.setdefault(checker, []).append( (reportid, r_title, r_cb)
) |
| 378 |
| 379 def enable_report(self, reportid): |
| 380 """disable the report of the given id""" |
| 381 reportid = reportid.upper() |
| 382 self._reports_state[reportid] = True |
| 383 |
| 384 def disable_report(self, reportid): |
| 385 """disable the report of the given id""" |
| 386 reportid = reportid.upper() |
| 387 self._reports_state[reportid] = False |
| 388 |
| 389 def report_is_enabled(self, reportid): |
| 390 """return true if the report associated to the given identifier is |
| 391 enabled |
| 392 """ |
| 393 return self._reports_state.get(reportid, True) |
| 394 |
| 395 def make_reports(self, stats, old_stats): |
| 396 """render registered reports""" |
| 397 if self.config.files_output: |
| 398 filename = 'pylint_global.' + self.reporter.extension |
| 399 self.reporter.set_output(open(filename, 'w')) |
| 400 sect = Section('Report', |
| 401 '%s statements analysed.'% (self.stats['statement'])) |
| 402 for checker in self._reports: |
| 403 for reportid, r_title, r_cb in self._reports[checker]: |
| 404 if not self.report_is_enabled(reportid): |
| 405 continue |
| 406 report_sect = Section(r_title) |
| 407 try: |
| 408 r_cb(report_sect, stats, old_stats) |
| 409 except EmptyReport: |
| 410 continue |
| 411 report_sect.report_id = reportid |
| 412 sect.append(report_sect) |
| 413 self.reporter.display_results(sect) |
| 414 |
| 415 def add_stats(self, **kwargs): |
| 416 """add some stats entries to the statistic dictionary |
| 417 raise an AssertionError if there is a key conflict |
| 418 """ |
| 419 for key, value in kwargs.items(): |
| 420 if key[-1] == '_': |
| 421 key = key[:-1] |
| 422 assert key not in self.stats |
| 423 self.stats[key] = value |
| 424 return self.stats |
| 425 |
| 426 |
| 427 def expand_modules(files_or_modules, black_list): |
| 428 """take a list of files/modules/packages and return the list of tuple |
| 429 (file, module name) which have to be actually checked |
| 430 """ |
| 431 result = [] |
| 432 errors = [] |
| 433 for something in files_or_modules: |
| 434 if exists(something): |
| 435 # this is a file or a directory |
| 436 try: |
| 437 modname = '.'.join(modpath_from_file(something)) |
| 438 except ImportError: |
| 439 modname = splitext(basename(something))[0] |
| 440 if isdir(something): |
| 441 filepath = join(something, '__init__.py') |
| 442 else: |
| 443 filepath = something |
| 444 else: |
| 445 # suppose it's a module or package |
| 446 modname = something |
| 447 try: |
| 448 filepath = file_from_modpath(modname.split('.')) |
| 449 if filepath is None: |
| 450 errors.append( {'key' : 'F0003', 'mod': modname} ) |
| 451 continue |
| 452 except (ImportError, SyntaxError), ex: |
| 453 # FIXME p3k : the SyntaxError is a Python bug and should be |
| 454 # removed as soon as possible http://bugs.python.org/issue10588 |
| 455 errors.append( {'key': 'F0001', 'mod': modname, 'ex': ex} ) |
| 456 continue |
| 457 filepath = normpath(filepath) |
| 458 result.append( {'path': filepath, 'name': modname, |
| 459 'basepath': filepath, 'basename': modname} ) |
| 460 if not (modname.endswith('.__init__') or modname == '__init__') \ |
| 461 and '__init__.py' in filepath: |
| 462 for subfilepath in get_module_files(dirname(filepath), black_list): |
| 463 if filepath == subfilepath: |
| 464 continue |
| 465 submodname = '.'.join(modpath_from_file(subfilepath)) |
| 466 result.append( {'path': subfilepath, 'name': submodname, |
| 467 'basepath': filepath, 'basename': modname} ) |
| 468 return result, errors |
| 469 |
| 470 |
| 471 class PyLintASTWalker(object): |
| 472 |
| 473 def __init__(self, linter): |
| 474 # callbacks per node types |
| 475 self.nbstatements = 1 |
| 476 self.visit_events = {} |
| 477 self.leave_events = {} |
| 478 self.linter = linter |
| 479 |
| 480 def add_checker(self, checker): |
| 481 """walk to the checker's dir and collect visit and leave methods""" |
| 482 # XXX : should be possible to merge needed_checkers and add_checker |
| 483 vcids = set() |
| 484 lcids = set() |
| 485 visits = self.visit_events |
| 486 leaves = self.leave_events |
| 487 msgs = self.linter._msgs_state |
| 488 for member in dir(checker): |
| 489 cid = member[6:] |
| 490 if cid == 'default': |
| 491 continue |
| 492 if member.startswith('visit_'): |
| 493 v_meth = getattr(checker, member) |
| 494 # don't use visit_methods with no activated message: |
| 495 if hasattr(v_meth, 'checks_msgs'): |
| 496 if not any(msgs.get(m, True) for m in v_meth.checks_msgs): |
| 497 continue |
| 498 visits.setdefault(cid, []).append(v_meth) |
| 499 vcids.add(cid) |
| 500 elif member.startswith('leave_'): |
| 501 l_meth = getattr(checker, member) |
| 502 # don't use leave_methods with no activated message: |
| 503 if hasattr(l_meth, 'checks_msgs'): |
| 504 if not any(msgs.get(m, True) for m in l_meth.checks_msgs): |
| 505 continue |
| 506 leaves.setdefault(cid, []).append(l_meth) |
| 507 lcids.add(cid) |
| 508 visit_default = getattr(checker, 'visit_default', None) |
| 509 if visit_default: |
| 510 for cls in nodes.ALL_NODE_CLASSES: |
| 511 cid = cls.__name__.lower() |
| 512 if cid not in vcids: |
| 513 visits.setdefault(cid, []).append(visit_default) |
| 514 # for now we have no "leave_default" method in Pylint |
| 515 |
| 516 def walk(self, astng): |
| 517 """call visit events of astng checkers for the given node, recurse on |
| 518 its children, then leave events. |
| 519 """ |
| 520 cid = astng.__class__.__name__.lower() |
| 521 if astng.is_statement: |
| 522 self.nbstatements += 1 |
| 523 # generate events for this node on each checker |
| 524 for cb in self.visit_events.get(cid, ()): |
| 525 cb(astng) |
| 526 # recurse on children |
| 527 for child in astng.get_children(): |
| 528 self.walk(child) |
| 529 for cb in self.leave_events.get(cid, ()): |
| 530 cb(astng) |
| 531 |
OLD | NEW |