diff options
author | Sylvain Th?nault <sylvain.thenault@logilab.fr> | 2010-04-16 17:51:44 +0200 |
---|---|---|
committer | Sylvain Th?nault <sylvain.thenault@logilab.fr> | 2010-04-16 17:51:44 +0200 |
commit | 9510476e61591849f1a209c6ec0b56122ac35555 (patch) | |
tree | deaf3fd5afca97b182a06b60c9a040a0b5f4c89d | |
parent | 78ecf6f9726aa1802a4103faa4ba06315423739f (diff) | |
download | pylint-9510476e61591849f1a209c6ec0b56122ac35555.tar.gz |
refactor messages and checker handling
* new ast walker, built at registration time
* properly handle dependencies between checkers instead of
priority based order
* only run checker necessary according to activated message
* much more simple command line: [en|dis]able-msg,
[en|dis]able-msg-cat and [en|dis]able-report superseeded
by [en|dis]able options
-> we should split current checkers into simpler checker with the
same name, and proper dependencies when needed
-rw-r--r-- | checkers/__init__.py | 18 | ||||
-rw-r--r-- | lint.py | 178 | ||||
-rw-r--r-- | utils.py | 158 |
3 files changed, 160 insertions, 194 deletions
diff --git a/checkers/__init__.py b/checkers/__init__.py index 84e4188..144b680 100644 --- a/checkers/__init__.py +++ b/checkers/__init__.py @@ -66,9 +66,7 @@ class BaseChecker(OptionsProviderMixIn, ASTWalker): """base class for checkers""" options = () - priority = -9 - enabled = True - may_be_disabled = True + needs_checkers = () name = None level = 1 @@ -86,20 +84,6 @@ class BaseChecker(OptionsProviderMixIn, ASTWalker): """add a message of a given type""" self.linter.add_message(msg_id, line, node, args) - def is_enabled(self): - """return true if the checker is enabled""" - return self.enabled - - def enable(self, enabled): - """enable / disable this checker if true / false is given - - it false values has no effect if the checker can't be disabled - """ - if not enabled and not self.may_be_disabled: - raise Exception("can't disable %s checker" % self.name) - if enabled or self.may_be_disabled: - self.enabled = enabled - def package_dir(self): """return the base directory for the analysed package""" return dirname(self.linter.base_file) @@ -111,6 +111,37 @@ MSGS = { 'Used when a bad value for an inline option is encountered.'), } + +class PyLintASTWalker(object): + def __init__(self): + # callbacks per node types + self.visit_events = {} + self.leave_events = {} + + def add_checker(self, checker): + for member in dir(checker): + if member.startswith('visit_'): + cbs = self.visit_events.setdefault(cid, []) + cbs.append(getattr(checker, member[6:])) + if member.startswith('leave_'): + cbs = self.leave_events.setdefault(cid, []) + cbs.insert(0, getattr(checker, member[6:])) + + def walk(self, astng): + """call visit events of astng checkers for the given node, recurse on + its children, then leave events. + """ + cid = node.__class__.__name__.lower() + # generate events for this node on each checker + for cb in self.visit_events[cid]: + cb(astng) + # recurse on children + for child in astng.get_children(): + self.walk(child) + for cb in self.leave_events[cid]: + cb(astng) + + class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, BaseRawChecker): """lint Python modules using external checkers. @@ -136,19 +167,6 @@ class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, 'help' : 'Add <file or directory> to the black list. It \ should be a base name, not a path. You may set this option multiple times.'}), - ('enable-checker', - {'type' : 'csv', 'metavar': '<checker ids>', - 'group': 'Messages control', - 'help' : 'Enable only checker(s) with the given id(s).\ - This option conflicts with the disable-checker option'}), - - ('disable-checker', - {'type' : 'csv', 'metavar': '<checker ids>', - 'group': 'Messages control', - 'help' : 'Enable all checker(s) except those with the \ - given id(s).\ - This option conflicts with the enable-checker option'}), - ('persistent', {'default': True, 'type' : 'yn', 'metavar' : '<y_or_n>', 'level': 1, @@ -214,25 +232,19 @@ This is used by the global evaluation report (R0004).'}), 'group': 'Reports', 'help' : 'Disable the report(s) with the given id(s).'}), - ('enable-msg-cat', - {'type' : 'string', 'metavar': '<msg cats>', - 'group': 'Messages control', - 'help' : 'Enable all messages in the listed categories (IRCWEF).'}), - - ('disable-msg-cat', - {'type' : 'string', 'metavar': '<msg cats>', 'default': 'I', - 'group': 'Messages control', - 'help' : 'Disable all messages in the listed categories (IRCWEF).'}), - - ('enable-msg', + ('enable', {'type' : 'csv', 'metavar': '<msg ids>', 'group': 'Messages control', - 'help' : 'Enable the message(s) with the given id(s).'}), + 'help' : 'Enable the message or category or checker with the ' + 'given id(s). You can either give multiple identifier ' + 'separated by comma (,) or put this option multiple time.'}), - ('disable-msg', + ('disable', {'type' : 'csv', 'metavar': '<msg ids>', 'group': 'Messages control', - 'help' : 'Disable the message(s) with the given id(s).'}), + 'help' : 'Disable themessage or category or checker with the ' + 'given id(s). You can either give multiple identifier ' + 'separated by comma (,) or put this option multiple time.'}), ) option_groups = ( @@ -261,9 +273,7 @@ This is used by the global evaluation report (R0004).'}), 'enable-report': self.enable_report, 'disable-report': self.disable_report, 'enable-msg': self.enable_message, - 'disable-msg': self.disable_message, - 'enable-msg-cat': self.enable_message_category, - 'disable-msg-cat': self.disable_message_category} + 'disable-msg': self.disable_message} full_version = '%%prog %s, \nastng %s, common %s\nPython %s' % ( version, astng_version, common_version, sys.version) OptionsManagerMixIn.__init__(self, usage=__doc__, @@ -318,11 +328,6 @@ This is used by the global evaluation report (R0004).'}), meth(value) elif opt_name == 'output-format': self.set_reporter(REPORTER_OPT_MAP[value.lower()]()) - elif opt_name in ('enable-checker', 'disable-checker'): - if not value: - return - checkerids = [v.lower() for v in check_csv(None, opt_name, value)] - self.enable_checkers(checkerids, opt_name == 'enable-checker') try: BaseRawChecker.set_option(self, opt_name, value, action, opt_dict) except UnsupportedAction: @@ -337,7 +342,7 @@ This is used by the global evaluation report (R0004).'}), checker is an object implementing IRawChecker or / and IASTNGChecker """ assert checker.priority <= 0, 'checker priority can\'t be >= 0' - self._checkers[checker.name] = checker + self._checkers.setdefault(checker.name, []).append(checker) if hasattr(checker, 'reports'): for r_id, r_title, r_cb in checker.reports: self.register_report(r_id, r_title, r_cb, checker) @@ -346,38 +351,14 @@ This is used by the global evaluation report (R0004).'}), self.register_messages(checker) checker.load_defaults() - def enable_checkers(self, listed, enabled): - """only enable/disable checkers from the given list""" - if enabled: # if we are activating a checker; deactivate them all first - for checker in self._checkers.values(): - if not checker.may_be_disabled: - continue - checker.enable(not enabled) - for checkerid in listed: - try: - checker = self._checkers[checkerid] - except KeyError: - raise Exception('no checker named %s' % checkerid) - checker.enable(enabled) - - def disable_noerror_checkers(self): - """disable all checkers without error messages, and the - 'miscellaneous' checker which can be safely deactivated in debug - mode - """ - for checker in self._checkers.values(): - if checker.name == 'miscellaneous': - checker.enable(False) - continue - # if checker is already explicitly disabled (e.g. rpython), don't - # enable it - if checker.enabled: - for msgid in getattr(checker, 'msgs', {}).keys(): - if msgid[0] == 'E': - checker.enable(True) - break - else: - checker.enable(False) + def disable_noerror_messages(self): + for msgcat, msgids in self._msgs_by_category.iteritems(): + if msgcat == 'E': + for msgid in msgids: + self.enable_message(msgid) + else: + for msgid in msgids: + self.disable_message(msgid) # block level option handling ############################################# # @@ -474,10 +455,22 @@ This is used by the global evaluation report (R0004).'}), self.reporter.include_ids = self.config.include_ids if not isinstance(files_or_modules, (list, tuple)): files_or_modules = (files_or_modules,) - checkers = sort_checkers(self._checkers.values()) + rawcheckers = [] + walker = PyLintASTWalker() + # + neededcheckers = set() + for checker in self._checkers.values(): + for msgid in checker.msgs: + if self._msgs_state.get(msgid, True): + neededcheckers.add(checker) + break # notify global begin - for checker in checkers: + for checker in sort_checkers(neededcheckers): checker.open() + if implements(checker, IASTNGChecker): + walker.add_checker(checker) + if implements(checker, IRawChecker) and checker is not self: #XXX + rawcheckers.append(checker) # build ast and check modules or packages for descr in self.expand_files(files_or_modules): modname, filepath = descr['name'], descr['path'] @@ -495,11 +488,11 @@ This is used by the global evaluation report (R0004).'}), # fix the current file (if the source file was not available or # if it's actually a c extension) self.current_file = astng.file - self.check_astng_module(astng, checkers) + self.check_astng_module(astng, walker, rawcheckers) # notify global end self.set_current_module('') checkers.reverse() - for checker in checkers: + for checker in checkers: checker.close() def expand_files(self, modules): @@ -548,8 +541,7 @@ This is used by the global evaluation report (R0004).'}), # traceback.print_exc() self.add_message('F0002', args=(ex.__class__, ex)) - - def check_astng_module(self, astng, checkers): + def check_astng_module(self, astng, walker, rawcheckers): """check a module from its astng representation, real work""" # call raw checkers if possible if not astng.pure_python: @@ -566,34 +558,13 @@ This is used by the global evaluation report (R0004).'}), orig_state = self._module_msgs_state.copy() self._module_msgs_state = {} self.collect_block_lines(astng, orig_state) - for checker in checkers: - if implements(checker, IRawChecker) and checker is not self: - stream.seek(0) - checker.process_module(stream) + for checker in rawcheckers: + stream.seek(0) + checker.process_module(stream) # generate events to astng checkers - self.astng_events(astng, [checker for checker in checkers - if implements(checker, IASTNGChecker)]) + walker.walk(astng) return True - def astng_events(self, astng, checkers, _reversed_checkers=None): - """generate event to astng checkers according to the current astng - node and recurse on its children - """ - if _reversed_checkers is None: - _reversed_checkers = checkers[:] - _reversed_checkers.reverse() - if astng.is_statement: - self.stats['statement'] += 1 - # generate events for this node on each checker - for checker in checkers: - checker.visit(astng) - # recurse on children - for child in astng.get_children(): - self.astng_events(child, checkers, _reversed_checkers) - for checker in _reversed_checkers: - checker.leave(astng) - - # IASTNGChecker interface ################################################# def open(self): @@ -900,13 +871,14 @@ been issued by analysing pylint output status code def cb_error_mode(self, *args, **kwargs): """error mode: - * checkers without error messages are disabled - * for others, only the ERROR messages are displayed + * disable all but error messages + * disable the 'miscellaneous' checker which can be safely deactivated in + debug * disable reports * do not save execution information """ - self.linter.disable_noerror_checkers() - self.linter.set_option('disable-msg-cat', 'WCRI') + self.linter.disable_noerror_messages() + self.disable_message('miscellaneous') self.linter.set_option('reports', False) self.linter.set_option('persistent', False) @@ -24,6 +24,7 @@ from os import linesep from logilab.common.textutils import normalize_text from logilab.common.configuration import rest_format_section from logilab.common.ureports import Section +from logilab.common.graph import ordered_nodes from logilab.astng import Module @@ -41,6 +42,8 @@ MSG_TYPES = { 'E' : 'error', 'F' : 'fatal' } +MSG_TYPES_LONG = dict([(v, k) for k, v in MSG_TYPES.iteritems()]) + MSG_TYPES_STATUS = { 'I' : 0, 'C' : 16, @@ -51,12 +54,14 @@ MSG_TYPES_STATUS = { } def sort_checkers(checkers): - """return a list of enabled checker sorted by priority""" - checkers = [checker for checker in checkers if checker.is_enabled()] - checkers.sort(lambda x, y: cmp(-x.priority, -y.priority) ) - return checkers - -def sort_msgs(msg_ids): + graph = {} + cls_instance = {} + for checker in checkers: + graph[cube.__class__] = set(checker.needs_checkers) + cls_instance[cube.__class__] = checker + return [cls_instance.get(cls) for cls in ordered_nodes(graph)] + +def sort_msgs(msgids): """sort message identifiers according to their category first""" msg_order = 'EWRCIF' def cmp_func(msgid1, msgid2): @@ -65,8 +70,8 @@ def sort_msgs(msg_ids): return cmp(msg_order.index(msgid1[0]), msg_order.index(msgid2[0])) else: return cmp(msgid1, msgid2) - msg_ids.sort(cmp_func) - return msg_ids + msgids.sort(cmp_func) + return msgids def get_module_and_frameid(node): """return the module name and the frame id in the module""" @@ -84,6 +89,12 @@ def get_module_and_frameid(node): obj.reverse() return module, '.'.join(obj) +def category_id(id): + id = id.upper() + if id in MSG_TYPES: + return id + return MSG_TYPES_LONG.get(id) + class Message: def __init__(self, checker, msgid, msg, descr): @@ -105,8 +116,7 @@ class MessagesHandlerMixIn: self._messages = {} self._msgs_state = {} self._module_msgs_state = {} # None - self._msg_cats_state = {} - self._module_msg_cats_state = None + self._msgs_by_category = {} self.msg_status = 0 def register_messages(self, checker): @@ -128,10 +138,11 @@ class MessagesHandlerMixIn: 'Inconsistent checker part in message id %r' % msgid chkid = msgid[1:3] self._messages[msgid] = Message(checker, msgid, msg, msgdescr) + self._msgs_by_category.setdefault(msgid[0], []).append(msgid) - def get_message_help(self, msg_id, checkerref=False): + def get_message_help(self, msgid, checkerref=False): """return the help string for the given message id""" - msg = self.check_message_id(msg_id) + msg = self.check_message_id(msgid) desc = normalize_text(' '.join(msg.descr.split()), indent=' ') if checkerref: desc += ' This message belongs to the %s checker.' % \ @@ -142,17 +153,25 @@ class MessagesHandlerMixIn: return ':%s: *%s*\n%s' % (msg.msgid, title, desc) return ':%s:\n%s' % (msg.msgid, desc) - def disable_message(self, msg_id, scope='package', line=None): + def disable_message(self, msgid, scope='package', line=None): """don't output message of the given id""" assert scope in ('package', 'module') - msg = self.check_message_id(msg_id) + catid = category_id(msgid) + if catid is not None: + self._activate_msgs_cat(catid, scope, line, False, 'I0011') + return + # msgid is a checker name? + if msgid.lower() in self._checkers: + self._activate_checker_msgs(msgid.lower(), True) + return + msg = self.check_message_id(msgid) if scope == 'module': assert line > 0 try: self._module_msgs_state[msg.msgid][line] = False except KeyError: self._module_msgs_state[msg.msgid] = {line: False} - if msg_id != 'I0011': + if msgid != 'I0011': self.add_message('I0011', line=line, args=msg.msgid) else: @@ -162,11 +181,20 @@ class MessagesHandlerMixIn: self.config.disable_msg = [mid for mid, val in msgs.items() if not val] - def enable_message(self, msg_id, scope='package', line=None): + def enable_message(self, msgid, scope='package', line=None): """reenable message of the given id""" assert scope in ('package', 'module') - msg = self.check_message_id(msg_id) - msg.checker.enabled = True # ensure the related checker is enabled + catid = category_id(msgid) + # msgid is a category? + if catid is not None: + self._activate_msgs_cat(catid, scope, line, True, 'I0012') + return + # msgid is a checker name? + if msgid.lower() in self._checkers: + self._activate_checker_msgs(msgid.lower(), True) + return + # msgid is a msgid. + msg = self.check_message_id(msgid) if scope == 'module': assert line > 0 try: @@ -180,59 +208,41 @@ class MessagesHandlerMixIn: # sync configuration object self.config.enable_msg = [mid for mid, val in msgs.items() if val] - def _cat_ids(self, categories): - for catid in categories: - catid = catid.upper() - if not catid in MSG_TYPES: - raise Exception('Unknown category identifier %s' % catid) - yield catid + def _activate_checker_msgs(self, checkername, value): + for checker in self._checkers[checkername]: + for msgid in checker.msgs: + msgsdict[msgid] = value - def disable_message_category(self, categories, scope='package', line=None): - """don't output message in the given category""" + def _activate_msgs_cat(self, catid, scope, line, value, warnmsgid): assert scope in ('package', 'module') - for catid in self._cat_ids(categories): - if scope == 'module': - self.add_message('I0011', line=line, args=catid) - self._module_msg_cats_state[catid] = False - else: - self._msg_cats_state[catid] = False - - def enable_message_category(self, categories, scope='package', line=None): - """reenable message of the given category""" - assert scope in ('package', 'module') - for catid in self._cat_ids(categories): - if scope == 'module': - self.add_message('I0012', line=line, args=catid) - self._module_msg_cats_state[catid] = True - else: - self._msg_cats_state[catid] = True + if scope == 'module': + self.add_message(warnmsgid, line=line, args=catid) + msgsdict = self._module_msgs_state + else: + msgsdict = self._msgs_state + for msgid in self._msgs_by_category.get(catid): + msgsdict[msgid] = value - def check_message_id(self, msg_id): + def check_message_id(self, msgid): """raise UnknownMessage if the message id is not defined""" - msg_id = msg_id.upper() + msgid = msgid.upper() try: - return self._messages[msg_id] + return self._messages[msgid] except KeyError: - raise UnknownMessage('No such message id %s' % msg_id) + raise UnknownMessage('No such message id %s' % msgid) - def is_message_enabled(self, msg_id, line=None): + def is_message_enabled(self, msgid, line=None): """return true if the message associated to the given message id is enabled """ - try: - if not self._module_msg_cats_state[msg_id[0]]: - return False - except (KeyError, TypeError): - if not self._msg_cats_state.get(msg_id[0], True): - return False if line is None: - return self._msgs_state.get(msg_id, True) + return self._msgs_state.get(msgid, True) try: - return self._module_msgs_state[msg_id][line] + return self._module_msgs_state[msgid][line] except (KeyError, TypeError): - return self._msgs_state.get(msg_id, True) + return self._msgs_state.get(msgid, True) - def add_message(self, msg_id, line=None, node=None, args=None): + def add_message(self, msgid, line=None, node=None, args=None): """add the message corresponding to the given id. If provided, msg is expanded using args @@ -243,18 +253,18 @@ class MessagesHandlerMixIn: if line is None and node is not None: line = node.fromlineno # should this message be displayed - if not self.is_message_enabled(msg_id, line): + if not self.is_message_enabled(msgid, line): return # update stats - msg_cat = MSG_TYPES[msg_id[0]] - self.msg_status |= MSG_TYPES_STATUS[msg_id[0]] + msg_cat = MSG_TYPES[msgid[0]] + self.msg_status |= MSG_TYPES_STATUS[msgid[0]] self.stats[msg_cat] += 1 self.stats['by_module'][self.current_name][msg_cat] += 1 try: - self.stats['by_msg'][msg_id] += 1 + self.stats['by_msg'][msgid] += 1 except KeyError: - self.stats['by_msg'][msg_id] = 1 - msg = self._messages[msg_id].msg + self.stats['by_msg'][msgid] = 1 + msg = self._messages[msgid].msg # expand message ? if args: msg %= args @@ -266,13 +276,13 @@ class MessagesHandlerMixIn: module, obj = get_module_and_frameid(node) path = node.root().file # add the message - self.reporter.add_message(msg_id, (path, module, obj, line or 1), msg) + self.reporter.add_message(msgid, (path, module, obj, line or 1), msg) def help_message(self, msgids): """display help messages for the given message identifiers""" - for msg_id in msgids: + for msgid in msgids: try: - print self.get_message_help(msg_id, True) + print self.get_message_help(msgid, True) print except UnknownMessage, ex: print ex @@ -281,8 +291,8 @@ class MessagesHandlerMixIn: def list_checkers_messages(self, checker): """print checker's messages in reST format""" - for msg_id in sort_msgs(checker.msgs.keys()): - print self.get_message_help(msg_id, False) + for msgid in sort_msgs(checker.msgs.keys()): + print self.get_message_help(msgid, False) def print_full_documentation(self): """output a full documentation in ReST format""" @@ -338,13 +348,13 @@ class MessagesHandlerMixIn: def list_sorted_messages(self): """output full sorted messages list in ReST format""" - msg_ids = [] + msgids = [] for checker in self._checkers.values(): - for msg_id in checker.msgs.keys(): - msg_ids.append(msg_id) - msg_ids.sort() - for msg_id in msg_ids: - print self.get_message_help(msg_id, False) + for msgid in checker.msgs.keys(): + msgids.append(msgid) + msgids.sort() + for msgid in msgids: + print self.get_message_help(msgid, False) print |