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.

authortmarek@google.com
changeset2fac75f47913
branchdefault
phasepublic
hiddenno
parent revision#73472a5cb03a Change the regular expression for inline options so that a preceeding # is not optional.
child revision#968110ad5426 Make dot output for import graph predictable and not depend
files modified by this revision
ChangeLog
lint.py
test/input/func_block_disable_msg.py
test/input/func_i0020.py
test/messages/func_i0011.txt
test/messages/func_i0020.txt
test/unittest_lint.py
utils.py
# HG changeset patch
# User tmarek@google.com
# Date 1352725586 -3600
# Mon Nov 12 14:06:26 2012 +0100
# Node ID 2fac75f47913c74593fd0baa6d9a634fdd512449
# Parent 73472a5cb03a41ba187846a0a718ff25415d233f
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.

diff --git a/ChangeLog b/ChangeLog
@@ -7,13 +7,15 @@
1 
2      * #105327: add support for --disable=all option and deprecate the
3        'disable-all' inline directive in favour of 'skip-file' (patch by
4        A.Fayolle)
5 
6 +    * #110840: Add messages I0020 and I0021 for reporting of suppressed messages
7 +      and useless suppression pragmas. (patch by Torsten Marek)
8 
9 -    * Changed the regular expression for inline options so that
10 -      it must be preceeded by a # (patch by Torsten Marek)
11 +    * Changed the regular expression for inline options so that it must be
12 +      preceeded by a # (patch by Torsten Marek)
13 
14  2012-10-05  --  0.26.0
15      * #106534: add --ignore-imports option to code similarity checking
16        and 'symilar' command line tool (patch by Ry4an Brase)
17 
diff --git a/lint.py b/lint.py
@@ -129,10 +129,20 @@
18      'I0014': ('Used deprecated directive "pylint:disable-all" or "pylint:disable=all"',
19                'deprecated-disable-all',
20                'You should preferably use "pylint:skip-file" as this directive '
21                'has a less confusing name. Do this only if you are sure that all '
22                'people running Pylint on your code have version >= 0.26'),
23 +    'I0020': ('Suppressed %s (from line %d)',
24 +              'suppressed-message',
25 +              'A message was triggered on a line, but suppressed explicitly '
26 +              'by a disable= comment in the file. This message is not '
27 +              'generated for messages that are ignored due to configuration '
28 +              'settings.'),
29 +    'I0021': ('Useless suppression of %s',
30 +              'useless-suppression',
31 +              'Reported when a message is explicitly disabled for a line or '
32 +              'a block of code, but never triggered.'),
33 
34 
35      'E0001': ('%s',
36                'syntax-error',
37                'Used when a syntax error is raised for a module.'),
@@ -483,20 +493,24 @@
38              firstchildlineno = node.body[0].fromlineno
39          else:
40              firstchildlineno = last
41          for msgid, lines in msg_state.iteritems():
42              for lineno, state in lines.items():
43 +                original_lineno = lineno
44                  if first <= lineno <= last:
45                      if lineno > firstchildlineno:
46                          state = True
47                      # set state for all lines for this block
48                      first, last = node.block_range(lineno)
49                      for line in xrange(first, last+1):
50                          # do not override existing entries
51                          if not line in self._module_msgs_state.get(msgid, ()):
52                              if line in lines: # state change in the same block
53                                  state = lines[line]
54 +                                original_lineno = line
55 +                            if not state:
56 +                                self._suppression_mapping[(msgid, line)] = original_lineno
57                              try:
58                                  self._module_msgs_state[msgid][line] = state
59                              except KeyError:
60                                  self._module_msgs_state[msgid] = {line: state}
61                      del lines[lineno]
@@ -597,10 +611,12 @@
62          # for every analyzed module (the problem stands with localized
63          # messages which are only detected in the .close step)
64          if modname:
65              self._module_msgs_state = {}
66              self._module_msg_cats_state = {}
67 +            self._raw_module_msgs_state = {}
68 +            self._ignored_msgs = {}
69 
70      def get_astng(self, filepath, modname):
71          """return a astng representation for a module"""
72          try:
73              return MANAGER.astng_from_file(filepath, modname, source=True)
@@ -624,12 +640,15 @@
74              # level options
75              self.process_module(astng)
76              if self._ignore_file:
77                  return False
78              # walk ast to collect line numbers
79 +            for msg, lines in self._module_msgs_state.iteritems():
80 +                self._raw_module_msgs_state[msg] = lines.copy()
81              orig_state = self._module_msgs_state.copy()
82              self._module_msgs_state = {}
83 +            self._suppression_mapping = {}
84              self.collect_block_lines(astng, orig_state)
85              for checker in rawcheckers:
86                  checker.process_module(astng)
87          # generate events to astng checkers
88          walker.walk(astng)
@@ -648,10 +667,11 @@
89      def close(self):
90          """close the whole package /module, it's time to make reports !
91 
92          if persistent run, pickle results for later comparison
93          """
94 +        self._add_suppression_messages()
95          if self.base_name is not None:
96              # load previous results if any
97              previous_stats = config.load_results(self.base_name)
98              # XXX code below needs refactoring to be more reporter agnostic
99              self.reporter.on_close(self.stats, previous_stats)
@@ -668,10 +688,20 @@
100              if self.config.persistent:
101                  config.save_results(self.stats, self.base_name)
102 
103      # specific reports ########################################################
104 
105 +    def _add_suppression_messages(self):
106 +        for warning, lines in self._raw_module_msgs_state.iteritems():
107 +            for line, enable in lines.iteritems():
108 +                if not enable and (warning, line) not in self._ignored_msgs:
109 +                    self.add_message('I0021', line, None, (warning,))
110 +
111 +        for (warning, from_), lines in self._ignored_msgs.iteritems():
112 +            for line in lines:
113 +                self.add_message('I0020', line, None, (warning, from_))
114 +
115      def report_evaluation(self, sect, stats, previous_stats):
116          """make the global evaluation report"""
117          # check with at least check 1 statements (usually 0 when there is a
118          # syntax error preventing pylint from further processing)
119          if stats['statement'] == 0:
@@ -906,10 +936,12 @@
120  'status 1 to 16 will be bit-ORed so you can know which different categories has\n'
121  'been issued by analysing pylint output status code\n',
122          level=1)
123          # read configuration
124          linter.disable('W0704')
125 +        linter.disable('I0020')
126 +        linter.disable('I0021')
127          linter.read_config_file()
128          # is there some additional plugins in the file configuration, in
129          config_parser = linter.cfgfile_parser
130          if config_parser.has_option('MASTER', 'load-plugins'):
131              plugins = splitstrip(config_parser.get('MASTER', 'load-plugins'))
diff --git a/test/input/func_block_disable_msg.py b/test/input/func_block_disable_msg.py
@@ -99,10 +99,19 @@
132          print self.blu
133          # pylint: enable=E1101
134          # error
135          print self.blip
136 
137 +    def meth10(self):
138 +        """Test double disable"""
139 +        # pylint: disable=E1101
140 +        # no error
141 +        print self.bla
142 +        # pylint: disable=E1101
143 +        print self.blu
144 +
145 +
146  class ClassLevelMessage(object):
147      """shouldn't display to much attributes/not enough methods messages
148      """
149      # pylint: disable=R0902,R0903
150 
diff --git a/test/input/func_i0020.py b/test/input/func_i0020.py
@@ -0,0 +1,8 @@
151 +"""Test for reporting of suppressed messages."""
152 +
153 +__revision__ = 0
154 +
155 +def suppressed():
156 +    """A function with an unused variable."""
157 +    # pylint: disable=W0612
158 +    var = 0
diff --git a/test/messages/func_i0011.txt b/test/messages/func_i0011.txt
@@ -1,2 +1,2 @@
159  I:  1: Locally disabling W0404
160 -
161 +I:  1: Useless suppression of W0404
diff --git a/test/messages/func_i0020.txt b/test/messages/func_i0020.txt
@@ -0,0 +1,2 @@
162 +I:  7: Locally disabling W0612
163 +I:  8: Suppressed W0612 (from line 7)
diff --git a/test/unittest_lint.py b/test/unittest_lint.py
@@ -24,11 +24,13 @@
164  from logilab.common.compat import reload
165 
166  from pylint import config
167  from pylint.lint import PyLinter, Run, UnknownMessage, preprocess_options, \
168       ArgumentPreprocessingError
169 -from pylint.utils import sort_msgs, PyLintASTWalker
170 +from pylint.utils import sort_msgs, PyLintASTWalker, MSG_STATE_SCOPE_CONFIG, \
171 +     MSG_STATE_SCOPE_MODULE
172 +
173  from pylint import checkers
174 
175  class SortMessagesTC(TestCase):
176 
177      def test(self):
@@ -129,19 +131,35 @@
178          linter.enable('C', scope='module', line=1)
179          self.assertTrue(linter.is_message_enabled('W0101'))
180          self.assertTrue(linter.is_message_enabled('C0121'))
181          self.assertTrue(linter.is_message_enabled('C0121', line=1))
182 
183 +    def test_message_state_scope(self):
184 +        linter = self.linter
185 +        linter.open()
186 +        linter.disable('C0121')
187 +        self.assertEqual(MSG_STATE_SCOPE_CONFIG,
188 +                         linter.get_message_state_scope('C0121'))
189 +        linter.disable('W0101', scope='module', line=3)
190 +        self.assertEqual(MSG_STATE_SCOPE_CONFIG,
191 +                         linter.get_message_state_scope('C0121'))
192 +        self.assertEqual(MSG_STATE_SCOPE_MODULE,
193 +                         linter.get_message_state_scope('W0101', 3))
194 +        linter.enable('W0102', scope='module', line=3)
195 +        self.assertEqual(MSG_STATE_SCOPE_MODULE,
196 +                         linter.get_message_state_scope('W0102', 3))
197 +
198      def test_enable_message_block(self):
199          linter = self.linter
200          linter.open()
201          filepath = join(INPUTDIR, 'func_block_disable_msg.py')
202          linter.set_current_module('func_block_disable_msg')
203          astng = linter.get_astng(filepath, 'func_block_disable_msg')
204          linter.process_module(astng)
205          orig_state = linter._module_msgs_state.copy()
206          linter._module_msgs_state = {}
207 +        linter._suppression_mapping = {}
208          linter.collect_block_lines(astng, orig_state)
209          # global (module level)
210          self.assertTrue(linter.is_message_enabled('W0613'))
211          self.assertTrue(linter.is_message_enabled('E1101'))
212          # meth1
@@ -175,10 +193,21 @@
213          self.assertFalse(linter.is_message_enabled('E1101', 70))
214          self.assertTrue(linter.is_message_enabled('E1101', 72))
215          self.assertTrue(linter.is_message_enabled('E1101', 75))
216          self.assertTrue(linter.is_message_enabled('E1101', 77))
217 
218 +        self.assertEqual(17, linter._suppression_mapping['W0613', 18])
219 +        self.assertEqual(30, linter._suppression_mapping['E1101', 33])
220 +        self.assert_(('E1101', 46) not in linter._suppression_mapping)
221 +        self.assertEqual(1, linter._suppression_mapping['C0302', 18])
222 +        self.assertEqual(1, linter._suppression_mapping['C0302', 50])
223 +        # This is tricky. While the disable in line 106 is disabling
224 +        # both 108 and 110, this is usually not what the user wanted.
225 +        # Therefore, we report the closest previous disable comment.
226 +        self.assertEqual(106, linter._suppression_mapping['E1101', 108])
227 +        self.assertEqual(109, linter._suppression_mapping['E1101', 110])
228 +
229      def test_enable_by_symbol(self):
230          """messages can be controlled by symbolic names.
231 
232          The state is consistent across symbols and numbers.
233          """
diff --git a/utils.py b/utils.py
@@ -55,10 +55,12 @@
234      'E' : 2,
235      'F' : 1
236      }
237 
238  _MSG_ORDER = 'EWRCIF'
239 +MSG_STATE_SCOPE_CONFIG = 0
240 +MSG_STATE_SCOPE_MODULE = 1
241 
242  def sort_msgs(msgids):
243      """sort message identifiers according to their category first"""
244      msgs = {}
245      for msg in msgids:
@@ -113,12 +115,14 @@
246          self._messages = {}
247          # dictionary from string symbolic id to Message object.
248          self._messages_by_symbol = {}
249          self._msgs_state = {}
250          self._module_msgs_state = {} # None
251 +        self._raw_module_msgs_state = {}
252          self._msgs_by_category = {}
253          self.msg_status = 0
254 +        self._ignored_msgs = {}
255 
256      def register_messages(self, checker):
257          """register a dictionary of messages
258 
259          Keys are message ids, values are a 2-uple with the message type and the
@@ -258,10 +262,18 @@
260          try:
261              return self._messages[msgid]
262          except KeyError:
263              raise UnknownMessage('No such message id %s' % msgid)
264 
265 +    def get_message_state_scope(self, msgid, line=None):
266 +        """Returns the scope at which a message was enabled/disabled."""
267 +        try:
268 +            if line in self._module_msgs_state[msgid]:
269 +                return MSG_STATE_SCOPE_MODULE
270 +        except (KeyError, TypeError):
271 +            return MSG_STATE_SCOPE_CONFIG
272 +
273      def is_message_enabled(self, msgid, line=None):
274          """return true if the message associated to the given message id is
275          enabled
276 
277          msgid may be either a numeric or symbolic message id.
@@ -273,10 +285,24 @@
278          try:
279              return self._module_msgs_state[msgid][line]
280          except (KeyError, TypeError):
281              return self._msgs_state.get(msgid, True)
282 
283 +    def handle_ignored_message(self, state_scope, msgid, line, node, args):
284 +        """Report an ignored message.
285 +
286 +        state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG,
287 +        depending on whether the message was disabled locally in the module,
288 +        or globally. The other arguments are the same as for add_message.
289 +        """
290 +        if state_scope == MSG_STATE_SCOPE_MODULE:
291 +            try:
292 +                orig_line = self._suppression_mapping[(msgid, line)]
293 +                self._ignored_msgs.setdefault((msgid, orig_line), set()).add(line)
294 +            except KeyError:
295 +                pass
296 +
297      def add_message(self, msgid, line=None, node=None, args=None):
298          """add the message corresponding to the given id.
299 
300          If provided, msg is expanded using args
301 
@@ -289,10 +315,12 @@
302              col_offset = node.col_offset # XXX measured in bytes for utf-8, divide by two for chars?
303          else:
304              col_offset = None
305          # should this message be displayed
306          if not self.is_message_enabled(msgid, line):
307 +            self.handle_ignored_message(
308 +                self.get_message_state_scope(msgid, line), msgid, line, node, args)
309              return
310          # update stats
311          msg_cat = MSG_TYPES[msgid[0]]
312          self.msg_status |= MSG_TYPES_STATUS[msgid[0]]
313          self.stats[msg_cat] += 1