From fa9474f3a80c618a5eedbb00b9f5683ee6532732 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Jan 2026 22:24:34 +0000 Subject: [PATCH] Layer UnionDiagnostic and IntersectionDiagnostic for nested types When an intersection fails inside a union, report errors with both union and intersection context: - Add LayeredDiagnostic that combines both contexts - Show the correct intersection type (not the full union) - Include test with snapshot diagnostics demonstrating the layered output This addresses review comment 9 from PR #22469. --- .../resources/mdtest/call/union.md | 31 +++++ ...on_of_intersectio…_(db3e1dc3b7caa912).snap | 111 ++++++++++++++++++ .../ty_python_semantic/src/types/call/bind.rs | 75 +++++++++++- 3 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/union.md_-_Unions_in_calls_-_Union_of_intersectio…_(db3e1dc3b7caa912).snap diff --git a/crates/ty_python_semantic/resources/mdtest/call/union.md b/crates/ty_python_semantic/resources/mdtest/call/union.md index c944a49cb7..064830e15d 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/union.md +++ b/crates/ty_python_semantic/resources/mdtest/call/union.md @@ -834,3 +834,34 @@ def _(flag: bool): # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[str, int] & dict[Unknown | str, Unknown | int]`" f({"y": 1}) ``` + +## Union of intersections with failing bindings + + + +When calling a union where one element is an intersection of callables, and all bindings +in that intersection fail, we should report errors with both union and intersection context. + +```py +from ty_extensions import Intersection +from typing import Callable + +class IntCaller: + def __call__(self, x: int) -> int: + return x + +class StrCaller: + def __call__(self, x: str) -> str: + return x + +class BytesCaller: + def __call__(self, x: bytes) -> bytes: + return x + +def test(f: Intersection[IntCaller, StrCaller] | BytesCaller): + # Call with None - should fail for IntCaller, StrCaller, and BytesCaller + # error: [invalid-argument-type] + # error: [invalid-argument-type] + # error: [invalid-argument-type] + f(None) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/union.md_-_Unions_in_calls_-_Union_of_intersectio…_(db3e1dc3b7caa912).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/union.md_-_Unions_in_calls_-_Union_of_intersectio…_(db3e1dc3b7caa912).snap new file mode 100644 index 0000000000..84da848ac7 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/union.md_-_Unions_in_calls_-_Union_of_intersectio…_(db3e1dc3b7caa912).snap @@ -0,0 +1,111 @@ +--- +source: crates/ty_test/src/lib.rs +assertion_line: 623 +expression: snapshot +--- + +--- +mdtest name: union.md - Unions in calls - Union of intersections with failing bindings +mdtest path: crates/ty_python_semantic/resources/mdtest/call/union.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from ty_extensions import Intersection + 2 | from typing import Callable + 3 | + 4 | class IntCaller: + 5 | def __call__(self, x: int) -> int: + 6 | return x + 7 | + 8 | class StrCaller: + 9 | def __call__(self, x: str) -> str: +10 | return x +11 | +12 | class BytesCaller: +13 | def __call__(self, x: bytes) -> bytes: +14 | return x +15 | +16 | def test(f: Intersection[IntCaller, StrCaller] | BytesCaller): +17 | # Call with None - should fail for IntCaller, StrCaller, and BytesCaller +18 | # error: [invalid-argument-type] +19 | # error: [invalid-argument-type] +20 | # error: [invalid-argument-type] +21 | f(None) +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to bound method `__call__` is incorrect + --> src/mdtest_snippet.py:21:7 + | +19 | # error: [invalid-argument-type] +20 | # error: [invalid-argument-type] +21 | f(None) + | ^^^^ Expected `int`, found `None` + | +info: Method defined here + --> src/mdtest_snippet.py:5:9 + | +4 | class IntCaller: +5 | def __call__(self, x: int) -> int: + | ^^^^^^^^ ------ Parameter declared here +6 | return x + | +info: Intersection element `IntCaller` is incompatible with this call site +info: Attempted to call intersection type `IntCaller & StrCaller` +info: Attempted to call union type `(IntCaller & StrCaller) | BytesCaller` +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Argument to bound method `__call__` is incorrect + --> src/mdtest_snippet.py:21:7 + | +19 | # error: [invalid-argument-type] +20 | # error: [invalid-argument-type] +21 | f(None) + | ^^^^ Expected `str`, found `None` + | +info: Method defined here + --> src/mdtest_snippet.py:9:9 + | + 8 | class StrCaller: + 9 | def __call__(self, x: str) -> str: + | ^^^^^^^^ ------ Parameter declared here +10 | return x + | +info: Intersection element `StrCaller` is incompatible with this call site +info: Attempted to call intersection type `IntCaller & StrCaller` +info: Attempted to call union type `(IntCaller & StrCaller) | BytesCaller` +info: rule `invalid-argument-type` is enabled by default + +``` + +``` +error[invalid-argument-type]: Argument to bound method `__call__` is incorrect + --> src/mdtest_snippet.py:21:7 + | +19 | # error: [invalid-argument-type] +20 | # error: [invalid-argument-type] +21 | f(None) + | ^^^^ Expected `bytes`, found `None` + | +info: Method defined here + --> src/mdtest_snippet.py:13:9 + | +12 | class BytesCaller: +13 | def __call__(self, x: bytes) -> bytes: + | ^^^^^^^^ -------- Parameter declared here +14 | return x + | +info: Union variant `BytesCaller` is incompatible with this call site +info: Attempted to call union type `(IntCaller & StrCaller) | BytesCaller` +info: rule `invalid-argument-type` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index a8706ea973..7df4fec607 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -538,19 +538,38 @@ impl<'db> Bindings<'db> { return; } + let is_union = self.elements.len() > 1; + // For intersection elements, use priority hierarchy if element.is_intersection() { // Find the highest priority error among bindings in this element let max_priority = element.error_priority(); + // Construct the intersection type from the bindings + let intersection_type = IntersectionType::from_elements( + context.db(), + element.bindings.iter().map(|b| b.callable_type), + ); + // Only report errors from bindings with the highest priority for binding in &element.bindings { if binding.error_priority() == max_priority { - let intersection_diag = IntersectionDiagnostic { - callable_type: self.callable_type(), - binding, - }; - binding.report_diagnostics(context, node, Some(&intersection_diag)); + if is_union { + // Use layered diagnostic for intersection inside a union + let layered_diag = LayeredDiagnostic { + union_callable_type: self.callable_type(), + intersection_callable_type: intersection_type, + binding, + }; + binding.report_diagnostics(context, node, Some(&layered_diag)); + } else { + // Just intersection, no union context needed + let intersection_diag = IntersectionDiagnostic { + callable_type: intersection_type, + binding, + }; + binding.report_diagnostics(context, node, Some(&intersection_diag)); + } } } } else { @@ -4954,6 +4973,52 @@ impl CompoundDiagnostic for IntersectionDiagnostic<'_, '_> { } } +/// Contains both union and intersection context for layered diagnostics. +/// +/// Used when an intersection fails inside a union - we want to report both +/// that this is a union variant AND that this is an intersection element. +struct LayeredDiagnostic<'b, 'db> { + /// The type of the union. + union_callable_type: Type<'db>, + /// The type of the intersection (for intersection context). + intersection_callable_type: Type<'db>, + /// The specific binding that failed. + binding: &'b CallableBinding<'db>, +} + +impl CompoundDiagnostic for LayeredDiagnostic<'_, '_> { + fn add_context(&self, db: &dyn Db, diag: &mut Diagnostic) { + // Add intersection context first (more specific) + let sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!( + "Intersection element `{callable_ty}` is incompatible with this call site", + callable_ty = self.binding.callable_type.display(db), + ), + ); + diag.sub(sub); + + let sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!( + "Attempted to call intersection type `{}`", + self.intersection_callable_type.display(db) + ), + ); + diag.sub(sub); + + // Then add union context (outer layer) + let sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!( + "Attempted to call union type `{}`", + self.union_callable_type.display(db) + ), + ); + diag.sub(sub); + } +} + /// Represents the matching overload of a function literal that was found via the overload call /// evaluation algorithm. struct MatchingOverloadLiteral<'db> {