OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2003-2014 LOGILAB S.A. (Paris, FRANCE). |
| 2 # http://www.logilab.fr/ -- mailto:contact@logilab.fr |
| 3 # |
| 4 # This program is free software; you can redistribute it and/or modify it under |
| 5 # the terms of the GNU General Public License as published by the Free Software |
| 6 # Foundation; either version 2 of the License, or (at your option) any later |
| 7 # version. |
| 8 # |
| 9 # This program is distributed in the hope that it will be useful, but WITHOUT |
| 10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| 11 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details |
| 12 # |
| 13 # You should have received a copy of the GNU General Public License along with |
| 14 # this program; if not, write to the Free Software Foundation, Inc., |
| 15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 16 """some various utilities and helper classes, most of them used in the |
| 17 main pylint class |
| 18 """ |
| 19 from __future__ import print_function |
| 20 |
| 21 import collections |
| 22 import os |
| 23 import re |
| 24 import sys |
| 25 import tokenize |
| 26 import warnings |
| 27 from os.path import dirname, basename, splitext, exists, isdir, join, normpath |
| 28 |
| 29 import six |
| 30 from six.moves import zip # pylint: disable=redefined-builtin |
| 31 |
| 32 from logilab.common.interface import implements |
| 33 from logilab.common.textutils import normalize_text |
| 34 from logilab.common.configuration import rest_format_section |
| 35 from logilab.common.ureports import Section |
| 36 |
| 37 from astroid import nodes, Module |
| 38 from astroid.modutils import modpath_from_file, get_module_files, \ |
| 39 file_from_modpath, load_module_from_file |
| 40 |
| 41 from pylint.interfaces import IRawChecker, ITokenChecker, UNDEFINED |
| 42 |
| 43 |
| 44 class UnknownMessage(Exception): |
| 45 """raised when a unregistered message id is encountered""" |
| 46 |
| 47 class EmptyReport(Exception): |
| 48 """raised when a report is empty and so should not be displayed""" |
| 49 |
| 50 |
| 51 MSG_TYPES = { |
| 52 'I' : 'info', |
| 53 'C' : 'convention', |
| 54 'R' : 'refactor', |
| 55 'W' : 'warning', |
| 56 'E' : 'error', |
| 57 'F' : 'fatal' |
| 58 } |
| 59 MSG_TYPES_LONG = {v: k for k, v in six.iteritems(MSG_TYPES)} |
| 60 |
| 61 MSG_TYPES_STATUS = { |
| 62 'I' : 0, |
| 63 'C' : 16, |
| 64 'R' : 8, |
| 65 'W' : 4, |
| 66 'E' : 2, |
| 67 'F' : 1 |
| 68 } |
| 69 |
| 70 _MSG_ORDER = 'EWRCIF' |
| 71 MSG_STATE_SCOPE_CONFIG = 0 |
| 72 MSG_STATE_SCOPE_MODULE = 1 |
| 73 MSG_STATE_CONFIDENCE = 2 |
| 74 |
| 75 OPTION_RGX = re.compile(r'\s*#.*\bpylint:(.*)') |
| 76 |
| 77 # The line/node distinction does not apply to fatal errors and reports. |
| 78 _SCOPE_EXEMPT = 'FR' |
| 79 |
| 80 class WarningScope(object): |
| 81 LINE = 'line-based-msg' |
| 82 NODE = 'node-based-msg' |
| 83 |
| 84 _MsgBase = collections.namedtuple( |
| 85 '_MsgBase', |
| 86 ['msg_id', 'symbol', 'msg', 'C', 'category', 'confidence', |
| 87 'abspath', 'path', 'module', 'obj', 'line', 'column']) |
| 88 |
| 89 |
| 90 class Message(_MsgBase): |
| 91 """This class represent a message to be issued by the reporters""" |
| 92 def __new__(cls, msg_id, symbol, location, msg, confidence): |
| 93 return _MsgBase.__new__( |
| 94 cls, msg_id, symbol, msg, msg_id[0], MSG_TYPES[msg_id[0]], |
| 95 confidence, *location) |
| 96 |
| 97 def format(self, template): |
| 98 """Format the message according to the given template. |
| 99 |
| 100 The template format is the one of the format method : |
| 101 cf. http://docs.python.org/2/library/string.html#formatstrings |
| 102 """ |
| 103 # For some reason, _asdict on derived namedtuples does not work with |
| 104 # Python 3.4. Needs some investigation. |
| 105 return template.format(**dict(zip(self._fields, self))) |
| 106 |
| 107 |
| 108 def get_module_and_frameid(node): |
| 109 """return the module name and the frame id in the module""" |
| 110 frame = node.frame() |
| 111 module, obj = '', [] |
| 112 while frame: |
| 113 if isinstance(frame, Module): |
| 114 module = frame.name |
| 115 else: |
| 116 obj.append(getattr(frame, 'name', '<lambda>')) |
| 117 try: |
| 118 frame = frame.parent.frame() |
| 119 except AttributeError: |
| 120 frame = None |
| 121 obj.reverse() |
| 122 return module, '.'.join(obj) |
| 123 |
| 124 def category_id(cid): |
| 125 cid = cid.upper() |
| 126 if cid in MSG_TYPES: |
| 127 return cid |
| 128 return MSG_TYPES_LONG.get(cid) |
| 129 |
| 130 |
| 131 def _decoding_readline(stream, module): |
| 132 return lambda: stream.readline().decode(module.file_encoding, |
| 133 'replace') |
| 134 |
| 135 |
| 136 def tokenize_module(module): |
| 137 with module.stream() as stream: |
| 138 readline = stream.readline |
| 139 if sys.version_info < (3, 0): |
| 140 if module.file_encoding is not None: |
| 141 readline = _decoding_readline(stream, module) |
| 142 return list(tokenize.generate_tokens(readline)) |
| 143 return list(tokenize.tokenize(readline)) |
| 144 |
| 145 def build_message_def(checker, msgid, msg_tuple): |
| 146 if implements(checker, (IRawChecker, ITokenChecker)): |
| 147 default_scope = WarningScope.LINE |
| 148 else: |
| 149 default_scope = WarningScope.NODE |
| 150 options = {} |
| 151 if len(msg_tuple) > 3: |
| 152 (msg, symbol, descr, options) = msg_tuple |
| 153 elif len(msg_tuple) > 2: |
| 154 (msg, symbol, descr) = msg_tuple[:3] |
| 155 else: |
| 156 # messages should have a symbol, but for backward compatibility |
| 157 # they may not. |
| 158 (msg, descr) = msg_tuple |
| 159 warnings.warn("[pylint 0.26] description of message %s doesn't include " |
| 160 "a symbolic name" % msgid, DeprecationWarning) |
| 161 symbol = None |
| 162 options.setdefault('scope', default_scope) |
| 163 return MessageDefinition(checker, msgid, msg, descr, symbol, **options) |
| 164 |
| 165 |
| 166 class MessageDefinition(object): |
| 167 def __init__(self, checker, msgid, msg, descr, symbol, scope, |
| 168 minversion=None, maxversion=None, old_names=None): |
| 169 self.checker = checker |
| 170 assert len(msgid) == 5, 'Invalid message id %s' % msgid |
| 171 assert msgid[0] in MSG_TYPES, \ |
| 172 'Bad message type %s in %r' % (msgid[0], msgid) |
| 173 self.msgid = msgid |
| 174 self.msg = msg |
| 175 self.descr = descr |
| 176 self.symbol = symbol |
| 177 self.scope = scope |
| 178 self.minversion = minversion |
| 179 self.maxversion = maxversion |
| 180 self.old_names = old_names or [] |
| 181 |
| 182 def may_be_emitted(self): |
| 183 """return True if message may be emitted using the current interpreter""
" |
| 184 if self.minversion is not None and self.minversion > sys.version_info: |
| 185 return False |
| 186 if self.maxversion is not None and self.maxversion <= sys.version_info: |
| 187 return False |
| 188 return True |
| 189 |
| 190 def format_help(self, checkerref=False): |
| 191 """return the help string for the given message id""" |
| 192 desc = self.descr |
| 193 if checkerref: |
| 194 desc += ' This message belongs to the %s checker.' % \ |
| 195 self.checker.name |
| 196 title = self.msg |
| 197 if self.symbol: |
| 198 msgid = '%s (%s)' % (self.symbol, self.msgid) |
| 199 else: |
| 200 msgid = self.msgid |
| 201 if self.minversion or self.maxversion: |
| 202 restr = [] |
| 203 if self.minversion: |
| 204 restr.append('< %s' % '.'.join([str(n) for n in self.minversion]
)) |
| 205 if self.maxversion: |
| 206 restr.append('>= %s' % '.'.join([str(n) for n in self.maxversion
])) |
| 207 restr = ' or '.join(restr) |
| 208 if checkerref: |
| 209 desc += " It can't be emitted when using Python %s." % restr |
| 210 else: |
| 211 desc += " This message can't be emitted when using Python %s." %
restr |
| 212 desc = normalize_text(' '.join(desc.split()), indent=' ') |
| 213 if title != '%s': |
| 214 title = title.splitlines()[0] |
| 215 return ':%s: *%s*\n%s' % (msgid, title, desc) |
| 216 return ':%s:\n%s' % (msgid, desc) |
| 217 |
| 218 |
| 219 class MessagesHandlerMixIn(object): |
| 220 """a mix-in class containing all the messages related methods for the main |
| 221 lint class |
| 222 """ |
| 223 |
| 224 def __init__(self): |
| 225 self._msgs_state = {} |
| 226 self.msg_status = 0 |
| 227 |
| 228 def disable(self, msgid, scope='package', line=None, ignore_unknown=False): |
| 229 """don't output message of the given id""" |
| 230 assert scope in ('package', 'module') |
| 231 # handle disable=all by disabling all categories |
| 232 if msgid == 'all': |
| 233 for msgid in MSG_TYPES: |
| 234 self.disable(msgid, scope, line) |
| 235 return |
| 236 # msgid is a category? |
| 237 catid = category_id(msgid) |
| 238 if catid is not None: |
| 239 for _msgid in self.msgs_store._msgs_by_category.get(catid): |
| 240 self.disable(_msgid, scope, line) |
| 241 return |
| 242 # msgid is a checker name? |
| 243 if msgid.lower() in self._checkers: |
| 244 msgs_store = self.msgs_store |
| 245 for checker in self._checkers[msgid.lower()]: |
| 246 for _msgid in checker.msgs: |
| 247 if _msgid in msgs_store._alternative_names: |
| 248 self.disable(_msgid, scope, line) |
| 249 return |
| 250 # msgid is report id? |
| 251 if msgid.lower().startswith('rp'): |
| 252 self.disable_report(msgid) |
| 253 return |
| 254 |
| 255 try: |
| 256 # msgid is a symbolic or numeric msgid. |
| 257 msg = self.msgs_store.check_message_id(msgid) |
| 258 except UnknownMessage: |
| 259 if ignore_unknown: |
| 260 return |
| 261 raise |
| 262 |
| 263 if scope == 'module': |
| 264 self.file_state.set_msg_status(msg, line, False) |
| 265 if msg.symbol != 'locally-disabled': |
| 266 self.add_message('locally-disabled', line=line, |
| 267 args=(msg.symbol, msg.msgid)) |
| 268 |
| 269 else: |
| 270 msgs = self._msgs_state |
| 271 msgs[msg.msgid] = False |
| 272 # sync configuration object |
| 273 self.config.disable = [mid for mid, val in six.iteritems(msgs) |
| 274 if not val] |
| 275 |
| 276 def enable(self, msgid, scope='package', line=None, ignore_unknown=False): |
| 277 """reenable message of the given id""" |
| 278 assert scope in ('package', 'module') |
| 279 catid = category_id(msgid) |
| 280 # msgid is a category? |
| 281 if catid is not None: |
| 282 for msgid in self.msgs_store._msgs_by_category.get(catid): |
| 283 self.enable(msgid, scope, line) |
| 284 return |
| 285 # msgid is a checker name? |
| 286 if msgid.lower() in self._checkers: |
| 287 for checker in self._checkers[msgid.lower()]: |
| 288 for msgid_ in checker.msgs: |
| 289 self.enable(msgid_, scope, line) |
| 290 return |
| 291 # msgid is report id? |
| 292 if msgid.lower().startswith('rp'): |
| 293 self.enable_report(msgid) |
| 294 return |
| 295 |
| 296 try: |
| 297 # msgid is a symbolic or numeric msgid. |
| 298 msg = self.msgs_store.check_message_id(msgid) |
| 299 except UnknownMessage: |
| 300 if ignore_unknown: |
| 301 return |
| 302 raise |
| 303 |
| 304 if scope == 'module': |
| 305 self.file_state.set_msg_status(msg, line, True) |
| 306 self.add_message('locally-enabled', line=line, args=(msg.symbol, msg
.msgid)) |
| 307 else: |
| 308 msgs = self._msgs_state |
| 309 msgs[msg.msgid] = True |
| 310 # sync configuration object |
| 311 self.config.enable = [mid for mid, val in six.iteritems(msgs) if val
] |
| 312 |
| 313 def get_message_state_scope(self, msgid, line=None, confidence=UNDEFINED): |
| 314 """Returns the scope at which a message was enabled/disabled.""" |
| 315 if self.config.confidence and confidence.name not in self.config.confide
nce: |
| 316 return MSG_STATE_CONFIDENCE |
| 317 try: |
| 318 if line in self.file_state._module_msgs_state[msgid]: |
| 319 return MSG_STATE_SCOPE_MODULE |
| 320 except (KeyError, TypeError): |
| 321 return MSG_STATE_SCOPE_CONFIG |
| 322 |
| 323 def is_message_enabled(self, msg_descr, line=None, confidence=None): |
| 324 """return true if the message associated to the given message id is |
| 325 enabled |
| 326 |
| 327 msgid may be either a numeric or symbolic message id. |
| 328 """ |
| 329 if self.config.confidence and confidence: |
| 330 if confidence.name not in self.config.confidence: |
| 331 return False |
| 332 try: |
| 333 msgid = self.msgs_store.check_message_id(msg_descr).msgid |
| 334 except UnknownMessage: |
| 335 # The linter checks for messages that are not registered |
| 336 # due to version mismatch, just treat them as message IDs |
| 337 # for now. |
| 338 msgid = msg_descr |
| 339 if line is None: |
| 340 return self._msgs_state.get(msgid, True) |
| 341 try: |
| 342 return self.file_state._module_msgs_state[msgid][line] |
| 343 except KeyError: |
| 344 return self._msgs_state.get(msgid, True) |
| 345 |
| 346 def add_message(self, msg_descr, line=None, node=None, args=None, confidence
=UNDEFINED): |
| 347 """Adds a message given by ID or name. |
| 348 |
| 349 If provided, the message string is expanded using args |
| 350 |
| 351 AST checkers should must the node argument (but may optionally |
| 352 provide line if the line number is different), raw and token checkers |
| 353 must provide the line argument. |
| 354 """ |
| 355 msg_info = self.msgs_store.check_message_id(msg_descr) |
| 356 msgid = msg_info.msgid |
| 357 # backward compatibility, message may not have a symbol |
| 358 symbol = msg_info.symbol or msgid |
| 359 # Fatal messages and reports are special, the node/scope distinction |
| 360 # does not apply to them. |
| 361 if msgid[0] not in _SCOPE_EXEMPT: |
| 362 if msg_info.scope == WarningScope.LINE: |
| 363 assert node is None and line is not None, ( |
| 364 'Message %s must only provide line, got line=%s, node=%s' %
(msgid, line, node)) |
| 365 elif msg_info.scope == WarningScope.NODE: |
| 366 # Node-based warnings may provide an override line. |
| 367 assert node is not None, 'Message %s must provide Node, got None
' |
| 368 |
| 369 if line is None and node is not None: |
| 370 line = node.fromlineno |
| 371 if hasattr(node, 'col_offset'): |
| 372 col_offset = node.col_offset # XXX measured in bytes for utf-8, divi
de by two for chars? |
| 373 else: |
| 374 col_offset = None |
| 375 # should this message be displayed |
| 376 if not self.is_message_enabled(msgid, line, confidence): |
| 377 self.file_state.handle_ignored_message( |
| 378 self.get_message_state_scope(msgid, line, confidence), |
| 379 msgid, line, node, args, confidence) |
| 380 return |
| 381 # update stats |
| 382 msg_cat = MSG_TYPES[msgid[0]] |
| 383 self.msg_status |= MSG_TYPES_STATUS[msgid[0]] |
| 384 self.stats[msg_cat] += 1 |
| 385 self.stats['by_module'][self.current_name][msg_cat] += 1 |
| 386 try: |
| 387 self.stats['by_msg'][symbol] += 1 |
| 388 except KeyError: |
| 389 self.stats['by_msg'][symbol] = 1 |
| 390 # expand message ? |
| 391 msg = msg_info.msg |
| 392 if args: |
| 393 msg %= args |
| 394 # get module and object |
| 395 if node is None: |
| 396 module, obj = self.current_name, '' |
| 397 abspath = self.current_file |
| 398 else: |
| 399 module, obj = get_module_and_frameid(node) |
| 400 abspath = node.root().file |
| 401 path = abspath.replace(self.reporter.path_strip_prefix, '') |
| 402 # add the message |
| 403 self.reporter.handle_message( |
| 404 Message(msgid, symbol, |
| 405 (abspath, path, module, obj, line or 1, col_offset or 0), ms
g, confidence)) |
| 406 |
| 407 def print_full_documentation(self): |
| 408 """output a full documentation in ReST format""" |
| 409 print("Pylint global options and switches") |
| 410 print("----------------------------------") |
| 411 print("") |
| 412 print("Pylint provides global options and switches.") |
| 413 print("") |
| 414 |
| 415 by_checker = {} |
| 416 for checker in self.get_checkers(): |
| 417 if checker.name == 'master': |
| 418 if checker.options: |
| 419 for section, options in checker.options_by_section(): |
| 420 if section is None: |
| 421 title = 'General options' |
| 422 else: |
| 423 title = '%s options' % section.capitalize() |
| 424 print(title) |
| 425 print('~' * len(title)) |
| 426 rest_format_section(sys.stdout, None, options) |
| 427 print("") |
| 428 else: |
| 429 try: |
| 430 by_checker[checker.name][0] += checker.options_and_values() |
| 431 by_checker[checker.name][1].update(checker.msgs) |
| 432 by_checker[checker.name][2] += checker.reports |
| 433 except KeyError: |
| 434 by_checker[checker.name] = [list(checker.options_and_values(
)), |
| 435 dict(checker.msgs), |
| 436 list(checker.reports)] |
| 437 |
| 438 print("Pylint checkers' options and switches") |
| 439 print("-------------------------------------") |
| 440 print("") |
| 441 print("Pylint checkers can provide three set of features:") |
| 442 print("") |
| 443 print("* options that control their execution,") |
| 444 print("* messages that they can raise,") |
| 445 print("* reports that they can generate.") |
| 446 print("") |
| 447 print("Below is a list of all checkers and their features.") |
| 448 print("") |
| 449 |
| 450 for checker, (options, msgs, reports) in six.iteritems(by_checker): |
| 451 title = '%s checker' % (checker.replace("_", " ").title()) |
| 452 print(title) |
| 453 print('~' * len(title)) |
| 454 print("") |
| 455 print("Verbatim name of the checker is ``%s``." % checker) |
| 456 print("") |
| 457 if options: |
| 458 title = 'Options' |
| 459 print(title) |
| 460 print('^' * len(title)) |
| 461 rest_format_section(sys.stdout, None, options) |
| 462 print("") |
| 463 if msgs: |
| 464 title = 'Messages' |
| 465 print(title) |
| 466 print('~' * len(title)) |
| 467 for msgid, msg in sorted(six.iteritems(msgs), |
| 468 key=lambda kv: (_MSG_ORDER.index(kv[0][
0]), kv[1])): |
| 469 msg = build_message_def(checker, msgid, msg) |
| 470 print(msg.format_help(checkerref=False)) |
| 471 print("") |
| 472 if reports: |
| 473 title = 'Reports' |
| 474 print(title) |
| 475 print('~' * len(title)) |
| 476 for report in reports: |
| 477 print(':%s: %s' % report[:2]) |
| 478 print("") |
| 479 print("") |
| 480 |
| 481 |
| 482 class FileState(object): |
| 483 """Hold internal state specific to the currently analyzed file""" |
| 484 |
| 485 def __init__(self, modname=None): |
| 486 self.base_name = modname |
| 487 self._module_msgs_state = {} |
| 488 self._raw_module_msgs_state = {} |
| 489 self._ignored_msgs = collections.defaultdict(set) |
| 490 self._suppression_mapping = {} |
| 491 |
| 492 def collect_block_lines(self, msgs_store, module_node): |
| 493 """Walk the AST to collect block level options line numbers.""" |
| 494 for msg, lines in six.iteritems(self._module_msgs_state): |
| 495 self._raw_module_msgs_state[msg] = lines.copy() |
| 496 orig_state = self._module_msgs_state.copy() |
| 497 self._module_msgs_state = {} |
| 498 self._suppression_mapping = {} |
| 499 self._collect_block_lines(msgs_store, module_node, orig_state) |
| 500 |
| 501 def _collect_block_lines(self, msgs_store, node, msg_state): |
| 502 """Recursivly walk (depth first) AST to collect block level options line |
| 503 numbers. |
| 504 """ |
| 505 for child in node.get_children(): |
| 506 self._collect_block_lines(msgs_store, child, msg_state) |
| 507 first = node.fromlineno |
| 508 last = node.tolineno |
| 509 # first child line number used to distinguish between disable |
| 510 # which are the first child of scoped node with those defined later. |
| 511 # For instance in the code below: |
| 512 # |
| 513 # 1. def meth8(self): |
| 514 # 2. """test late disabling""" |
| 515 # 3. # pylint: disable=E1102 |
| 516 # 4. print self.blip |
| 517 # 5. # pylint: disable=E1101 |
| 518 # 6. print self.bla |
| 519 # |
| 520 # E1102 should be disabled from line 1 to 6 while E1101 from line 5 to 6 |
| 521 # |
| 522 # this is necessary to disable locally messages applying to class / |
| 523 # function using their fromlineno |
| 524 if isinstance(node, (nodes.Module, nodes.Class, nodes.Function)) and nod
e.body: |
| 525 firstchildlineno = node.body[0].fromlineno |
| 526 else: |
| 527 firstchildlineno = last |
| 528 for msgid, lines in six.iteritems(msg_state): |
| 529 for lineno, state in list(lines.items()): |
| 530 original_lineno = lineno |
| 531 if first <= lineno <= last: |
| 532 # Set state for all lines for this block, if the |
| 533 # warning is applied to nodes. |
| 534 if msgs_store.check_message_id(msgid).scope == WarningScope
.NODE: |
| 535 if lineno > firstchildlineno: |
| 536 state = True |
| 537 first_, last_ = node.block_range(lineno) |
| 538 else: |
| 539 first_ = lineno |
| 540 last_ = last |
| 541 for line in range(first_, last_+1): |
| 542 # do not override existing entries |
| 543 if not line in self._module_msgs_state.get(msgid, ()): |
| 544 if line in lines: # state change in the same block |
| 545 state = lines[line] |
| 546 original_lineno = line |
| 547 if not state: |
| 548 self._suppression_mapping[(msgid, line)] = origi
nal_lineno |
| 549 try: |
| 550 self._module_msgs_state[msgid][line] = state |
| 551 except KeyError: |
| 552 self._module_msgs_state[msgid] = {line: state} |
| 553 del lines[lineno] |
| 554 |
| 555 def set_msg_status(self, msg, line, status): |
| 556 """Set status (enabled/disable) for a given message at a given line""" |
| 557 assert line > 0 |
| 558 try: |
| 559 self._module_msgs_state[msg.msgid][line] = status |
| 560 except KeyError: |
| 561 self._module_msgs_state[msg.msgid] = {line: status} |
| 562 |
| 563 def handle_ignored_message(self, state_scope, msgid, line, |
| 564 node, args, confidence): # pylint: disable=unused
-argument |
| 565 """Report an ignored message. |
| 566 |
| 567 state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG, |
| 568 depending on whether the message was disabled locally in the module, |
| 569 or globally. The other arguments are the same as for add_message. |
| 570 """ |
| 571 if state_scope == MSG_STATE_SCOPE_MODULE: |
| 572 try: |
| 573 orig_line = self._suppression_mapping[(msgid, line)] |
| 574 self._ignored_msgs[(msgid, orig_line)].add(line) |
| 575 except KeyError: |
| 576 pass |
| 577 |
| 578 def iter_spurious_suppression_messages(self, msgs_store): |
| 579 for warning, lines in six.iteritems(self._raw_module_msgs_state): |
| 580 for line, enable in six.iteritems(lines): |
| 581 if not enable and (warning, line) not in self._ignored_msgs: |
| 582 yield 'useless-suppression', line, \ |
| 583 (msgs_store.get_msg_display_string(warning),) |
| 584 # don't use iteritems here, _ignored_msgs may be modified by add_message |
| 585 for (warning, from_), lines in list(self._ignored_msgs.items()): |
| 586 for line in lines: |
| 587 yield 'suppressed-message', line, \ |
| 588 (msgs_store.get_msg_display_string(warning), from_) |
| 589 |
| 590 |
| 591 class MessagesStore(object): |
| 592 """The messages store knows information about every possible message but has |
| 593 no particular state during analysis. |
| 594 """ |
| 595 |
| 596 def __init__(self): |
| 597 # Primary registry for all active messages (i.e. all messages |
| 598 # that can be emitted by pylint for the underlying Python |
| 599 # version). It contains the 1:1 mapping from symbolic names |
| 600 # to message definition objects. |
| 601 self._messages = {} |
| 602 # Maps alternative names (numeric IDs, deprecated names) to |
| 603 # message definitions. May contain several names for each definition |
| 604 # object. |
| 605 self._alternative_names = {} |
| 606 self._msgs_by_category = collections.defaultdict(list) |
| 607 |
| 608 @property |
| 609 def messages(self): |
| 610 """The list of all active messages.""" |
| 611 return six.itervalues(self._messages) |
| 612 |
| 613 def add_renamed_message(self, old_id, old_symbol, new_symbol): |
| 614 """Register the old ID and symbol for a warning that was renamed. |
| 615 |
| 616 This allows users to keep using the old ID/symbol in suppressions. |
| 617 """ |
| 618 msg = self.check_message_id(new_symbol) |
| 619 msg.old_names.append((old_id, old_symbol)) |
| 620 self._alternative_names[old_id] = msg |
| 621 self._alternative_names[old_symbol] = msg |
| 622 |
| 623 def register_messages(self, checker): |
| 624 """register a dictionary of messages |
| 625 |
| 626 Keys are message ids, values are a 2-uple with the message type and the |
| 627 message itself |
| 628 |
| 629 message ids should be a string of len 4, where the two first characters |
| 630 are the checker id and the two last the message id in this checker |
| 631 """ |
| 632 chkid = None |
| 633 for msgid, msg_tuple in six.iteritems(checker.msgs): |
| 634 msg = build_message_def(checker, msgid, msg_tuple) |
| 635 assert msg.symbol not in self._messages, \ |
| 636 'Message symbol %r is already defined' % msg.symbol |
| 637 # avoid duplicate / malformed ids |
| 638 assert msg.msgid not in self._alternative_names, \ |
| 639 'Message id %r is already defined' % msgid |
| 640 assert chkid is None or chkid == msg.msgid[1:3], \ |
| 641 'Inconsistent checker part in message id %r' % msgid |
| 642 chkid = msg.msgid[1:3] |
| 643 self._messages[msg.symbol] = msg |
| 644 self._alternative_names[msg.msgid] = msg |
| 645 for old_id, old_symbol in msg.old_names: |
| 646 self._alternative_names[old_id] = msg |
| 647 self._alternative_names[old_symbol] = msg |
| 648 self._msgs_by_category[msg.msgid[0]].append(msg.msgid) |
| 649 |
| 650 def check_message_id(self, msgid): |
| 651 """returns the Message object for this message. |
| 652 |
| 653 msgid may be either a numeric or symbolic id. |
| 654 |
| 655 Raises UnknownMessage if the message id is not defined. |
| 656 """ |
| 657 if msgid[1:].isdigit(): |
| 658 msgid = msgid.upper() |
| 659 for source in (self._alternative_names, self._messages): |
| 660 try: |
| 661 return source[msgid] |
| 662 except KeyError: |
| 663 pass |
| 664 raise UnknownMessage('No such message id %s' % msgid) |
| 665 |
| 666 def get_msg_display_string(self, msgid): |
| 667 """Generates a user-consumable representation of a message. |
| 668 |
| 669 Can be just the message ID or the ID and the symbol. |
| 670 """ |
| 671 return repr(self.check_message_id(msgid).symbol) |
| 672 |
| 673 def help_message(self, msgids): |
| 674 """display help messages for the given message identifiers""" |
| 675 for msgid in msgids: |
| 676 try: |
| 677 print(self.check_message_id(msgid).format_help(checkerref=True)) |
| 678 print("") |
| 679 except UnknownMessage as ex: |
| 680 print(ex) |
| 681 print("") |
| 682 continue |
| 683 |
| 684 def list_messages(self): |
| 685 """output full messages list documentation in ReST format""" |
| 686 msgs = sorted(six.itervalues(self._messages), key=lambda msg: msg.msgid) |
| 687 for msg in msgs: |
| 688 if not msg.may_be_emitted(): |
| 689 continue |
| 690 print(msg.format_help(checkerref=False)) |
| 691 print("") |
| 692 |
| 693 |
| 694 class ReportsHandlerMixIn(object): |
| 695 """a mix-in class containing all the reports and stats manipulation |
| 696 related methods for the main lint class |
| 697 """ |
| 698 def __init__(self): |
| 699 self._reports = collections.defaultdict(list) |
| 700 self._reports_state = {} |
| 701 |
| 702 def report_order(self): |
| 703 """ Return a list of reports, sorted in the order |
| 704 in which they must be called. |
| 705 """ |
| 706 return list(self._reports) |
| 707 |
| 708 def register_report(self, reportid, r_title, r_cb, checker): |
| 709 """register a report |
| 710 |
| 711 reportid is the unique identifier for the report |
| 712 r_title the report's title |
| 713 r_cb the method to call to make the report |
| 714 checker is the checker defining the report |
| 715 """ |
| 716 reportid = reportid.upper() |
| 717 self._reports[checker].append((reportid, r_title, r_cb)) |
| 718 |
| 719 def enable_report(self, reportid): |
| 720 """disable the report of the given id""" |
| 721 reportid = reportid.upper() |
| 722 self._reports_state[reportid] = True |
| 723 |
| 724 def disable_report(self, reportid): |
| 725 """disable the report of the given id""" |
| 726 reportid = reportid.upper() |
| 727 self._reports_state[reportid] = False |
| 728 |
| 729 def report_is_enabled(self, reportid): |
| 730 """return true if the report associated to the given identifier is |
| 731 enabled |
| 732 """ |
| 733 return self._reports_state.get(reportid, True) |
| 734 |
| 735 def make_reports(self, stats, old_stats): |
| 736 """render registered reports""" |
| 737 sect = Section('Report', |
| 738 '%s statements analysed.'% (self.stats['statement'])) |
| 739 for checker in self.report_order(): |
| 740 for reportid, r_title, r_cb in self._reports[checker]: |
| 741 if not self.report_is_enabled(reportid): |
| 742 continue |
| 743 report_sect = Section(r_title) |
| 744 try: |
| 745 r_cb(report_sect, stats, old_stats) |
| 746 except EmptyReport: |
| 747 continue |
| 748 report_sect.report_id = reportid |
| 749 sect.append(report_sect) |
| 750 return sect |
| 751 |
| 752 def add_stats(self, **kwargs): |
| 753 """add some stats entries to the statistic dictionary |
| 754 raise an AssertionError if there is a key conflict |
| 755 """ |
| 756 for key, value in six.iteritems(kwargs): |
| 757 if key[-1] == '_': |
| 758 key = key[:-1] |
| 759 assert key not in self.stats |
| 760 self.stats[key] = value |
| 761 return self.stats |
| 762 |
| 763 |
| 764 def expand_modules(files_or_modules, black_list): |
| 765 """take a list of files/modules/packages and return the list of tuple |
| 766 (file, module name) which have to be actually checked |
| 767 """ |
| 768 result = [] |
| 769 errors = [] |
| 770 for something in files_or_modules: |
| 771 if exists(something): |
| 772 # this is a file or a directory |
| 773 try: |
| 774 modname = '.'.join(modpath_from_file(something)) |
| 775 except ImportError: |
| 776 modname = splitext(basename(something))[0] |
| 777 if isdir(something): |
| 778 filepath = join(something, '__init__.py') |
| 779 else: |
| 780 filepath = something |
| 781 else: |
| 782 # suppose it's a module or package |
| 783 modname = something |
| 784 try: |
| 785 filepath = file_from_modpath(modname.split('.')) |
| 786 if filepath is None: |
| 787 errors.append({'key' : 'ignored-builtin-module', 'mod': modn
ame}) |
| 788 continue |
| 789 except (ImportError, SyntaxError) as ex: |
| 790 # FIXME p3k : the SyntaxError is a Python bug and should be |
| 791 # removed as soon as possible http://bugs.python.org/issue10588 |
| 792 errors.append({'key': 'fatal', 'mod': modname, 'ex': ex}) |
| 793 continue |
| 794 filepath = normpath(filepath) |
| 795 result.append({'path': filepath, 'name': modname, 'isarg': True, |
| 796 'basepath': filepath, 'basename': modname}) |
| 797 if not (modname.endswith('.__init__') or modname == '__init__') \ |
| 798 and '__init__.py' in filepath: |
| 799 for subfilepath in get_module_files(dirname(filepath), black_list): |
| 800 if filepath == subfilepath: |
| 801 continue |
| 802 submodname = '.'.join(modpath_from_file(subfilepath)) |
| 803 result.append({'path': subfilepath, 'name': submodname, |
| 804 'isarg': False, |
| 805 'basepath': filepath, 'basename': modname}) |
| 806 return result, errors |
| 807 |
| 808 |
| 809 class PyLintASTWalker(object): |
| 810 |
| 811 def __init__(self, linter): |
| 812 # callbacks per node types |
| 813 self.nbstatements = 1 |
| 814 self.visit_events = collections.defaultdict(list) |
| 815 self.leave_events = collections.defaultdict(list) |
| 816 self.linter = linter |
| 817 |
| 818 def _is_method_enabled(self, method): |
| 819 if not hasattr(method, 'checks_msgs'): |
| 820 return True |
| 821 for msg_desc in method.checks_msgs: |
| 822 if self.linter.is_message_enabled(msg_desc): |
| 823 return True |
| 824 return False |
| 825 |
| 826 def add_checker(self, checker): |
| 827 """walk to the checker's dir and collect visit and leave methods""" |
| 828 # XXX : should be possible to merge needed_checkers and add_checker |
| 829 vcids = set() |
| 830 lcids = set() |
| 831 visits = self.visit_events |
| 832 leaves = self.leave_events |
| 833 for member in dir(checker): |
| 834 cid = member[6:] |
| 835 if cid == 'default': |
| 836 continue |
| 837 if member.startswith('visit_'): |
| 838 v_meth = getattr(checker, member) |
| 839 # don't use visit_methods with no activated message: |
| 840 if self._is_method_enabled(v_meth): |
| 841 visits[cid].append(v_meth) |
| 842 vcids.add(cid) |
| 843 elif member.startswith('leave_'): |
| 844 l_meth = getattr(checker, member) |
| 845 # don't use leave_methods with no activated message: |
| 846 if self._is_method_enabled(l_meth): |
| 847 leaves[cid].append(l_meth) |
| 848 lcids.add(cid) |
| 849 visit_default = getattr(checker, 'visit_default', None) |
| 850 if visit_default: |
| 851 for cls in nodes.ALL_NODE_CLASSES: |
| 852 cid = cls.__name__.lower() |
| 853 if cid not in vcids: |
| 854 visits[cid].append(visit_default) |
| 855 # for now we have no "leave_default" method in Pylint |
| 856 |
| 857 def walk(self, astroid): |
| 858 """call visit events of astroid checkers for the given node, recurse on |
| 859 its children, then leave events. |
| 860 """ |
| 861 cid = astroid.__class__.__name__.lower() |
| 862 if astroid.is_statement: |
| 863 self.nbstatements += 1 |
| 864 # generate events for this node on each checker |
| 865 for cb in self.visit_events.get(cid, ()): |
| 866 cb(astroid) |
| 867 # recurse on children |
| 868 for child in astroid.get_children(): |
| 869 self.walk(child) |
| 870 for cb in self.leave_events.get(cid, ()): |
| 871 cb(astroid) |
| 872 |
| 873 |
| 874 PY_EXTS = ('.py', '.pyc', '.pyo', '.pyw', '.so', '.dll') |
| 875 |
| 876 def register_plugins(linter, directory): |
| 877 """load all module and package in the given directory, looking for a |
| 878 'register' function in each one, used to register pylint checkers |
| 879 """ |
| 880 imported = {} |
| 881 for filename in os.listdir(directory): |
| 882 base, extension = splitext(filename) |
| 883 if base in imported or base == '__pycache__': |
| 884 continue |
| 885 if extension in PY_EXTS and base != '__init__' or ( |
| 886 not extension and isdir(join(directory, base))): |
| 887 try: |
| 888 module = load_module_from_file(join(directory, filename)) |
| 889 except ValueError: |
| 890 # empty module name (usually emacs auto-save files) |
| 891 continue |
| 892 except ImportError as exc: |
| 893 print("Problem importing module %s: %s" % (filename, exc), |
| 894 file=sys.stderr) |
| 895 else: |
| 896 if hasattr(module, 'register'): |
| 897 module.register(linter) |
| 898 imported[base] = 1 |
| 899 |
| 900 def get_global_option(checker, option, default=None): |
| 901 """ Retrieve an option defined by the given *checker* or |
| 902 by all known option providers. |
| 903 |
| 904 It will look in the list of all options providers |
| 905 until the given *option* will be found. |
| 906 If the option wasn't found, the *default* value will be returned. |
| 907 """ |
| 908 # First, try in the given checker's config. |
| 909 # After that, look in the options providers. |
| 910 |
| 911 try: |
| 912 return getattr(checker.config, option.replace("-", "_")) |
| 913 except AttributeError: |
| 914 pass |
| 915 for provider in checker.linter.options_providers: |
| 916 for options in provider.options: |
| 917 if options[0] == option: |
| 918 return getattr(provider.config, option.replace("-", "_")) |
| 919 return default |
OLD | NEW |