Make sure that pragmas that apply to whole lines are interpreted literally, so that

their scope is not extended to the whole scope if they occur at the beginning of a scope.

Closes #123285

authorTorsten Marek <tmarek@google.com>
changeset2748816ac5de
branchdefault
phasepublic
hiddenno
parent revision#7080486d663c Improve the warning message for E1124[redundant-keyword-arg].
child revision#4c4a8edd9115 Lambdas can contain yields, do not warn about them.
files modified by this revision
ChangeLog
checkers/base.py
checkers/format.py
lint.py
test/input/func_disable_linebased.py
test/messages/func_disable_linebased.txt
utils.py
# HG changeset patch
# User Torsten Marek <tmarek@google.com>
# Date 1364576999 -3600
# Fri Mar 29 18:09:59 2013 +0100
# Node ID 2748816ac5de522bcb510f665de73425477078eb
# Parent 7080486d663cc613c7e5a7e73b4f921c767ed0b6
Make sure that pragmas that apply to whole lines are interpreted literally, so that
their scope is not extended to the whole scope if they occur at the beginning of a
scope.

Closes #123285

diff --git a/ChangeLog b/ChangeLog
@@ -12,10 +12,13 @@
1        report
2 
3      * #124662: fix name error causing crash when symbols are included in output
4        messages
5 
6 +    * #123285: apply pragmas for warnings attached to lines to
7 +      physical source code lines
8 +
9      * Simplify checks for dangerous default values by unifying tests
10        for all different mutable compound literals.
11 
12      * Improve the description for E1124[redundant-keyword-arg]
13 
diff --git a/checkers/base.py b/checkers/base.py
@@ -540,11 +540,11 @@
14      @check_messages('W0199')
15      def visit_assert(self, node):
16          """check the use of an assert statement on a tuple."""
17          if node.fail is None and isinstance(node.test, astng.Tuple) and \
18             len(node.test.elts) == 2:
19 -            self.add_message('W0199', line=node.fromlineno, node=node)
20 +            self.add_message('W0199', node=node)
21 
22      @check_messages('W0109')
23      def visit_dict(self, node):
24          """check duplicate key in dictionary"""
25          keys = set()
diff --git a/checkers/format.py b/checkers/format.py
@@ -30,10 +30,11 @@
26  from logilab.astng import nodes
27 
28  from pylint.interfaces import IRawChecker, IASTNGChecker
29  from pylint.checkers import BaseRawChecker
30  from pylint.checkers.utils import check_messages
31 +from pylint.utils import WarningScope
32 
33  MSGS = {
34      'C0301': ('Line too long (%s/%s)',
35                'line-too-long',
36                'Used when a line is longer than a given number of characters.'),
@@ -53,22 +54,26 @@
37                'unnecessary-semicolon',
38                'Used when a statement is ended by a semi-colon (";"), which \
39                isn\'t necessary (that\'s python, not C ;).'),
40      'C0321': ('More than one statement on a single line',
41                'multiple-statements',
42 -              'Used when more than on statement are found on the same line.'),
43 +              'Used when more than on statement are found on the same line.',
44 +              {'scope': WarningScope.NODE}),
45      'C0322': ('Operator not preceded by a space\n%s',
46                'no-space-before-operator',
47                'Used when one of the following operator (!= | <= | == | >= | < '
48 -              '| > | = | \\+= | -= | \\*= | /= | %) is not preceded by a space.'),
49 +              '| > | = | \\+= | -= | \\*= | /= | %) is not preceded by a space.',
50 +              {'scope': WarningScope.NODE}),
51      'C0323': ('Operator not followed by a space\n%s',
52                'no-space-after-operator',
53                'Used when one of the following operator (!= | <= | == | >= | < '
54 -              '| > | = | \\+= | -= | \\*= | /= | %) is not followed by a space.'),
55 +              '| > | = | \\+= | -= | \\*= | /= | %) is not followed by a space.',
56 +              {'scope': WarningScope.NODE}),
57      'C0324': ('Comma not followed by a space\n%s',
58                'no-space-after-comma',
59 -              'Used when a comma (",") is not followed by a space.'),
60 +              'Used when a comma (",") is not followed by a space.',
61 +              {'scope': WarningScope.NODE}),
62      }
63 
64  if sys.version_info < (3, 0):
65 
66      MSGS.update({
@@ -82,11 +87,12 @@
67                'should use a upper case "L" since the letter "l" looks too much '
68                'like the digit "1"'),
69      'W0333': ('Use of the `` operator',
70                'backtick',
71                'Used when the deprecated "``" (backtick) operator is used '
72 -              'instead  of the str() function.'),
73 +              'instead  of the str() function.',
74 +              {'scope': WarningScope.NODE}),
75      })
76 
77  # simple quoted string rgx
78  SQSTRING_RGX = r'"([^"\\]|\\.)*?"'
79  # simple apostrophed rgx
diff --git a/lint.py b/lint.py
@@ -46,11 +46,12 @@
80 
81  from logilab.astng import MANAGER, nodes, ASTNGBuildingException
82  from logilab.astng.__pkginfo__ import version as astng_version
83 
84  from pylint.utils import (PyLintASTWalker, UnknownMessage, MessagesHandlerMixIn,
85 -                          ReportsHandlerMixIn, MSG_TYPES, expand_modules)
86 +                          ReportsHandlerMixIn, MSG_TYPES, expand_modules,
87 +                          WarningScope)
88  from pylint.interfaces import ILinter, IRawChecker, IASTNGChecker
89  from pylint.checkers import (BaseRawChecker, EmptyReport,
90                               table_lines_from_stats)
91  from pylint.reporters.text import (TextReporter, ParseableTextReporter,
92                                     VSTextReporter, ColorizedTextReporter)
@@ -506,15 +507,20 @@
93              firstchildlineno = last
94          for msgid, lines in msg_state.iteritems():
95              for lineno, state in lines.items():
96                  original_lineno = lineno
97                  if first <= lineno <= last:
98 -                    if lineno > firstchildlineno:
99 -                        state = True
100 -                    # set state for all lines for this block
101 -                    first, last = node.block_range(lineno)
102 -                    for line in xrange(first, last+1):
103 +                    # Set state for all lines for this block, if the
104 +                    # warning is applied to nodes.
105 +                    if self._messages[msgid].scope == WarningScope.NODE:
106 +                        if lineno > firstchildlineno:
107 +                            state = True
108 +                        first_, last_ = node.block_range(lineno)
109 +                    else:
110 +                        first_ = lineno
111 +                        last_ = last
112 +                    for line in xrange(first_, last_+1):
113                          # do not override existing entries
114                          if not line in self._module_msgs_state.get(msgid, ()):
115                              if line in lines: # state change in the same block
116                                  state = lines[line]
117                                  original_lineno = line
diff --git a/test/input/func_disable_linebased.py b/test/input/func_disable_linebased.py
@@ -0,0 +1,14 @@
118 +# This is a very very very very very very very very very very very very very very very very very very very very very long line.
119 +# pylint: disable=line-too-long
120 +"""Make sure enable/disable pragmas work for messages that are applied to lines and not syntax nodes.
121 +
122 +A disable pragma for a message that applies to nodes is applied to the whole
123 +block if it comes before the first statement (excluding the docstring). For
124 +line-based messages, this behavior needs to be altered to really only apply to
125 +the enclosed lines.
126 +"""
127 +# pylint: enable=line-too-long
128 +
129 +__revision__ = '1'
130 +
131 +print 'This is a very long line which the linter will warn about, now that line-too-long has been enabled again.'
diff --git a/test/messages/func_disable_linebased.txt b/test/messages/func_disable_linebased.txt
@@ -0,0 +1,2 @@
132 +C:  1: Line too long (127/80)
133 +C: 14: Line too long (113/80)
diff --git a/utils.py b/utils.py
@@ -20,19 +20,21 @@
134 
135  import sys
136  from warnings import warn
137  from os.path import dirname, basename, splitext, exists, isdir, join, normpath
138 
139 +from logilab.common.interface import implements
140  from logilab.common.modutils import modpath_from_file, get_module_files, \
141                                      file_from_modpath
142  from logilab.common.textutils import normalize_text
143  from logilab.common.configuration import rest_format_section
144  from logilab.common.ureports import Section
145 
146  from logilab.astng import nodes, Module
147 
148  from pylint.checkers import EmptyReport
149 +from pylint.interfaces import IRawChecker
150 
151 
152  class UnknownMessage(Exception):
153      """raised when a unregistered message id is encountered"""
154 
@@ -58,10 +60,19 @@
155 
156  _MSG_ORDER = 'EWRCIF'
157  MSG_STATE_SCOPE_CONFIG = 0
158  MSG_STATE_SCOPE_MODULE = 1
159 
160 +
161 +# The line/node distinction does not apply to fatal errors and reports.
162 +_SCOPE_EXEMPT = 'FR'
163 +
164 +class WarningScope(object):
165 +    LINE = 'line-based-msg'
166 +    NODE = 'node-based-msg'
167 +
168 +
169  def sort_msgs(msgids):
170      """sort message identifiers according to their category first"""
171      msgs = {}
172      for msg in msgids:
173          msgs.setdefault(msg[0], []).append(msg)
@@ -93,19 +104,20 @@
174          return id
175      return MSG_TYPES_LONG.get(id)
176 
177 
178  class Message:
179 -    def __init__(self, checker, msgid, msg, descr, symbol):
180 +    def __init__(self, checker, msgid, msg, descr, symbol, scope):
181          assert len(msgid) == 5, 'Invalid message id %s' % msgid
182          assert msgid[0] in MSG_TYPES, \
183                 'Bad message type %s in %r' % (msgid[0], msgid)
184          self.msgid = msgid
185          self.msg = msg
186          self.descr = descr
187          self.checker = checker
188          self.symbol = symbol
189 +        self.scope = scope
190 
191  class MessagesHandlerMixIn:
192      """a mix-in class containing all the messages related methods for the main
193      lint class
194      """
@@ -131,15 +143,22 @@
195          message ids should be a string of len 4, where the two first characters
196          are the checker id and the two last the message id in this checker
197          """
198          msgs_dict = checker.msgs
199          chkid = None
200 +
201          for msgid, msg_tuple in msgs_dict.iteritems():
202 -            if len(msg_tuple) == 3:
203 -                (msg, msgsymbol, msgdescr) = msg_tuple
204 +            if implements(checker, IRawChecker):
205 +                scope = WarningScope.LINE
206 +            else:
207 +                scope = WarningScope.NODE
208 +            if len(msg_tuple) > 2:
209 +                (msg, msgsymbol, msgdescr) = msg_tuple[:3]
210                  assert msgsymbol not in self._messages_by_symbol, \
211                      'Message symbol %r is already defined' % msgsymbol
212 +                if len(msg_tuple) > 3 and 'scope' in msg_tuple[3]:
213 +                  scope = msg_tuple[3]['scope']
214              else:
215                  # messages should have a symbol, but for backward compatibility
216                  # they may not.
217                  (msg, msgdescr) = msg_tuple
218                  warn("[pylint 0.26] description of message %s doesn't include "
@@ -149,11 +168,11 @@
219              assert msgid not in self._messages, \
220                     'Message id %r is already defined' % msgid
221              assert chkid is None or chkid == msgid[1:3], \
222                     'Inconsistent checker part in message id %r' % msgid
223              chkid = msgid[1:3]
224 -            msg = Message(checker, msgid, msg, msgdescr, msgsymbol)
225 +            msg = Message(checker, msgid, msg, msgdescr, msgsymbol, scope)
226              self._messages[msgid] = msg
227              self._messages_by_symbol[msgsymbol] = msg
228              self._msgs_by_category.setdefault(msgid[0], []).append(msgid)
229 
230      def get_message_help(self, msgid, checkerref=False):
@@ -318,10 +337,21 @@
231          If provided, msg is expanded using args
232 
233          astng checkers should provide the node argument, raw checkers should
234          provide the line argument.
235          """
236 +        msg_info = self._messages[msgid]
237 +        # Fatal messages and reports are special, the node/scope distinction
238 +        # does not apply to them.
239 +        if msgid[0] not in _SCOPE_EXEMPT:
240 +            if msg_info.scope == WarningScope.LINE:
241 +                assert node is None and line is not None, (
242 +                    'Message %s must only provide line, got line=%s, node=%s' % (msgid, line, node))
243 +            elif msg_info.scope == WarningScope.NODE:
244 +                # Node-based warnings may provide an override line.
245 +                assert node is not None, 'Message %s must provide Node, got None'
246 +
247          if line is None and node is not None:
248              line = node.fromlineno
249          if hasattr(node, 'col_offset'):
250              col_offset = node.col_offset # XXX measured in bytes for utf-8, divide by two for chars?
251          else:
@@ -338,12 +368,12 @@
252          self.stats['by_module'][self.current_name][msg_cat] += 1
253          try:
254              self.stats['by_msg'][msgid] += 1
255          except KeyError:
256              self.stats['by_msg'][msgid] = 1
257 -        msg = self._messages[msgid].msg
258          # expand message ?
259 +        msg = msg_info.msg
260          if args:
261              msg %= args
262          # get module and object
263          if node is None:
264              module, obj = self.current_name, ''