From 0c41e090472ecb0be259f64e1f17d36364395ff4 Mon Sep 17 00:00:00 2001 From: zhengguangzhuo <100548634+zhengguangzhuo@users.noreply.github.com> Date: Thu, 22 Oct 2026 17:21:56 +0800 Subject: [PATCH] Fix narrowing after always-true blocks with allow-redefinition --- mypy/binder.py | 2 ++ mypy/checker.py | 3 ++- test-data/unit/check-redefine2.test | 38 +++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/mypy/binder.py b/mypy/binder.py index 9984781733cd4..cd6d9ae3ff89e 100644 --- a/mypy/binder.py +++ b/mypy/binder.py @@ -398,6 +398,8 @@ def update_from_options(self, frames: list[Frame]) -> bool: if ( current_value is None or not is_same_type(type, current_value.type) + # 新语义下,即使类型语义等价,也要保留合并得到的窄化表示。 + or (not old_semantics and type != current_value.type) # Manually carry over any narrowing from hasattr() from inner frames. This is # a bit ad-hoc, but our handling of hasattr() is on best effort basis anyway. or isinstance(p_type := get_proper_type(type), Instance) diff --git a/mypy/checker.py b/mypy/checker.py index fd4486218dc11..1f40f4fc86b28 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5477,7 +5477,8 @@ def visit_if_stmt(self, s: IfStmt) -> None: s.unreachable_else = is_unreachable_map(else_map) # XXX Issue a warning if condition is always False? - with self.binder.frame_context(can_skip=True, fall_through=2): + # else 分支不可达时,不能把进入 if 前的状态作为可跳过路径合并。 + with self.binder.frame_context(can_skip=not s.unreachable_else, fall_through=2): self.push_type_map(if_map, from_assignment=False) self.accept(b) diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index b5a8aeac0e3da..12a352c4c0c4f 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -1297,6 +1297,44 @@ def foo() -> None: if bool(): name = "a" +[case testNarrowingSurvivesAlwaysTrueBlock] +# flags: --allow-redefinition +class Base: ... +class Sub(Base): ... + +def f(x: Sub | Base) -> None: + if True: + assert not isinstance(x, Sub) + reveal_type(x) # N: Revealed type is "__main__.Base" + reveal_type(x) # N: Revealed type is "__main__.Base" +[builtins fixtures/isinstance.pyi] + +[case testNarrowingSurvivesAlwaysTrueBlockDefault] +class Base: ... +class Sub(Base): ... + +def f(x: Sub | Base) -> None: + if True: + assert not isinstance(x, Sub) + reveal_type(x) # N: Revealed type is "__main__.Base" + reveal_type(x) # N: Revealed type is "__main__.Base" +[builtins fixtures/isinstance.pyi] + +[case testNarrowingSurvivesTypeCheckingBlock] +# flags: --allow-redefinition +from typing import TYPE_CHECKING + +class Base: ... +class Sub(Base): ... + +def f(x: Sub | Base) -> None: + if TYPE_CHECKING: + assert not isinstance(x, Sub) + reveal_type(x) # N: Revealed type is "__main__.Base" + reveal_type(x) # N: Revealed type is "__main__.Base" +[builtins fixtures/isinstance.pyi] +[typing fixtures/typing-full.pyi] + [case testNewRedefinePartialTypeForUnderscore] # flags: --allow-redefinition