From e44c4892737652d7ed70a3e6ea2e751efaae1c99 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 24 Jun 2025 19:39:02 +0100 Subject: [PATCH] [ty] Fix false positives when subscripting an object inferred as having an `Intersection` type (#18920) --- .../resources/mdtest/subscript/class.md | 18 ++++++++ .../resources/mdtest/subscript/tuple.md | 10 +++-- crates/ty_python_semantic/src/types/infer.rs | 42 ++++++++++++------- 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/class.md b/crates/ty_python_semantic/resources/mdtest/subscript/class.md index 14d82f43f5..86de205086 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/class.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/class.md @@ -84,3 +84,21 @@ def _(flag: bool): reveal_type(a) # revealed: str | Unknown ``` + +## Intersection of nominal-instance types + +If a subscript operation could succeed for *any* positive element of an intersection, no diagnostic +should be reported even if it would not succeed for some other element of the intersection. + +```py +class Foo: ... + +class Bar: + def __getitem__(self, key: str) -> int: + return 42 + +def f(x: Foo): + if isinstance(x, Bar): + # TODO: should be `int` + reveal_type(x["whatever"]) # revealed: @Todo(Subscript expressions on intersections) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md index 9b4edb75c1..17886ffaef 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md @@ -210,8 +210,12 @@ def test3(val: tuple[str] | tuple[int] | int): ### Intersection subscript access ```py -from ty_extensions import Intersection, Not +from ty_extensions import Intersection -def test4(val: Intersection[tuple[str], tuple[int]]): - reveal_type(val[0]) # revealed: str & int +class Foo: ... +class Bar: ... + +def test4(val: Intersection[tuple[Foo], tuple[Bar]]): + # TODO: should be `Foo & Bar` + reveal_type(val[0]) # revealed: @Todo(Subscript expressions on intersections) ``` diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index c5d08f5a7e..ed9f534b41 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -8140,22 +8140,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { slice_ty, ) } - // If the value type is a union make sure to union the load types. - // For example: - // val: tuple[int] | tuple[str] - // val[0] can be an int or str type - (Type::Union(union_ty), _, _) => union_ty.map(self.db(), |ty| { - self.infer_subscript_expression_types(value_node, *ty, slice_ty) + + (Type::Union(union), _, _) => union.map(self.db(), |element| { + self.infer_subscript_expression_types(value_node, *element, slice_ty) }), - (Type::Intersection(intersection_ty), _, _) => intersection_ty - .positive(self.db()) - .iter() - .map(|ty| self.infer_subscript_expression_types(value_node, *ty, slice_ty)) - .fold( - IntersectionBuilder::new(self.db()), - IntersectionBuilder::add_positive, - ) - .build(), + + // TODO: we can map over the intersection and fold the results back into an intersection, + // but we need to make sure we avoid emitting a diagnostic if one positive element has a `__getitem__` + // method but another does not. This means `infer_subscript_expression_types` + // needs to return a `Result` rather than eagerly emitting diagnostics. + (Type::Intersection(_), _, _) => { + todo_type!("Subscript expressions on intersections") + } + // Ex) Given `("a", "b", "c", "d")[1]`, return `"b"` (Type::Tuple(tuple_ty), Type::IntLiteral(int), _) if i32::try_from(int).is_ok() => { let tuple = tuple_ty.tuple(self.db()); @@ -8176,6 +8173,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::unknown() }) } + // Ex) Given `("a", 1, Null)[0:2]`, return `("a", 1)` (Type::Tuple(tuple_ty), _, Some(SliceLiteral { start, stop, step })) => { let TupleSpec::Fixed(tuple) = tuple_ty.tuple(self.db()) else { @@ -8189,6 +8187,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::unknown() } } + // Ex) Given `"value"[1]`, return `"a"` (Type::StringLiteral(literal_ty), Type::IntLiteral(int), _) if i32::try_from(int).is_ok() => @@ -8212,6 +8211,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::unknown() }) } + // Ex) Given `"value"[1:3]`, return `"al"` (Type::StringLiteral(literal_ty), _, Some(SliceLiteral { start, stop, step })) => { let literal_value = literal_ty.value(self.db()); @@ -8226,6 +8226,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::unknown() } } + // Ex) Given `b"value"[1]`, return `97` (i.e., `ord(b"a")`) (Type::BytesLiteral(literal_ty), Type::IntLiteral(int), _) if i32::try_from(int).is_ok() => @@ -8249,6 +8250,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::unknown() }) } + // Ex) Given `b"value"[1:3]`, return `b"al"` (Type::BytesLiteral(literal_ty), _, Some(SliceLiteral { start, stop, step })) => { let literal_value = literal_ty.value(self.db()); @@ -8261,6 +8263,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::unknown() } } + // Ex) Given `"value"[True]`, return `"a"` ( Type::Tuple(_) | Type::StringLiteral(_) | Type::BytesLiteral(_), @@ -8271,6 +8274,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { value_ty, Type::IntLiteral(i64::from(bool)), ), + (Type::SpecialForm(SpecialFormType::Protocol), Type::Tuple(typevars), _) => { let TupleSpec::Fixed(typevars) = typevars.tuple(self.db()) else { // TODO: emit a diagnostic @@ -8284,6 +8288,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(context))) .unwrap_or_else(Type::unknown) } + (Type::SpecialForm(SpecialFormType::Protocol), typevar, _) => self .legacy_generic_class_context( value_node, @@ -8292,10 +8297,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) .map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(context))) .unwrap_or_else(Type::unknown), + (Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(_)), _, _) => { // TODO: emit a diagnostic todo_type!("doubly-specialized typing.Protocol") } + (Type::SpecialForm(SpecialFormType::Generic), Type::Tuple(typevars), _) => { let TupleSpec::Fixed(typevars) = typevars.tuple(self.db()) else { // TODO: emit a diagnostic @@ -8309,6 +8316,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(context))) .unwrap_or_else(Type::unknown) } + (Type::SpecialForm(SpecialFormType::Generic), typevar, _) => self .legacy_generic_class_context( value_node, @@ -8317,18 +8325,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) .map(|context| Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(context))) .unwrap_or_else(Type::unknown), + (Type::KnownInstance(KnownInstanceType::SubscriptedGeneric(_)), _, _) => { // TODO: emit a diagnostic todo_type!("doubly-specialized typing.Generic") } + (Type::SpecialForm(special_form), _, _) if special_form.class().is_special_form() => { todo_type!("Inference of subscript on special form") } + (Type::KnownInstance(known_instance), _, _) if known_instance.class().is_special_form() => { todo_type!("Inference of subscript on special form") } + (value_ty, slice_ty, _) => { // If the class defines `__getitem__`, return its return type. //