diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md b/crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md index b158912160..394eb97588 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 e63a420cd1..e727655990 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,12 @@ 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()) + protocol.inner.interface(db).members(db).all(|member| { + matches!( + self.member(db, member.name()).symbol, + Symbol::Type(_, Boundness::Bound) + ) + }) } }