From f0368c858fef59402ad94089ce95764a198b65ff Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 12 Feb 2026 16:14:06 +0100 Subject: [PATCH] [ty] Fix method calls on subclasses of `Any` --- .../resources/mdtest/call/methods.md | 15 ++++++++++++++ crates/ty_python_semantic/src/place.rs | 20 +------------------ crates/ty_python_semantic/src/types/class.rs | 7 ++++--- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index fea7c85a57c73e..edcea666ae1df1 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -234,6 +234,21 @@ def _(a: object, b: SupportsStr, c: Falsy, d: AlwaysFalsy, e: None, f: Foo | Non f.__str__() # error: [missing-argument] ``` +## Method calls on subclasses of `Any` + +```py +from typing_extensions import assert_type, Any + +class SubclassOfAny(Any): + def method(self) -> int: + return 1 + +a = SubclassOfAny() +assert_type(a.method(), int) + +assert_type(a.non_existing_method(), Any) +``` + ## Error cases: Calling `__get__` for methods The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed: diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index db868895b1479d..f26b6416baca02 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -15,7 +15,7 @@ use crate::semantic_index::{DeclarationWithConstraint, global_scope, use_def_map use crate::types::{ ApplyTypeMappingVisitor, DynamicType, KnownClass, MaterializationKind, MemberLookupPolicy, Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, UnionType, binding_type, - declaration_type, todo_type, + declaration_type, }; use crate::{Db, FxOrderSet, Program}; @@ -174,13 +174,6 @@ impl<'db> Place<'db> { Place::Defined(DefinedPlace::new(ty.into()).with_origin(TypeOrigin::Declared)) } - /// Constructor that creates a [`Place`] with a [`crate::types::TodoType`] type - /// and definedness [`Definedness::AlwaysDefined`]. - #[allow(unused_variables)] // Only unused in release builds - pub(crate) fn todo(message: &'static str) -> Self { - Place::Defined(DefinedPlace::new(todo_type!(message))) - } - pub(crate) fn is_undefined(&self) -> bool { matches!(self, Place::Undefined) } @@ -675,17 +668,6 @@ pub(crate) struct PlaceAndQualifiers<'db> { } impl<'db> PlaceAndQualifiers<'db> { - /// Constructor that creates a [`PlaceAndQualifiers`] instance with a [`TodoType`] type - /// and no qualifiers. - /// - /// [`TodoType`]: crate::types::TodoType - pub(crate) fn todo(message: &'static str) -> Self { - Self { - place: Place::todo(message), - qualifiers: TypeQualifiers::empty(), - } - } - pub(crate) fn unbound() -> Self { Self::default() } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index b6e254cdf88ab3..d6ebbd8b6dffc7 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -6111,9 +6111,10 @@ impl<'db, I: Iterator>> MroLookup<'db, I> { // Skip over these very special class bases that aren't really classes. } ClassBase::Dynamic(_) => { - return InstanceMemberResult::Done(PlaceAndQualifiers::todo( - "instance attribute on class with dynamic base", - )); + // We already return the dynamic type for class member lookup, so we can + // just return unbound here (to avoid having to build a union of the + // dynamic type with itself). + return InstanceMemberResult::Done(PlaceAndQualifiers::unbound()); } ClassBase::Class(class) => { if let member @ PlaceAndQualifiers {