From 489615fa1bd9e5eff7768f6f2ed84d137e2e4243 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 4 Jun 2025 18:41:03 +0100 Subject: [PATCH 1/2] [ty] Only consider a type `T` a subtype of a protocol `P` if all of `P`s members are fully bound on `T` --- .../resources/mdtest/narrow/hasattr.md | 23 ++++++++++++++++++- .../ty_python_semantic/src/types/instance.rs | 15 ++++++------ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md b/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md index b15891216086fd..394eb975887190 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md @@ -26,7 +26,7 @@ def f(x: Foo): else: reveal_type(x) # revealed: Foo -def y(x: Bar): +def g(x: Bar): if hasattr(x, "spam"): reveal_type(x) # revealed: Never reveal_type(x.spam) # revealed: Never @@ -35,4 +35,25 @@ def y(x: Bar): # error: [unresolved-attribute] reveal_type(x.spam) # revealed: Unknown + +def returns_bool() -> bool: + return False + +class Baz: + if returns_bool(): + x: int = 42 + +def h(obj: Baz): + reveal_type(obj) # revealed: Baz + # error: [possibly-unbound-attribute] + reveal_type(obj.x) # revealed: int + + if hasattr(obj, "x"): + reveal_type(obj) # revealed: Baz & + reveal_type(obj.x) # revealed: int + else: + reveal_type(obj) # revealed: Baz & ~ + + # TODO: should emit `[unresolved-attribute]` and reveal `Unknown` + reveal_type(obj.x) # revealed: @Todo(map_with_boundness: intersections with negative contributions) ``` diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index e63a420cd1c3dd..1a60de8fd31b83 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -4,7 +4,7 @@ use std::marker::PhantomData; use super::protocol_class::ProtocolInterface; use super::{ClassType, KnownClass, SubclassOfType, Type}; -use crate::symbol::{Symbol, SymbolAndQualifiers}; +use crate::symbol::{Boundness, Symbol, SymbolAndQualifiers}; use crate::types::{ClassLiteral, TypeMapping, TypeVarInstance}; use crate::{Db, FxOrderSet}; @@ -45,12 +45,13 @@ impl<'db> Type<'db> { protocol: ProtocolInstanceType<'db>, ) -> bool { // TODO: this should consider the types of the protocol members - // as well as whether each member *exists* on `self`. - protocol - .inner - .interface(db) - .members(db) - .all(|member| !self.member(db, member.name()).symbol.is_unbound()) + // as well as whether each member *exists fully bound* on `self`. + protocol.inner.interface(db).members(db).all(|member| { + matches!( + self.member(db, member.name()).symbol, + Symbol::Type(_, Boundness::Bound) + ) + }) } } From b02be172c01103ba3a51a29ec16361834a4689ce Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 4 Jun 2025 20:36:01 +0100 Subject: [PATCH 2/2] Update crates/ty_python_semantic/src/types/instance.rs Co-authored-by: Carl Meyer --- crates/ty_python_semantic/src/types/instance.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 1a60de8fd31b83..e727655990ad8f 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -45,7 +45,6 @@ impl<'db> Type<'db> { protocol: ProtocolInstanceType<'db>, ) -> bool { // TODO: this should consider the types of the protocol members - // as well as whether each member *exists fully bound* on `self`. protocol.inner.interface(db).members(db).all(|member| { matches!( self.member(db, member.name()).symbol,