diff options
author | Torsten Marek <tmarek@google.com> | 2012-11-12 14:06:26 +0100 |
---|---|---|
committer | Torsten Marek <tmarek@google.com> | 2012-11-12 14:06:26 +0100 |
commit | f235861d6ceeab1efd82f0fca4dd61893cf1ac97 (patch) | |
tree | 1b2e3a08d7a0ee9a8a6171e1c4b3988fcd669fc2 | |
parent | bd8bd1e42cebd5d93505cc299ee0b619ea7fd90d (diff) | |
download | pylint-git-f235861d6ceeab1efd82f0fca4dd61893cf1ac97.tar.gz |
Add messages I0020 and I0021 for reporting of suppressed messages and useless suppression pragmas. Closes #110840
Both messages are disabled by default, and only emitted after all other
checkers have been processed.
-rw-r--r-- | ChangeLog | 6 | ||||
-rw-r--r-- | lint.py | 32 | ||||
-rw-r--r-- | test/input/func_block_disable_msg.py | 9 | ||||
-rw-r--r-- | test/input/func_i0020.py | 8 | ||||
-rw-r--r-- | test/messages/func_i0011.txt | 2 | ||||
-rw-r--r-- | test/messages/func_i0020.txt | 2 | ||||
-rw-r--r-- | test/unittest_lint.py | 31 | ||||
-rw-r--r-- | utils.py | 28 |
8 files changed, 114 insertions, 4 deletions
@@ -9,9 +9,11 @@ ChangeLog for PyLint 'disable-all' inline directive in favour of 'skip-file' (patch by A.Fayolle) + * #110840: Add messages I0020 and I0021 for reporting of suppressed messages + and useless suppression pragmas. (patch by Torsten Marek) - * Changed the regular expression for inline options so that - it must be preceeded by a # (patch by Torsten Marek) + * Changed the regular expression for inline options so that it must be + preceeded by a # (patch by Torsten Marek) 2012-10-05 -- 0.26.0 * #106534: add --ignore-imports option to code similarity checking @@ -131,6 +131,16 @@ MSGS = { 'You should preferably use "pylint:skip-file" as this directive ' 'has a less confusing name. Do this only if you are sure that all ' 'people running Pylint on your code have version >= 0.26'), + 'I0020': ('Suppressed %s (from line %d)', + 'suppressed-message', + 'A message was triggered on a line, but suppressed explicitly ' + 'by a disable= comment in the file. This message is not ' + 'generated for messages that are ignored due to configuration ' + 'settings.'), + 'I0021': ('Useless suppression of %s', + 'useless-suppression', + 'Reported when a message is explicitly disabled for a line or ' + 'a block of code, but never triggered.'), 'E0001': ('%s', @@ -485,6 +495,7 @@ This is used by the global evaluation report (RP0004).'}), firstchildlineno = last for msgid, lines in msg_state.iteritems(): for lineno, state in lines.items(): + original_lineno = lineno if first <= lineno <= last: if lineno > firstchildlineno: state = True @@ -495,6 +506,9 @@ This is used by the global evaluation report (RP0004).'}), if not line in self._module_msgs_state.get(msgid, ()): if line in lines: # state change in the same block state = lines[line] + original_lineno = line + if not state: + self._suppression_mapping[(msgid, line)] = original_lineno try: self._module_msgs_state[msgid][line] = state except KeyError: @@ -599,6 +613,8 @@ This is used by the global evaluation report (RP0004).'}), if modname: self._module_msgs_state = {} self._module_msg_cats_state = {} + self._raw_module_msgs_state = {} + self._ignored_msgs = {} def get_astng(self, filepath, modname): """return a astng representation for a module""" @@ -626,8 +642,11 @@ This is used by the global evaluation report (RP0004).'}), if self._ignore_file: return False # walk ast to collect line numbers + for msg, lines in self._module_msgs_state.iteritems(): + self._raw_module_msgs_state[msg] = lines.copy() orig_state = self._module_msgs_state.copy() self._module_msgs_state = {} + self._suppression_mapping = {} self.collect_block_lines(astng, orig_state) for checker in rawcheckers: checker.process_module(astng) @@ -650,6 +669,7 @@ This is used by the global evaluation report (RP0004).'}), if persistent run, pickle results for later comparison """ + self._add_suppression_messages() if self.base_name is not None: # load previous results if any previous_stats = config.load_results(self.base_name) @@ -670,6 +690,16 @@ This is used by the global evaluation report (RP0004).'}), # specific reports ######################################################## + def _add_suppression_messages(self): + for warning, lines in self._raw_module_msgs_state.iteritems(): + for line, enable in lines.iteritems(): + if not enable and (warning, line) not in self._ignored_msgs: + self.add_message('I0021', line, None, (warning,)) + + for (warning, from_), lines in self._ignored_msgs.iteritems(): + for line in lines: + self.add_message('I0020', line, None, (warning, from_)) + def report_evaluation(self, sect, stats, previous_stats): """make the global evaluation report""" # check with at least check 1 statements (usually 0 when there is a @@ -908,6 +938,8 @@ are done by default'''}), level=1) # read configuration linter.disable('W0704') + linter.disable('I0020') + linter.disable('I0021') linter.read_config_file() # is there some additional plugins in the file configuration, in config_parser = linter.cfgfile_parser diff --git a/test/input/func_block_disable_msg.py b/test/input/func_block_disable_msg.py index caf2d5b62..1a43ede62 100644 --- a/test/input/func_block_disable_msg.py +++ b/test/input/func_block_disable_msg.py @@ -101,6 +101,15 @@ class Foo(object): # error print self.blip + def meth10(self): + """Test double disable""" + # pylint: disable=E1101 + # no error + print self.bla + # pylint: disable=E1101 + print self.blu + + class ClassLevelMessage(object): """shouldn't display to much attributes/not enough methods messages """ diff --git a/test/input/func_i0020.py b/test/input/func_i0020.py new file mode 100644 index 000000000..8c0fe9f5c --- /dev/null +++ b/test/input/func_i0020.py @@ -0,0 +1,8 @@ +"""Test for reporting of suppressed messages.""" + +__revision__ = 0 + +def suppressed(): + """A function with an unused variable.""" + # pylint: disable=W0612 + var = 0 diff --git a/test/messages/func_i0011.txt b/test/messages/func_i0011.txt index 667e36401..4b5ef231f 100644 --- a/test/messages/func_i0011.txt +++ b/test/messages/func_i0011.txt @@ -1,2 +1,2 @@ I: 1: Locally disabling W0404 - +I: 1: Useless suppression of W0404 diff --git a/test/messages/func_i0020.txt b/test/messages/func_i0020.txt new file mode 100644 index 000000000..5a9ef9737 --- /dev/null +++ b/test/messages/func_i0020.txt @@ -0,0 +1,2 @@ +I: 7: Locally disabling W0612 +I: 8: Suppressed W0612 (from line 7) diff --git a/test/unittest_lint.py b/test/unittest_lint.py index 533aa94a6..2043ca64d 100644 --- a/test/unittest_lint.py +++ b/test/unittest_lint.py @@ -26,7 +26,9 @@ from logilab.common.compat import reload from pylint import config from pylint.lint import PyLinter, Run, UnknownMessage, preprocess_options, \ ArgumentPreprocessingError -from pylint.utils import sort_msgs, PyLintASTWalker +from pylint.utils import sort_msgs, PyLintASTWalker, MSG_STATE_SCOPE_CONFIG, \ + MSG_STATE_SCOPE_MODULE + from pylint import checkers class SortMessagesTC(TestCase): @@ -131,6 +133,21 @@ class PyLinterTC(TestCase): self.assertTrue(linter.is_message_enabled('C0121')) self.assertTrue(linter.is_message_enabled('C0121', line=1)) + def test_message_state_scope(self): + linter = self.linter + linter.open() + linter.disable('C0121') + self.assertEqual(MSG_STATE_SCOPE_CONFIG, + linter.get_message_state_scope('C0121')) + linter.disable('W0101', scope='module', line=3) + self.assertEqual(MSG_STATE_SCOPE_CONFIG, + linter.get_message_state_scope('C0121')) + self.assertEqual(MSG_STATE_SCOPE_MODULE, + linter.get_message_state_scope('W0101', 3)) + linter.enable('W0102', scope='module', line=3) + self.assertEqual(MSG_STATE_SCOPE_MODULE, + linter.get_message_state_scope('W0102', 3)) + def test_enable_message_block(self): linter = self.linter linter.open() @@ -140,6 +157,7 @@ class PyLinterTC(TestCase): linter.process_module(astng) orig_state = linter._module_msgs_state.copy() linter._module_msgs_state = {} + linter._suppression_mapping = {} linter.collect_block_lines(astng, orig_state) # global (module level) self.assertTrue(linter.is_message_enabled('W0613')) @@ -177,6 +195,17 @@ class PyLinterTC(TestCase): self.assertTrue(linter.is_message_enabled('E1101', 75)) self.assertTrue(linter.is_message_enabled('E1101', 77)) + self.assertEqual(17, linter._suppression_mapping['W0613', 18]) + self.assertEqual(30, linter._suppression_mapping['E1101', 33]) + self.assert_(('E1101', 46) not in linter._suppression_mapping) + self.assertEqual(1, linter._suppression_mapping['C0302', 18]) + self.assertEqual(1, linter._suppression_mapping['C0302', 50]) + # This is tricky. While the disable in line 106 is disabling + # both 108 and 110, this is usually not what the user wanted. + # Therefore, we report the closest previous disable comment. + self.assertEqual(106, linter._suppression_mapping['E1101', 108]) + self.assertEqual(109, linter._suppression_mapping['E1101', 110]) + def test_enable_by_symbol(self): """messages can be controlled by symbolic names. @@ -57,6 +57,8 @@ MSG_TYPES_STATUS = { } _MSG_ORDER = 'EWRCIF' +MSG_STATE_SCOPE_CONFIG = 0 +MSG_STATE_SCOPE_MODULE = 1 def sort_msgs(msgids): """sort message identifiers according to their category first""" @@ -115,8 +117,10 @@ class MessagesHandlerMixIn: self._messages_by_symbol = {} self._msgs_state = {} self._module_msgs_state = {} # None + self._raw_module_msgs_state = {} self._msgs_by_category = {} self.msg_status = 0 + self._ignored_msgs = {} def register_messages(self, checker): """register a dictionary of messages @@ -260,6 +264,14 @@ class MessagesHandlerMixIn: except KeyError: raise UnknownMessage('No such message id %s' % msgid) + def get_message_state_scope(self, msgid, line=None): + """Returns the scope at which a message was enabled/disabled.""" + try: + if line in self._module_msgs_state[msgid]: + return MSG_STATE_SCOPE_MODULE + except (KeyError, TypeError): + return MSG_STATE_SCOPE_CONFIG + def is_message_enabled(self, msgid, line=None): """return true if the message associated to the given message id is enabled @@ -275,6 +287,20 @@ class MessagesHandlerMixIn: except (KeyError, TypeError): return self._msgs_state.get(msgid, True) + def handle_ignored_message(self, state_scope, msgid, line, node, args): + """Report an ignored message. + + state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG, + depending on whether the message was disabled locally in the module, + or globally. The other arguments are the same as for add_message. + """ + if state_scope == MSG_STATE_SCOPE_MODULE: + try: + orig_line = self._suppression_mapping[(msgid, line)] + self._ignored_msgs.setdefault((msgid, orig_line), set()).add(line) + except KeyError: + pass + def add_message(self, msgid, line=None, node=None, args=None): """add the message corresponding to the given id. @@ -291,6 +317,8 @@ class MessagesHandlerMixIn: col_offset = None # should this message be displayed if not self.is_message_enabled(msgid, line): + self.handle_ignored_message( + self.get_message_state_scope(msgid, line), msgid, line, node, args) return # update stats msg_cat = MSG_TYPES[msgid[0]] |