diff --git a/crates/ty_python_semantic/resources/mdtest/expression/len.md b/crates/ty_python_semantic/resources/mdtest/expression/len.md index 31770e9557..dce9c39e1c 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/len.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/len.md @@ -170,10 +170,10 @@ reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1] reveal_type(len(ZeroOrTrue())) # revealed: Literal[0, 1] reveal_type(len(OneOrFalse())) # revealed: Literal[1, 0] -# TODO: Emit a diagnostic +# error: [invalid-argument-type] "Argument to function `len` is incorrect: Expected `Sized`, found `OneOrFoo`" reveal_type(len(OneOrFoo())) # revealed: int -# TODO: Emit a diagnostic +# error: [invalid-argument-type] "Argument to function `len` is incorrect: Expected `Sized`, found `ZeroOrStr`" reveal_type(len(ZeroOrStr())) # revealed: int ``` @@ -194,46 +194,6 @@ reveal_type(len(LiteralTrue())) # revealed: Literal[1] reveal_type(len(LiteralFalse())) # revealed: Literal[0] ``` -### Enums - -```py -from enum import Enum, auto -from typing import Literal - -class SomeEnum(Enum): - AUTO = auto() - INT = 2 - STR = "4" - TUPLE = (8, "16") - INT_2 = 3_2 - -class Auto: - def __len__(self) -> Literal[SomeEnum.AUTO]: - return SomeEnum.AUTO - -class Int: - def __len__(self) -> Literal[SomeEnum.INT]: - return SomeEnum.INT - -class Str: - def __len__(self) -> Literal[SomeEnum.STR]: - return SomeEnum.STR - -class Tuple: - def __len__(self) -> Literal[SomeEnum.TUPLE]: - return SomeEnum.TUPLE - -class IntUnion: - def __len__(self) -> Literal[SomeEnum.INT, SomeEnum.INT_2]: - return SomeEnum.INT - -reveal_type(len(Auto())) # revealed: int -reveal_type(len(Int())) # revealed: int -reveal_type(len(Str())) # revealed: int -reveal_type(len(Tuple())) # revealed: int -reveal_type(len(IntUnion())) # revealed: int -``` - ### Negative integers ```py @@ -263,8 +223,8 @@ class SecondRequiredArgument: # this is fine: the call succeeds at runtime since the second argument is optional reveal_type(len(SecondOptionalArgument())) # revealed: Literal[0] -# TODO: Emit a diagnostic -reveal_type(len(SecondRequiredArgument())) # revealed: Literal[1] +# error: [invalid-argument-type] "Argument to function `len` is incorrect: Expected `Sized`, found `SecondRequiredArgument`" +reveal_type(len(SecondRequiredArgument())) # revealed: int ``` ### No `__len__` diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 1cc6e98f47..bbbb9a11b7 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -1766,9 +1766,7 @@ class DefinitelyNotSubtype: static_assert(is_subtype_of(NominalSubtype, P)) static_assert(not is_subtype_of(DefinitelyNotSubtype, P)) - -# TODO: should pass -static_assert(not is_subtype_of(NotSubtype, P)) # error: [static-assert-error] +static_assert(not is_subtype_of(NotSubtype, P)) ``` A callable instance attribute is not sufficient for a type to satisfy a protocol with a method @@ -1924,27 +1922,22 @@ static_assert(is_assignable_to(NominalGeneric, LegacyClassScoped[int])) # and there exist fully static materializations of `NewStyleClassScoped[Unknown]` # where `Nominal` would not be a subtype of the given materialization, # hence there is no subtyping relation: -# -# TODO: these should pass -static_assert(not is_subtype_of(NominalConcrete, NewStyleClassScoped)) # error: [static-assert-error] -static_assert(not is_subtype_of(NominalConcrete, LegacyClassScoped)) # error: [static-assert-error] +static_assert(not is_subtype_of(NominalConcrete, NewStyleClassScoped)) +static_assert(not is_subtype_of(NominalConcrete, LegacyClassScoped)) # Similarly, `NominalGeneric` is implicitly `NominalGeneric[Unknown`] -# -# TODO: these should pass -static_assert(not is_subtype_of(NominalGeneric, NewStyleClassScoped[int])) # error: [static-assert-error] -static_assert(not is_subtype_of(NominalGeneric, LegacyClassScoped[int])) # error: [static-assert-error] +static_assert(not is_subtype_of(NominalGeneric, NewStyleClassScoped[int])) +static_assert(not is_subtype_of(NominalGeneric, LegacyClassScoped[int])) static_assert(is_subtype_of(NominalConcrete, NewStyleClassScoped[int])) static_assert(is_subtype_of(NominalConcrete, LegacyClassScoped[int])) static_assert(is_subtype_of(NominalGeneric[int], NewStyleClassScoped[int])) static_assert(is_subtype_of(NominalGeneric[int], LegacyClassScoped[int])) -# TODO: these should pass -static_assert(not is_assignable_to(NominalConcrete, NewStyleClassScoped[str])) # error: [static-assert-error] -static_assert(not is_assignable_to(NominalConcrete, LegacyClassScoped[str])) # error: [static-assert-error] -static_assert(not is_subtype_of(NominalGeneric[int], NewStyleClassScoped[str])) # error: [static-assert-error] -static_assert(not is_subtype_of(NominalGeneric[int], LegacyClassScoped[str])) # error: [static-assert-error] +static_assert(not is_assignable_to(NominalConcrete, NewStyleClassScoped[str])) +static_assert(not is_assignable_to(NominalConcrete, LegacyClassScoped[str])) +static_assert(not is_subtype_of(NominalGeneric[int], NewStyleClassScoped[str])) +static_assert(not is_subtype_of(NominalGeneric[int], LegacyClassScoped[str])) ``` And they can also have generic contexts scoped to the method: @@ -2219,24 +2212,24 @@ class Foo(Protocol): static_assert(is_subtype_of(Callable[[int], str], Foo)) static_assert(is_assignable_to(Callable[[int], str], Foo)) -# TODO: these should pass -static_assert(not is_subtype_of(Callable[[str], str], Foo)) # error: [static-assert-error] -static_assert(not is_assignable_to(Callable[[str], str], Foo)) # error: [static-assert-error] -static_assert(not is_subtype_of(Callable[[CallMeMaybe, int], str], Foo)) # error: [static-assert-error] -static_assert(not is_assignable_to(Callable[[CallMeMaybe, int], str], Foo)) # error: [static-assert-error] +static_assert(not is_subtype_of(Callable[[str], str], Foo)) +static_assert(not is_assignable_to(Callable[[str], str], Foo)) +static_assert(not is_subtype_of(Callable[[CallMeMaybe, int], str], Foo)) +static_assert(not is_assignable_to(Callable[[CallMeMaybe, int], str], Foo)) def h(obj: Callable[[int], str], obj2: Foo, obj3: Callable[[str], str]): obj2 = obj - # TODO: we should emit [invalid-assignment] here because the signature of `obj3` is not assignable - # to the declared type of `obj2` + # error: [invalid-assignment] "Object of type `(str, /) -> str` is not assignable to `Foo`" obj2 = obj3 def satisfies_foo(x: int) -> str: return "foo" -static_assert(is_subtype_of(TypeOf[satisfies_foo], Foo)) static_assert(is_assignable_to(TypeOf[satisfies_foo], Foo)) + +# TODO: this should pass +static_assert(is_subtype_of(TypeOf[satisfies_foo], Foo)) # error: [static-assert-error] ``` ## Nominal subtyping of protocols diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 051f36b5a8..2c6de29635 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1658,12 +1658,15 @@ impl<'db> Type<'db> { | Type::EnumLiteral(_), ) => ConstraintSet::from(false), - (Type::Callable(self_callable), Type::Callable(other_callable)) => { - self_callable.has_relation_to_impl(db, other_callable, relation, visitor) - } + (Type::Callable(self_callable), Type::Callable(other_callable)) => visitor + .visit((self, target, relation), || { + self_callable.has_relation_to_impl(db, other_callable, relation, visitor) + }), - (_, Type::Callable(_)) => self.into_callable(db).when_some_and(|callable| { - callable.has_relation_to_impl(db, target, relation, visitor) + (_, Type::Callable(_)) => visitor.visit((self, target, relation), || { + self.into_callable(db).when_some_and(|callable| { + callable.has_relation_to_impl(db, target, relation, visitor) + }) }), (_, Type::ProtocolInstance(protocol)) => { @@ -3265,6 +3268,13 @@ impl<'db> Type<'db> { policy: InstanceFallbackShadowsNonDataDescriptor, member_policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { + // TODO: this is a workaround for the fact that looking up the `__call__` attribute on the + // meta-type of a `Callable` type currently returns `Unbound`. We should fix this by inferring + // a more sophisticated meta-type for `Callable` types; that would allow us to remove this branch. + if name == "__call__" && matches!(self, Type::Callable(_) | Type::DataclassTransformer(_)) { + return Place::bound(self).into(); + } + let ( PlaceAndQualifiers { place: meta_attr, diff --git a/crates/ty_python_semantic/src/types/property_tests.rs b/crates/ty_python_semantic/src/types/property_tests.rs index 5c998fec42..07db77e628 100644 --- a/crates/ty_python_semantic/src/types/property_tests.rs +++ b/crates/ty_python_semantic/src/types/property_tests.rs @@ -68,7 +68,7 @@ macro_rules! type_property_test { mod stable { use super::union; - use crate::types::{CallableType, Type}; + use crate::types::{CallableType, KnownClass, Type}; // Reflexivity: `T` is equivalent to itself. type_property_test!( @@ -205,6 +205,25 @@ mod stable { all_fully_static_type_pairs_are_subtype_of_their_union, db, forall fully_static_types s, t. s.is_subtype_of(db, union(db, [s, t])) && t.is_subtype_of(db, union(db, [s, t])) ); + + // Any type assignable to `Iterable[object]` should be considered iterable. + // + // Note that the inverse is not true, due to the fact that we recognize the old-style + // iteration protocol as well as the new-style iteration protocol: not all objects that + // we consider iterable are assignable to `Iterable[object]`. + // + // Note also that (like other property tests in this module), + // this invariant will only hold true for Liskov-compliant types assignable to `Iterable`. + // Since protocols can participate in nominal assignability/subtyping as well as + // structural assignability/subtyping, it is possible to construct types that a type + // checker must consider to be subtypes of `Iterable` even though they are not in fact + // iterable (as long as the user `type: ignore`s any type-checker errors stemming from + // the Liskov violation). All you need to do is to create a class that subclasses + // `Iterable` but assigns `__iter__ = None` in the class body (or similar). + type_property_test!( + all_type_assignable_to_iterable_are_iterable, db, + forall types t. t.is_assignable_to(db, KnownClass::Iterable.to_specialized_instance(db, [Type::object()])) => t.try_iterate(db).is_ok() + ); } /// This module contains property tests that currently lead to many false positives. @@ -217,8 +236,6 @@ mod stable { mod flaky { use itertools::Itertools; - use crate::types::{KnownClass, Type}; - use super::{intersection, union}; // Negating `T` twice is equivalent to `T`. @@ -313,25 +330,4 @@ mod flaky { bottom_materialization_of_type_is_assigneble_to_type, db, forall types t. t.bottom_materialization(db).is_assignable_to(db, t) ); - - // Any type assignable to `Iterable[object]` should be considered iterable. - // - // Note that the inverse is not true, due to the fact that we recognize the old-style - // iteration protocol as well as the new-style iteration protocol: not all objects that - // we consider iterable are assignable to `Iterable[object]`. - // - // Note also that (like other property tests in this module), - // this invariant will only hold true for Liskov-compliant types assignable to `Iterable`. - // Since protocols can participate in nominal assignability/subtyping as well as - // structural assignability/subtyping, it is possible to construct types that a type - // checker must consider to be subtypes of `Iterable` even though they are not in fact - // iterable (as long as the user `type: ignore`s any type-checker errors stemming from - // the Liskov violation). All you need to do is to create a class that subclasses - // `Iterable` but assigns `__iter__ = None` in the class body (or similar). - // - // Currently flaky due to - type_property_test!( - all_type_assignable_to_iterable_are_iterable, db, - forall types t. t.is_assignable_to(db, KnownClass::Iterable.to_specialized_instance(db, [Type::object()])) => t.try_iterate(db).is_ok() - ); } diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index f9870203e7..70866ba0f9 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -6,23 +6,24 @@ use itertools::Itertools; use ruff_python_ast::name::Name; use rustc_hash::FxHashMap; -use super::TypeVarVariance; -use crate::semantic_index::place::ScopedPlaceId; -use crate::semantic_index::{SemanticIndex, place_table}; -use crate::types::ClassType; -use crate::types::context::InferContext; -use crate::types::diagnostic::report_undeclared_protocol_member; use crate::{ Db, FxOrderSet, place::{Boundness, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations}, - semantic_index::{definition::Definition, use_def_map}, + semantic_index::{ + SemanticIndex, definition::Definition, place::ScopedPlaceId, place_table, use_def_map, + }, types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, - FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, KnownFunction, - NormalizedVisitor, PropertyInstanceType, Signature, Type, TypeMapping, TypeQualifiers, - TypeRelation, VarianceInferable, + ClassType, FindLegacyTypeVarsVisitor, HasRelationToVisitor, + InstanceFallbackShadowsNonDataDescriptor, IsDisjointVisitor, KnownFunction, + MemberLookupPolicy, NormalizedVisitor, PropertyInstanceType, Signature, Type, TypeMapping, + TypeQualifiers, TypeRelation, TypeVarVariance, VarianceInferable, constraints::{ConstraintSet, IteratorConstraintsExtension}, + context::InferContext, + diagnostic::report_undeclared_protocol_member, signatures::{Parameter, Parameters}, + todo_type, + visitor::any_over_type, }, }; @@ -539,12 +540,36 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { visitor: &HasRelationToVisitor<'db>, ) -> ConstraintSet<'db> { match &self.kind { - // TODO: consider the types of the attribute on `other` for method members - ProtocolMemberKind::Method(_) => ConstraintSet::from(matches!( - other.to_meta_type(db).member(db, self.name).place, - Place::Type(ty, Boundness::Bound) - if ty.is_assignable_to(db, CallableType::single(db, Signature::dynamic(Type::any()))) - )), + ProtocolMemberKind::Method(method) => { + let Place::Type(attribute_type, Boundness::Bound) = other + .invoke_descriptor_protocol( + db, + self.name, + Place::Unbound.into(), + InstanceFallbackShadowsNonDataDescriptor::No, + MemberLookupPolicy::default(), + ) + .place + else { + return ConstraintSet::from(false); + }; + + let proto_member_as_bound_method = method.bind_self(db); + + if any_over_type(db, proto_member_as_bound_method, &|t| { + matches!(t, Type::TypeVar(_)) + }) { + // TODO: proper validation for generic methods on protocols + return ConstraintSet::from(true); + } + + attribute_type.has_relation_to_impl( + db, + proto_member_as_bound_method, + relation, + visitor, + ) + } // TODO: consider the types of the attribute on `other` for property members ProtocolMemberKind::Property(_) => ConstraintSet::from(matches!( other.member(db, self.name).place, @@ -687,6 +712,13 @@ fn cached_protocol_interface<'db>( { ProtocolMemberKind::Method(callable) } + Type::FunctionLiteral(function) + if function.is_staticmethod(db) || function.is_classmethod(db) => + { + ProtocolMemberKind::Other(todo_type!( + "classmethod and staticmethod protocol members" + )) + } Type::FunctionLiteral(function) if bound_on_class.is_yes() => { ProtocolMemberKind::Method(function.into_callable_type(db)) }