Skip to content

Commit 4d61b4b

Browse files
committed
Fix error code precedence to ensure disable overrides enable
The logic for resolving conflicts between 'enable_error_code' and 'disable_error_code' was inverted in two locations: Options.process_error_codes and Options.apply_changes. In the global configuration path, enabling incorrectly won precedence. In the per-module override path, the logic allowed inherited enabling to undo an explicit disabling. This change swaps the set subtraction in process_error_codes and removes the redundant discard operation in apply_changes to enforce the rule: Disable always wins. Fixes #20348.
1 parent 4eb6b50 commit 4d61b4b

File tree

2 files changed

+75
-7
lines changed

2 files changed

+75
-7
lines changed

mypy/options.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ class BuildType:
8585
NEW_GENERIC_SYNTAX: Final = "NewGenericSyntax"
8686
INLINE_TYPEDDICT: Final = "InlineTypedDict"
8787
TYPE_FORM: Final = "TypeForm"
88-
INCOMPLETE_FEATURES: Final = frozenset((PRECISE_TUPLE_TYPES, INLINE_TYPEDDICT, TYPE_FORM))
88+
INCOMPLETE_FEATURES: Final = frozenset(
89+
(PRECISE_TUPLE_TYPES, INLINE_TYPEDDICT, TYPE_FORM)
90+
)
8991
COMPLETE_FEATURES: Final = frozenset((TYPE_VAR_TUPLE, UNPACK, NEW_GENERIC_SYNTAX))
9092

9193

@@ -468,10 +470,14 @@ def process_error_codes(self, *, error_callback: Callable[[str], Any]) -> None:
468470
self.enabled_error_codes |= {error_codes[code] for code in enabled_codes}
469471

470472
# Enabling an error code always overrides disabling
471-
self.disabled_error_codes -= self.enabled_error_codes
473+
# The original line: self.disabled_error_codes -= self.enabled_error_codes
474+
self.enabled_error_codes -= self.disabled_error_codes
472475

473476
def process_incomplete_features(
474-
self, *, error_callback: Callable[[str], Any], warning_callback: Callable[[str], Any]
477+
self,
478+
*,
479+
error_callback: Callable[[str], Any],
480+
warning_callback: Callable[[str], Any],
475481
) -> None:
476482
# Validate incomplete features.
477483
for feature in self.enable_incomplete_feature:
@@ -513,7 +519,8 @@ def apply_changes(self, changes: dict[str, object]) -> Options:
513519
for code_str in new_options.enable_error_code:
514520
code = error_codes[code_str]
515521
new_options.enabled_error_codes.add(code)
516-
new_options.disabled_error_codes.discard(code)
522+
# Fix: Remove the next line to ensure 'disabled' takes precedence.
523+
# Original line: new_options.disabled_error_codes.discard(code)
517524
return new_options
518525

519526
def compare_stable(self, other_snapshot: dict[str, object]) -> bool:
@@ -545,8 +552,12 @@ def build_per_module_cache(self) -> None:
545552
# than foo.bar.*.
546553
# (A section being "processed last" results in its config "winning".)
547554
# Unstructured glob configs are stored and are all checked for each module.
548-
unstructured_glob_keys = [k for k in self.per_module_options.keys() if "*" in k[:-1]]
549-
structured_keys = [k for k in self.per_module_options.keys() if "*" not in k[:-1]]
555+
unstructured_glob_keys = [
556+
k for k in self.per_module_options.keys() if "*" in k[:-1]
557+
]
558+
structured_keys = [
559+
k for k in self.per_module_options.keys() if "*" not in k[:-1]
560+
]
550561
wildcards = sorted(k for k in structured_keys if k.endswith(".*"))
551562
concrete = [k for k in structured_keys if not k.endswith(".*")]
552563

@@ -564,7 +575,9 @@ def build_per_module_cache(self) -> None:
564575
# on inheriting from parent configs.
565576
options = self.clone_for_module(key)
566577
# And then update it with its per-module options.
567-
self._per_module_cache[key] = options.apply_changes(self.per_module_options[key])
578+
self._per_module_cache[key] = options.apply_changes(
579+
self.per_module_options[key]
580+
)
568581

569582
# Add the more structured sections into unused configs, since
570583
# they only count as used if actually used by a real module.

mypyc/test/test_options.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# test/testopts.py (or similar file)
2+
3+
from mypy.errorcodes import error_codes, ErrorCode
4+
from mypy.options import Options
5+
import unittest # or another framework used by mypy
6+
7+
# Get the specific ErrorCode object we are testing
8+
POSSIBLY_UNDEFINED = error_codes['possibly-undefined']
9+
10+
class OptionsPrecedenceSuite(unittest.TestCase):
11+
# ... other test methods ...
12+
13+
# --- Your New Tests Below ---
14+
15+
def test_global_disable_precedence(self) -> None:
16+
"""
17+
Verify fix #1: Global disable via flag/config overrides global enable.
18+
(Tests Options.process_error_codes)
19+
"""
20+
options = Options()
21+
# 1. Simulate both being set in config/command line
22+
options.enable_error_code = ['possibly-undefined']
23+
options.disable_error_code = ['possibly-undefined']
24+
25+
# 2. Run the processing logic (this is where your fix lives)
26+
options.process_error_codes(error_callback=lambda x: None)
27+
28+
# 3. Assert the result: DISABLE must win
29+
self.assertIn(POSSIBLY_UNDEFINED, options.disabled_error_codes)
30+
self.assertNotIn(POSSIBLY_UNDEFINED, options.enabled_error_codes)
31+
32+
def test_per_module_disable_precedence(self) -> None:
33+
"""
34+
Verify fix #2: Per-module disable overrides global enable.
35+
(Tests Options.apply_changes)
36+
"""
37+
base_options = Options()
38+
39+
# 1. Setup the global options to ENABLE the code
40+
base_options.enable_error_code = ['possibly-undefined']
41+
base_options.process_error_codes(error_callback=lambda x: None)
42+
43+
# 2. Setup a per-module override to DISABLE the code
44+
per_module_changes = {
45+
'disable_error_code': ['possibly-undefined'],
46+
'enable_error_code': [], # ensure this list doesn't interfere
47+
}
48+
49+
# 3. Apply the per-module changes (this is where your fix lives)
50+
# We don't care about the module name here, just the application of changes.
51+
module_options = base_options.apply_changes(per_module_changes)
52+
53+
# 4. Assert the result: DISABLE must win at the module level
54+
self.assertIn(POSSIBLY_UNDEFINED, module_options.disabled_error_codes)
55+
self.assertNotIn(POSSIBLY_UNDEFINED, module_options.enabled_error_codes)

0 commit comments

Comments
 (0)