diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs index 5826da6073..29755a1343 100644 --- a/crates/ruff_benchmark/benches/ty_walltime.rs +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -194,7 +194,7 @@ static SYMPY: Benchmark = Benchmark::new( max_dep_date: "2025-06-17", python_version: PythonVersion::PY312, }, - 13100, + 13106, ); static TANJUN: Benchmark = Benchmark::new( diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index 35bd136b5e..c15e14a615 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -588,6 +588,31 @@ reveal_type(C.f2(1)) # revealed: str reveal_type(C().f2(1)) # revealed: str ``` +When a `@staticmethod` is decorated with `@contextmanager`, accessing it from an instance should not +bind `self`: + +```py +from contextlib import contextmanager +from collections.abc import Iterator + +class D: + @staticmethod + @contextmanager + def ctx(num: int) -> Iterator[int]: + yield num + + def use_ctx(self) -> None: + # Accessing via self should not bind self + with self.ctx(10) as x: + reveal_type(x) # revealed: int + +# Accessing via class works +reveal_type(D.ctx(5)) # revealed: _GeneratorContextManager[int, None, None] + +# Accessing via instance should also work (no self-binding) +reveal_type(D().ctx(5)) # revealed: _GeneratorContextManager[int, None, None] +``` + ### `__new__` `__new__` is an implicit `@staticmethod`; accessing it on an instance does not bind the `cls` diff --git a/crates/ty_python_semantic/resources/mdtest/final.md b/crates/ty_python_semantic/resources/mdtest/final.md index fc5233e246..80c0c9d0a0 100644 --- a/crates/ty_python_semantic/resources/mdtest/final.md +++ b/crates/ty_python_semantic/resources/mdtest/final.md @@ -119,8 +119,8 @@ class OtherChild(Parent): ... class Grandchild(OtherChild): @staticmethod - # TODO: we should emit a Liskov violation here too # error: [override-of-final-method] + # error: [invalid-method-override] def foo(): ... @property # TODO: we should emit a Liskov violation here too diff --git a/crates/ty_python_semantic/resources/mdtest/override.md b/crates/ty_python_semantic/resources/mdtest/override.md index 9a963c07d7..fe8dd37e3c 100644 --- a/crates/ty_python_semantic/resources/mdtest/override.md +++ b/crates/ty_python_semantic/resources/mdtest/override.md @@ -185,7 +185,7 @@ class LiskovViolatingButNotOverrideViolating(Parent): @staticmethod @override - def class_method1() -> int: ... + def class_method1() -> int: ... # error: [invalid-method-override] @classmethod @override diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Cannot_override_a_me…_(338615109711a91b).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Cannot_override_a_me…_(338615109711a91b).snap index fbb89644c6..e7ae3f2353 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Cannot_override_a_me…_(338615109711a91b).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi…_-_Cannot_override_a_me…_(338615109711a91b).snap @@ -96,8 +96,8 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md 82 | 83 | class Grandchild(OtherChild): 84 | @staticmethod - 85 | # TODO: we should emit a Liskov violation here too - 86 | # error: [override-of-final-method] + 85 | # error: [override-of-final-method] + 86 | # error: [invalid-method-override] 87 | def foo(): ... 88 | @property 89 | # TODO: we should emit a Liskov violation here too @@ -406,12 +406,38 @@ note: This is an unsafe fix and may change runtime behavior ``` +``` +error[invalid-method-override]: Invalid override of method `foo` + --> src/mdtest_snippet.pyi:87:9 + | +85 | # error: [override-of-final-method] +86 | # error: [invalid-method-override] +87 | def foo(): ... + | ^^^^^ Definition is incompatible with `Parent.foo` +88 | @property +89 | # TODO: we should emit a Liskov violation here too + | + ::: src/mdtest_snippet.pyi:7:9 + | + 5 | class Parent: + 6 | @final + 7 | def foo(self): ... + | --------- `Parent.foo` defined here + 8 | + 9 | @final + | +info: `Grandchild.foo` is a staticmethod but `Parent.foo` is an instance method +info: This violates the Liskov Substitution Principle +info: rule `invalid-method-override` is enabled by default + +``` + ``` error[override-of-final-method]: Cannot override `Parent.foo` --> src/mdtest_snippet.pyi:87:9 | -85 | # TODO: we should emit a Liskov violation here too -86 | # error: [override-of-final-method] +85 | # error: [override-of-final-method] +86 | # error: [invalid-method-override] 87 | def foo(): ... | ^^^ Overrides a definition from superclass `Parent` 88 | @property @@ -434,8 +460,8 @@ info: rule `override-of-final-method` is enabled by default 82 | 83 | class Grandchild(OtherChild): - @staticmethod - - # TODO: we should emit a Liskov violation here too - # error: [override-of-final-method] + - # error: [invalid-method-override] - def foo(): ... 84 + 85 | @property diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/override.md_-_`typing.override`_-_Basics_(b7c220f8171f11f0).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/override.md_-_`typing.override`_-_Basics_(b7c220f8171f11f0).snap index 6c6c55d590..b7e1ec94a4 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/override.md_-_`typing.override`_-_Basics_(b7c220f8171f11f0).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/override.md_-_`typing.override`_-_Basics_(b7c220f8171f11f0).snap @@ -188,7 +188,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/override.md 174 | 175 | @staticmethod 176 | @override -177 | def class_method1() -> int: ... +177 | def class_method1() -> int: ... # error: [invalid-method-override] 178 | 179 | @classmethod 180 | @override @@ -367,6 +367,31 @@ info: rule `invalid-explicit-override` is enabled by default ``` +``` +error[invalid-method-override]: Invalid override of method `class_method1` + --> src/mdtest_snippet.pyi:177:9 + | +175 | @staticmethod +176 | @override +177 | def class_method1() -> int: ... # error: [invalid-method-override] + | ^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.class_method1` +178 | +179 | @classmethod + | + ::: src/mdtest_snippet.pyi:21:9 + | + 20 | @classmethod + 21 | def class_method1(cls) -> int: ... + | ------------------------- `Parent.class_method1` defined here + 22 | + 23 | @staticmethod + | +info: `LiskovViolatingButNotOverrideViolating.class_method1` is a staticmethod but `Parent.class_method1` is a classmethod +info: This violates the Liskov Substitution Principle +info: rule `invalid-method-override` is enabled by default + +``` + ``` error[invalid-explicit-override]: Method `bar` is decorated with `@override` but does not override anything --> src/mdtest_snippet.pyi:209:9 diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 40451a0736..4bda87145f 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4685,6 +4685,11 @@ impl<'db> Type<'db> { owner.display(db) ); match self { + Type::Callable(callable) if callable.is_staticmethod_like(db) => { + // For "staticmethod-like" callables, model the behavior of `staticmethod.__get__`. + // The underlying function is returned as-is, without binding self. + return Some((self, AttributeKind::NormalOrNonDataDescriptor)); + } Type::Callable(callable) if callable.is_function_like(db) || callable.is_classmethod_like(db) => { @@ -12389,6 +12394,10 @@ pub enum CallableTypeKind { /// instances, i.e. they bind `self`. FunctionLike, + /// A callable type that represents a staticmethod. These callables do not bind `self` + /// when accessed as attributes on instances - they return the underlying function as-is. + StaticMethodLike, + /// A callable type that we believe represents a classmethod (i.e. it will unconditionally bind /// the first argument on `__get__`). ClassMethodLike, @@ -12493,6 +12502,10 @@ impl<'db> CallableType<'db> { matches!(self.kind(db), CallableTypeKind::ClassMethodLike) } + pub(crate) fn is_staticmethod_like(self, db: &'db dyn Db) -> bool { + matches!(self.kind(db), CallableTypeKind::StaticMethodLike) + } + pub(crate) fn bind_self( self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 606b4ff6d3..dadb196bd9 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1089,6 +1089,8 @@ impl<'db> FunctionType<'db> { pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> { let kind = if self.is_classmethod(db) { CallableTypeKind::ClassMethodLike + } else if self.is_staticmethod(db) { + CallableTypeKind::StaticMethodLike } else { CallableTypeKind::FunctionLike }; diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 5657653548..bd21a9ed8e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2436,6 +2436,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .and_then(CallableTypes::exactly_one) .and_then(|callable| match callable.kind(self.db()) { kind @ (CallableTypeKind::FunctionLike + | CallableTypeKind::StaticMethodLike | CallableTypeKind::ClassMethodLike) => Some(kind), _ => None, }); @@ -2445,9 +2446,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { // When a method on a class is decorated with a function that returns a // `Callable`, assume that the returned callable is also function-like (or - // classmethod-like). See "Decorating a method with a `Callable`-typed - // decorator" in `callables_as_descriptors.md` for the extended - // explanation. + // classmethod-like or staticmethod-like). See "Decorating a method with + // a `Callable`-typed decorator" in `callables_as_descriptors.md` for the + // extended explanation. return_ty_modified } else { return_ty