From 2f659ebfa4bb676792643a7e4e65176564abcc46 Mon Sep 17 00:00:00 2001 From: guocn <11581622+guocn@users.noreply.github.com> Date: Thu, 22 Oct 2026 20:59:36 +0800 Subject: [PATCH 1/2] Do not emit notes attached to errors suppressed by type: ignore A note attached to a parent error (parent_error) survived when the parent error was ignored by a 'type: ignore' comment on a different line of the same multi-line expression, because the note's own origin_span does not include the ignored line. Suppress such notes in add_error_info when the parent error is ignored on the parent's lines. Also attach the union operand notes to their parent error (unsupported operand types) so they take part in that logic, and normalize ErrorInfo.origin_span to a list so it can be inspected more than once. Fixes #22271 --- mypy/checkexpr.py | 10 +++++++--- mypy/errors.py | 11 ++++++++++- mypy/messages.py | 16 +++++++++++++--- test-data/unit/check-unions.test | 10 ++++++++++ 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 52a3affa81a9..d7441be10561 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4433,12 +4433,16 @@ def check_op( recent_context.line = err.line recent_context.column = err.column if len(left_variants) >= 2 and len(right_variants) >= 2: - self.msg.warn_both_operands_are_from_unions(recent_context) + self.msg.warn_both_operands_are_from_unions( + recent_context, parent_error=err + ) elif len(left_variants) >= 2: - self.msg.warn_operand_was_from_union("Left", base_type, context=recent_context) + self.msg.warn_operand_was_from_union( + "Left", base_type, context=recent_context, parent_error=err + ) elif len(right_variants) >= 2: self.msg.warn_operand_was_from_union( - "Right", right_type, context=recent_context + "Right", right_type, context=recent_context, parent_error=err ) # See the comment in 'check_overload_call' for more details on why diff --git a/mypy/errors.py b/mypy/errors.py index e95904ce355e..6a37f7048209 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -149,7 +149,7 @@ def __init__( self.code = code self.blocker = blocker self.only_once = only_once - self.origin_span = origin_span or [line] + self.origin_span = list(origin_span) if origin_span is not None else [line] self.target = target self.priority = priority if parent_error is not None: @@ -776,6 +776,15 @@ def add_error_info(self, info: ErrorInfo, *, file: str | None = None) -> None: if self._filter_error(file, info): return if not info.blocker: # Blockers cannot be ignored + if info.parent_error is not None and file in self.ignored_lines: + # A note attached to an error must not outlive its parent: if + # the parent error is ignored on its own line(s), the note is + # not shown even if it is reported on another line. + for scope_line in info.parent_error.origin_span: + if self.is_ignored_error( + scope_line, info.parent_error, self.ignored_lines[file] + ): + return if file in self.ignored_lines: # Check each line in this context for "type: ignore" comments. # line == end_line for most nodes, so we only loop once. diff --git a/mypy/messages.py b/mypy/messages.py index 0f3a5be368a4..3bfb3c79cbd2 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1775,14 +1775,24 @@ def overloaded_signatures_ret_specific(self, index: int, context: Context) -> No context, ) - def warn_both_operands_are_from_unions(self, context: Context) -> None: - self.note("Both left and right operands are unions", context, code=codes.OPERATOR) + def warn_both_operands_are_from_unions( + self, context: Context, parent_error: ErrorInfo | None = None + ) -> None: + self.note( + "Both left and right operands are unions", + context, + code=codes.OPERATOR, + parent_error=parent_error, + ) - def warn_operand_was_from_union(self, side: str, original: Type, context: Context) -> None: + def warn_operand_was_from_union( + self, side: str, original: Type, context: Context, parent_error: ErrorInfo | None = None + ) -> None: self.note( f"{side} operand is of type {format_type(original, self.options)}", context, code=codes.OPERATOR, + parent_error=parent_error, ) def operator_method_signatures_overlap( diff --git a/test-data/unit/check-unions.test b/test-data/unit/check-unions.test index 6394e0722501..3975120d4c04 100644 --- a/test-data/unit/check-unions.test +++ b/test-data/unit/check-unions.test @@ -109,6 +109,16 @@ x[2] + 1 # E: Unsupported operand types for + ("str" and "int") \ # N: Left operand is of type "int | str" [builtins fixtures/isinstancelist.pyi] +[case testIgnoredOperatorErrorDoesNotLeakNote] +# A note attached to an error ignored by a "type: ignore" comment on a +# different line of the same multi-line expression must not leak. +a:b +b =dict[0, b|0] # type:ignore[valid-type] +a[0 +]+"" # type:ignore[operator] +[builtins fixtures/isinstancelist.pyi] + + [case testUnionAsOverloadArg] from foo import * x = 0 From c4cb56db3ec2e3e45f412372d300ba330791d967 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:01:35 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checkexpr.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index d7441be10561..23173847b200 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4433,9 +4433,7 @@ def check_op( recent_context.line = err.line recent_context.column = err.column if len(left_variants) >= 2 and len(right_variants) >= 2: - self.msg.warn_both_operands_are_from_unions( - recent_context, parent_error=err - ) + self.msg.warn_both_operands_are_from_unions(recent_context, parent_error=err) elif len(left_variants) >= 2: self.msg.warn_operand_was_from_union( "Left", base_type, context=recent_context, parent_error=err