Skip to content

Commit afa6853

Browse files
Fix disabling of bad-option-value (#6556)
Co-authored-by: Pierre Sassoulas <[email protected]>
1 parent 273a8b2 commit afa6853

File tree

9 files changed

+216
-18
lines changed

9 files changed

+216
-18
lines changed

ChangeLog

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ Release date: TBA
99
..
1010
Put new features here and also in 'doc/whatsnew/2.14.rst'
1111

12+
* We have improved our recognition of inline disable and enable comments. It is
13+
now possible to disable ``bad-option-value`` inline (as long as you disable it before
14+
the bad option value is raised, i.e. ``disable=bad-option-value,bad-message`` not ``disable=bad-message,bad-option-value`` ) as well as certain other previously unsupported messages.
15+
16+
Closes #3312
17+
1218
* Added new checker ``comparison-of-constants``.
1319

1420
Closes #6076

doc/whatsnew/2.14.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ Other Changes
129129

130130
Closes #4301
131131

132+
* We have improved our recognition of inline disable and enable comments. It is
133+
now possible to disable ``bad-option-value`` inline (as long as you disable it before
134+
the bad option value is raised, i.e. ``disable=bad-option-value,bad-message`` not ``disable=bad-message,bad-option-value`` ) as well as certain other
135+
previously unsupported messages.
136+
137+
Closes #3312
138+
132139
* Update ``invalid-slots-object`` message to show bad object rather than its inferred value.
133140

134141
Closes #6101

pylint/lint/pylinter.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -937,8 +937,6 @@ def _check_astroid_module(
937937
self.process_tokens(tokens)
938938
if self._ignore_file:
939939
return False
940-
# walk ast to collect line numbers
941-
self.file_state.collect_block_lines(self.msgs_store, node)
942940
# run raw and tokens checkers
943941
for raw_checker in rawcheckers:
944942
raw_checker.process_module(node)

pylint/utils/file_state.py

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,25 +74,32 @@ def collect_block_lines(
7474
self, msgs_store: MessageDefinitionStore, module_node: nodes.Module
7575
) -> None:
7676
"""Walk the AST to collect block level options line numbers."""
77+
warnings.warn(
78+
"'collect_block_lines' has been deprecated and will be removed in pylint 3.0.",
79+
DeprecationWarning,
80+
)
7781
for msg, lines in self._module_msgs_state.items():
7882
self._raw_module_msgs_state[msg] = lines.copy()
7983
orig_state = self._module_msgs_state.copy()
8084
self._module_msgs_state = {}
8185
self._suppression_mapping = {}
8286
self._effective_max_line_number = module_node.tolineno
83-
self._collect_block_lines(msgs_store, module_node, orig_state)
87+
for msgid, lines in orig_state.items():
88+
for msgdef in msgs_store.get_message_definitions(msgid):
89+
self._set_state_on_block_lines(msgs_store, module_node, msgdef, lines)
8490

85-
def _collect_block_lines(
91+
def _set_state_on_block_lines(
8692
self,
8793
msgs_store: MessageDefinitionStore,
8894
node: nodes.NodeNG,
89-
msg_state: MessageStateDict,
95+
msg: MessageDefinition,
96+
msg_state: dict[int, bool],
9097
) -> None:
9198
"""Recursively walk (depth first) AST to collect block level options
92-
line numbers.
99+
line numbers and set the state correctly.
93100
"""
94101
for child in node.get_children():
95-
self._collect_block_lines(msgs_store, child, msg_state)
102+
self._set_state_on_block_lines(msgs_store, child, msg, msg_state)
96103
# first child line number used to distinguish between disable
97104
# which are the first child of scoped node with those defined later.
98105
# For instance in the code below:
@@ -115,9 +122,7 @@ def _collect_block_lines(
115122
firstchildlineno = node.body[0].fromlineno
116123
else:
117124
firstchildlineno = node.tolineno
118-
for msgid, lines in msg_state.items():
119-
for msg in msgs_store.get_message_definitions(msgid):
120-
self._set_message_state_in_block(msg, lines, node, firstchildlineno)
125+
self._set_message_state_in_block(msg, msg_state, node, firstchildlineno)
121126

122127
def _set_message_state_in_block(
123128
self,
@@ -139,18 +144,61 @@ def _set_message_state_in_block(
139144
if lineno > firstchildlineno:
140145
state = True
141146
first_, last_ = node.block_range(lineno)
147+
# pylint: disable=useless-suppression
148+
# For block nodes first_ is their definition line. For example, we
149+
# set the state of line zero for a module to allow disabling
150+
# invalid-name for the module. For example:
151+
# 1. # pylint: disable=invalid-name
152+
# 2. ...
153+
# OR
154+
# 1. """Module docstring"""
155+
# 2. # pylint: disable=invalid-name
156+
# 3. ...
157+
#
158+
# But if we already visited line 0 we don't need to set its state again
159+
# 1. # pylint: disable=invalid-name
160+
# 2. # pylint: enable=invalid-name
161+
# 3. ...
162+
# The state should come from line 1, not from line 2
163+
# Therefore, if the 'fromlineno' is already in the states we just start
164+
# with the lineno we were originally visiting.
165+
# pylint: enable=useless-suppression
166+
if (
167+
first_ == node.fromlineno
168+
and first_ >= firstchildlineno
169+
and node.fromlineno in self._module_msgs_state.get(msg.msgid, ())
170+
):
171+
first_ = lineno
172+
142173
else:
143174
first_ = lineno
144175
last_ = last
145176
for line in range(first_, last_ + 1):
146-
# do not override existing entries
147-
if line in self._module_msgs_state.get(msg.msgid, ()):
177+
# Do not override existing entries. This is especially important
178+
# when parsing the states for a scoped node where some line-disables
179+
# have already been parsed.
180+
if (
181+
(
182+
isinstance(node, nodes.Module)
183+
and node.fromlineno <= line < lineno
184+
)
185+
or (
186+
not isinstance(node, nodes.Module)
187+
and node.fromlineno < line < lineno
188+
)
189+
) and line in self._module_msgs_state.get(msg.msgid, ()):
148190
continue
149191
if line in lines: # state change in the same block
150192
state = lines[line]
151193
original_lineno = line
194+
195+
# Update suppression mapping
152196
if not state:
153197
self._suppression_mapping[(msg.msgid, line)] = original_lineno
198+
else:
199+
self._suppression_mapping.pop((msg.msgid, line), None)
200+
201+
# Update message state for respective line
154202
try:
155203
self._module_msgs_state[msg.msgid][line] = state
156204
except KeyError:
@@ -160,10 +208,20 @@ def _set_message_state_in_block(
160208
def set_msg_status(self, msg: MessageDefinition, line: int, status: bool) -> None:
161209
"""Set status (enabled/disable) for a given message at a given line."""
162210
assert line > 0
211+
assert self._module
212+
# TODO: 3.0: Remove unnecessary assertion
213+
assert self._msgs_store
214+
215+
# Expand the status to cover all relevant block lines
216+
self._set_state_on_block_lines(
217+
self._msgs_store, self._module, msg, {line: status}
218+
)
219+
220+
# Store the raw value
163221
try:
164-
self._module_msgs_state[msg.msgid][line] = status
222+
self._raw_module_msgs_state[msg.msgid][line] = status
165223
except KeyError:
166-
self._module_msgs_state[msg.msgid] = {line: status}
224+
self._raw_module_msgs_state[msg.msgid] = {line: status}
167225

168226
def handle_ignored_message(
169227
self, state_scope: Literal[0, 1, 2] | None, msgid: str, line: int | None
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Tests for the disabling of bad-option-value."""
2+
# pylint: disable=invalid-name
3+
4+
# pylint: disable=bad-option-value
5+
6+
var = 1 # pylint: disable=a-removed-option
7+
8+
# pylint: enable=bad-option-value
9+
10+
var = 1 # pylint: disable=a-removed-option # [bad-option-value]
11+
12+
# bad-option-value needs to be disabled before the bad option
13+
var = 1 # pylint: disable=a-removed-option, bad-option-value # [bad-option-value]
14+
var = 1 # pylint: disable=bad-option-value, a-removed-option
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bad-option-value:10:0:None:None::Bad option value for disable. Don't recognize message a-removed-option.:UNDEFINED
2+
bad-option-value:13:0:None:None::Bad option value for disable. Don't recognize message a-removed-option.:UNDEFINED

tests/lint/unittest_lint.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,14 @@ def reporter():
191191
@pytest.fixture
192192
def initialized_linter(linter: PyLinter) -> PyLinter:
193193
linter.open()
194-
linter.set_current_module("toto", "mydir/toto")
195-
linter.file_state = FileState("toto", linter.msgs_store)
194+
linter.set_current_module("long_test_file", "long_test_file")
195+
linter.file_state = FileState(
196+
"long_test_file",
197+
linter.msgs_store,
198+
linter.get_ast(
199+
str(join(REGRTEST_DATA_DIR, "long_test_file.py")), "long_test_file"
200+
),
201+
)
196202
return linter
197203

198204

@@ -272,9 +278,9 @@ def test_enable_message_block(initialized_linter: PyLinter) -> None:
272278
filepath = join(REGRTEST_DATA_DIR, "func_block_disable_msg.py")
273279
linter.set_current_module("func_block_disable_msg")
274280
astroid = linter.get_ast(filepath, "func_block_disable_msg")
281+
linter.file_state = FileState("func_block_disable_msg", linter.msgs_store, astroid)
275282
linter.process_tokens(tokenize_module(astroid))
276283
fs = linter.file_state
277-
fs.collect_block_lines(linter.msgs_store, astroid)
278284
# global (module level)
279285
assert linter.is_message_enabled("W0613")
280286
assert linter.is_message_enabled("E1101")
@@ -311,7 +317,6 @@ def test_enable_message_block(initialized_linter: PyLinter) -> None:
311317
assert linter.is_message_enabled("E1101", 75)
312318
assert linter.is_message_enabled("E1101", 77)
313319

314-
fs = linter.file_state
315320
assert fs._suppression_mapping["W0613", 18] == 17
316321
assert fs._suppression_mapping["E1101", 33] == 30
317322
assert ("E1101", 46) not in fs._suppression_mapping

tests/regrtest_data/long_test_file.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""
2+
This file is used for bad-option-value's test. We needed a module that isn’t restricted by line numbers
3+
as the tests use various line numbers to test the behaviour.
4+
5+
Using an empty module creates issue as you can’t disable something on line 10 if it doesn’t exist. Thus, we created an extra long file so we never run into an issue with that.
6+
"""
7+
8+
9+
10+
11+
12+
13+
14+
15+
16+
17+
18+
19+
20+
21+
22+
23+
24+
25+
26+
27+
28+
29+
30+
31+
32+
33+
34+
35+
36+
37+
38+
39+
40+
41+
42+
43+
44+
45+
46+
47+
48+
49+
50+
51+
52+
53+
54+
55+
56+
57+
58+
59+
60+
61+
62+
63+
64+
65+
66+
67+
68+
69+
70+
71+
72+
73+
74+
75+
76+
77+
78+
79+
80+
81+
82+
83+
84+
85+
86+
87+
88+
89+
90+
91+
92+
93+
94+
95+
96+
97+
98+
99+
100+
print(1)

tests/test_deprecation.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import Any
1010

1111
import pytest
12+
from astroid import nodes
1213

1314
from pylint.checkers import BaseChecker
1415
from pylint.checkers.mapreduce_checker import MapReduceMixin
@@ -100,3 +101,10 @@ def test_filestate() -> None:
100101
with pytest.warns(DeprecationWarning):
101102
FileState(msg_store=MessageDefinitionStore())
102103
FileState("foo", MessageDefinitionStore())
104+
105+
106+
def test_collectblocklines() -> None:
107+
"""Test FileState.collect_block_lines."""
108+
state = FileState("foo", MessageDefinitionStore())
109+
with pytest.warns(DeprecationWarning):
110+
state.collect_block_lines(MessageDefinitionStore(), nodes.Module("foo"))

0 commit comments

Comments
 (0)