diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 52a3affa81a9..23173847b200 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4433,12 +4433,14 @@ 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