From 89236a3b2d1185590fa4c00495934d3c61b334b4 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Wed, 12 Nov 2025 20:20:13 -0500 Subject: [PATCH 1/5] Resolve overloads for hovers --- crates/ty_ide/src/goto.rs | 16 +- crates/ty_ide/src/hover.rs | 205 ++++++++---------- .../src/types/call/arguments.rs | 30 +++ .../src/types/ide_support.rs | 51 +++++ 4 files changed, 186 insertions(+), 116 deletions(-) diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 094d2008d2447f..d4649ca31dfa3d 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -15,7 +15,8 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; use ty_python_semantic::ResolvedDefinition; use ty_python_semantic::types::Type; use ty_python_semantic::types::ide_support::{ - call_signature_details, definitions_for_keyword_argument, + CallSignatureDetails, call_signature_details, call_signature_details_typed, + definitions_for_keyword_argument, }; use ty_python_semantic::{ HasDefinition, HasType, ImportAliasResolution, SemanticModel, definitions_for_imported_symbol, @@ -326,6 +327,19 @@ impl GotoTarget<'_> { Some(ty) } + pub(crate) fn signature<'db>( + &self, + model: &SemanticModel<'db>, + ) -> Option>> { + if let GotoTarget::Call { call, .. } = self { + let signature_details = call_signature_details_typed(model.db(), model, call); + if !signature_details.is_empty() { + return Some(signature_details); + } + } + None + } + /// Gets the definitions for this goto target. /// /// The `alias_resolution` parameter controls whether import aliases diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index c1278637a16994..c2a6ff9f75aca0 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -6,6 +6,7 @@ use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; use std::fmt; use std::fmt::Formatter; +use ty_python_semantic::types::ide_support::CallSignatureDetails; use ty_python_semantic::types::{KnownInstanceType, Type, TypeVarVariance}; use ty_python_semantic::{DisplaySettings, SemanticModel}; @@ -20,7 +21,6 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option Option typevar @@ -62,7 +63,7 @@ pub struct Hover<'db> { impl<'db> Hover<'db> { /// Renders the hover to a string using the specified markup kind. - pub const fn display<'a>(&'a self, db: &'a dyn Db, kind: MarkupKind) -> DisplayHover<'a> { + pub const fn display<'a>(&'a self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHover<'db, 'a> { DisplayHover { db, hover: self, @@ -93,13 +94,13 @@ impl<'a, 'db> IntoIterator for &'a Hover<'db> { } } -pub struct DisplayHover<'a> { - db: &'a dyn Db, - hover: &'a Hover<'a>, +pub struct DisplayHover<'db, 'a> { + db: &'db dyn Db, + hover: &'a Hover<'db>, kind: MarkupKind, } -impl fmt::Display for DisplayHover<'_> { +impl fmt::Display for DisplayHover<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let mut first = true; for content in &self.hover.contents { @@ -115,8 +116,9 @@ impl fmt::Display for DisplayHover<'_> { } } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone)] pub enum HoverContent<'db> { + Signature(Vec>), Type(Type<'db>, Option), Docstring(Docstring), } @@ -140,6 +142,15 @@ pub(crate) struct DisplayHoverContent<'a, 'db> { impl fmt::Display for DisplayHoverContent<'_, '_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.content { + HoverContent::Signature(signatures) => { + for signature in signatures { + self.kind + .fenced_code_block(&signature.label, "python") + .fmt(f)?; + self.kind.horizontal_line().fmt(f)?; + } + Ok(()) + } HoverContent::Type(ty, variance) => { let variance = match variance { Some(TypeVarVariance::Covariant) => " (covariant)", @@ -222,10 +233,9 @@ mod tests { ); assert_snapshot!(test.hover(), @r" - def my_func( - a, - b - ) -> Unknown + (a, b) -> Unknown + --------------------------------------------- + --------------------------------------------- This is such a great func!! @@ -235,11 +245,10 @@ mod tests { --------------------------------------------- ```python - def my_func( - a, - b - ) -> Unknown + (a, b) -> Unknown ``` + --- + --- ```text This is such a great func!! @@ -484,14 +493,18 @@ mod tests { ); assert_snapshot!(test.hover(), @r" - + (val) -> MyClass + --------------------------------------------- + --------------------------------------------- initializes MyClass (perfectly) --------------------------------------------- ```python - + (val) -> MyClass ``` + --- + --- ```text initializes MyClass (perfectly) @@ -543,14 +556,18 @@ mod tests { .build(); assert_snapshot!(test.hover(), @r" - + (val) -> MyClass + --------------------------------------------- + --------------------------------------------- initializes MyClass (perfectly) --------------------------------------------- ```python - + (val) -> MyClass ``` + --- + --- ```text initializes MyClass (perfectly) @@ -601,7 +618,9 @@ mod tests { ); assert_snapshot!(test.hover(), @r" - + (val) -> MyClass + --------------------------------------------- + --------------------------------------------- This is such a great class!! @@ -611,8 +630,10 @@ mod tests { --------------------------------------------- ```python - + (val) -> MyClass ``` + --- + --- ```text This is such a great class!! @@ -669,10 +690,9 @@ mod tests { ); assert_snapshot!(test.hover(), @r" - bound method MyClass.my_method( - a, - b - ) -> Unknown + (a, b) -> Unknown + --------------------------------------------- + --------------------------------------------- This is such a great func!! @@ -682,11 +702,10 @@ mod tests { --------------------------------------------- ```python - bound method MyClass.my_method( - a, - b - ) -> Unknown + (a, b) -> Unknown ``` + --- + --- ```text This is such a great func!! @@ -961,15 +980,17 @@ def ab(a: str): ... assert_snapshot!(test.hover(), @r" (a: int) -> Unknown - (a: str) -> Unknown + --------------------------------------------- + --------------------------------------------- the int overload --------------------------------------------- ```python (a: int) -> Unknown - (a: str) -> Unknown ``` + --- + --- ```text the int overload @@ -1025,16 +1046,18 @@ def ab(a: str): .build(); assert_snapshot!(test.hover(), @r#" - (a: int) -> Unknown (a: str) -> Unknown + --------------------------------------------- + --------------------------------------------- the int overload --------------------------------------------- ```python - (a: int) -> Unknown (a: str) -> Unknown ``` + --- + --- ```text the int overload @@ -1090,22 +1113,18 @@ def ab(a: int): .build(); assert_snapshot!(test.hover(), @r" - ( - a: int, - b: int - ) -> Unknown - (a: int) -> Unknown + (a: int, b: int) -> Unknown + --------------------------------------------- + --------------------------------------------- the two arg overload --------------------------------------------- ```python - ( - a: int, - b: int - ) -> Unknown - (a: int) -> Unknown + (a: int, b: int) -> Unknown ``` + --- + --- ```text the two arg overload @@ -1161,22 +1180,18 @@ def ab(a: int): .build(); assert_snapshot!(test.hover(), @r" - ( - a: int, - b: int - ) -> Unknown (a: int) -> Unknown + --------------------------------------------- + --------------------------------------------- the two arg overload --------------------------------------------- ```python - ( - a: int, - b: int - ) -> Unknown (a: int) -> Unknown ``` + --- + --- ```text the two arg overload @@ -1236,34 +1251,18 @@ def ab(a: int, *, c: int): .build(); assert_snapshot!(test.hover(), @r" - (a: int) -> Unknown - ( - a: int, - *, - b: int - ) -> Unknown - ( - a: int, - *, - c: int - ) -> Unknown + (a: int, *, b: int) -> Unknown + --------------------------------------------- + --------------------------------------------- keywordless overload --------------------------------------------- ```python - (a: int) -> Unknown - ( - a: int, - *, - b: int - ) -> Unknown - ( - a: int, - *, - c: int - ) -> Unknown + (a: int, *, b: int) -> Unknown ``` + --- + --- ```text keywordless overload @@ -1323,34 +1322,18 @@ def ab(a: int, *, c: int): .build(); assert_snapshot!(test.hover(), @r" - (a: int) -> Unknown - ( - a: int, - *, - b: int - ) -> Unknown - ( - a: int, - *, - c: int - ) -> Unknown + (a: int, *, c: int) -> Unknown + --------------------------------------------- + --------------------------------------------- keywordless overload --------------------------------------------- ```python - (a: int) -> Unknown - ( - a: int, - *, - b: int - ) -> Unknown - ( - a: int, - *, - c: int - ) -> Unknown + (a: int, *, c: int) -> Unknown ``` + --- + --- ```text keywordless overload @@ -1397,28 +1380,18 @@ def ab(a: int, *, c: int): ); assert_snapshot!(test.hover(), @r#" - ( - a: int, - b - ) -> Unknown - ( - a: str, - b - ) -> Unknown + (a: int, b) -> Unknown + --------------------------------------------- + --------------------------------------------- The first overload --------------------------------------------- ```python - ( - a: int, - b - ) -> Unknown - ( - a: str, - b - ) -> Unknown + (a: int, b) -> Unknown ``` + --- + --- ```text The first overload @@ -1465,15 +1438,17 @@ def ab(a: int, *, c: int): assert_snapshot!(test.hover(), @r#" (a: int) -> Unknown - (a: str) -> Unknown + --------------------------------------------- + --------------------------------------------- The first overload --------------------------------------------- ```python (a: int) -> Unknown - (a: str) -> Unknown ``` + --- + --- ```text The first overload diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index fc8bf871e5e707..62c8f38ebddce0 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -66,6 +66,36 @@ impl<'a, 'db> CallArguments<'a, 'db> { .collect() } + /// Like [`Self::from_arguments`] but fills as much typing info in as possible. + pub(crate) fn from_arguments_typed( + arguments: &'a ast::Arguments, + mut infer_argument_type: impl FnMut(Option<&ast::Expr>, &ast::Expr) -> Type<'db>, + ) -> Self { + arguments + .arguments_source_order() + .map(|arg_or_keyword| match arg_or_keyword { + ast::ArgOrKeyword::Arg(arg) => match arg { + ast::Expr::Starred(ast::ExprStarred { value, .. }) => { + let ty = infer_argument_type(Some(arg), value); + (Argument::Variadic, Some(ty)) + } + _ => { + let ty = infer_argument_type(None, arg); + (Argument::Positional, Some(ty)) + } + }, + ast::ArgOrKeyword::Keyword(ast::Keyword { arg, value, .. }) => { + let ty = infer_argument_type(None, value); + if let Some(arg) = arg { + (Argument::Keyword(&arg.id), Some(ty)) + } else { + (Argument::Keywords, Some(ty)) + } + } + }) + .collect() + } + /// Create a [`CallArguments`] with no arguments. pub(crate) fn none() -> Self { Self::default() diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 475c3017c7b6fc..b3c0822c64e31a 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -973,6 +973,57 @@ pub fn call_signature_details<'db>( } } +/// Extract signature details from a function call expression using type info. +/// +/// Unlike [`call_signature_details`][] we reduce down to the exact match if possible. +pub fn call_signature_details_typed<'db>( + db: &'db dyn Db, + model: &SemanticModel<'db>, + call_expr: &ast::ExprCall, +) -> Vec> { + let func_type = call_expr.func.inferred_type(model); + + // Use into_callable to handle all the complex type conversions + if let Some(callable_type) = func_type.try_upcast_to_callable(db) { + // Really shove as much type info in as we can + let call_arguments = + CallArguments::from_arguments_typed(&call_expr.arguments, |_, splatted_value| { + splatted_value.inferred_type(model) + }); + + // Extract signature details from all callable bindings + callable_type + .bindings(db) + .match_parameters(db, &call_arguments) + .check_types(db, &call_arguments, TypeContext::default(), &[]) + // Only use the Ok + .iter() + .flatten() + // The first matching overload is the one to use + .filter_map(|binding| binding.matching_overloads().next()) + .map(|(_, binding)| { + let argument_to_parameter_mapping = binding.argument_matches().to_vec(); + let signature = binding.signature.clone(); + let display_details = signature.display(db).to_string_parts(); + let parameter_label_offsets = display_details.parameter_ranges; + let parameter_names = display_details.parameter_names; + + CallSignatureDetails { + definition: signature.definition(), + signature, + label: display_details.label, + parameter_label_offsets, + parameter_names, + argument_to_parameter_mapping, + } + }) + .collect() + } else { + // Type is not callable, return empty signatures + vec![] + } +} + /// Returns the definitions of the binary operation along with its callable type. pub fn definitions_for_bin_op<'db>( db: &'db dyn Db, From 3a10f87471eed9d8fbc68f263e363ba22d3f8f82 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Wed, 12 Nov 2025 23:23:29 -0500 Subject: [PATCH 2/5] only apply the rule if non-trivial --- crates/ty_ide/src/goto.rs | 9 ++++--- crates/ty_ide/src/hover.rs | 52 ++++++++++++++++---------------------- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index d4649ca31dfa3d..94d22fccffc572 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -332,9 +332,12 @@ impl GotoTarget<'_> { model: &SemanticModel<'db>, ) -> Option>> { if let GotoTarget::Call { call, .. } = self { - let signature_details = call_signature_details_typed(model.db(), model, call); - if !signature_details.is_empty() { - return Some(signature_details); + let signature_details = call_signature_details(model.db(), model, call); + if signature_details.len() > 1 { + let signature_details = call_signature_details_typed(model.db(), model, call); + if !signature_details.is_empty() { + return Some(signature_details); + } } } None diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index c2a6ff9f75aca0..cf9a66bf28ee2e 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -233,9 +233,10 @@ mod tests { ); assert_snapshot!(test.hover(), @r" - (a, b) -> Unknown - --------------------------------------------- - + def my_func( + a, + b + ) -> Unknown --------------------------------------------- This is such a great func!! @@ -245,10 +246,11 @@ mod tests { --------------------------------------------- ```python - (a, b) -> Unknown + def my_func( + a, + b + ) -> Unknown ``` - --- - --- ```text This is such a great func!! @@ -493,18 +495,14 @@ mod tests { ); assert_snapshot!(test.hover(), @r" - (val) -> MyClass - --------------------------------------------- - + --------------------------------------------- initializes MyClass (perfectly) --------------------------------------------- ```python - (val) -> MyClass + ``` - --- - --- ```text initializes MyClass (perfectly) @@ -556,18 +554,14 @@ mod tests { .build(); assert_snapshot!(test.hover(), @r" - (val) -> MyClass - --------------------------------------------- - + --------------------------------------------- initializes MyClass (perfectly) --------------------------------------------- ```python - (val) -> MyClass + ``` - --- - --- ```text initializes MyClass (perfectly) @@ -618,9 +612,7 @@ mod tests { ); assert_snapshot!(test.hover(), @r" - (val) -> MyClass - --------------------------------------------- - + --------------------------------------------- This is such a great class!! @@ -630,10 +622,8 @@ mod tests { --------------------------------------------- ```python - (val) -> MyClass + ``` - --- - --- ```text This is such a great class!! @@ -690,9 +680,10 @@ mod tests { ); assert_snapshot!(test.hover(), @r" - (a, b) -> Unknown - --------------------------------------------- - + bound method MyClass.my_method( + a, + b + ) -> Unknown --------------------------------------------- This is such a great func!! @@ -702,10 +693,11 @@ mod tests { --------------------------------------------- ```python - (a, b) -> Unknown + bound method MyClass.my_method( + a, + b + ) -> Unknown ``` - --- - --- ```text This is such a great func!! From dcc451d4d2fcf76b4c930f6aaa2f66b4830f7a04 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Thu, 13 Nov 2025 15:31:21 -0500 Subject: [PATCH 3/5] cleanup implementation --- crates/ty_ide/src/goto.rs | 21 ++-- crates/ty_ide/src/hover.rs | 101 +++++++++--------- .../src/types/call/arguments.rs | 3 + .../src/types/ide_support.rs | 86 ++++++++------- 4 files changed, 108 insertions(+), 103 deletions(-) diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 94d22fccffc572..23ff8e0f53f45f 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -15,8 +15,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; use ty_python_semantic::ResolvedDefinition; use ty_python_semantic::types::Type; use ty_python_semantic::types::ide_support::{ - CallSignatureDetails, call_signature_details, call_signature_details_typed, - definitions_for_keyword_argument, + call_signature_details, call_type_simplified_by_overloads, definitions_for_keyword_argument, }; use ty_python_semantic::{ HasDefinition, HasType, ImportAliasResolution, SemanticModel, definitions_for_imported_symbol, @@ -327,20 +326,16 @@ impl GotoTarget<'_> { Some(ty) } - pub(crate) fn signature<'db>( + /// Try to get a simplified display of this callable type by resolving overloads + pub(crate) fn call_type_simplified_by_overloads( &self, - model: &SemanticModel<'db>, - ) -> Option>> { + model: &SemanticModel, + ) -> Option { if let GotoTarget::Call { call, .. } = self { - let signature_details = call_signature_details(model.db(), model, call); - if signature_details.len() > 1 { - let signature_details = call_signature_details_typed(model.db(), model, call); - if !signature_details.is_empty() { - return Some(signature_details); - } - } + call_type_simplified_by_overloads(model.db(), model, call) + } else { + None } - None } /// Gets the definitions for this goto target. diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index cf9a66bf28ee2e..f1de7316bd59cd 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -6,7 +6,6 @@ use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; use std::fmt; use std::fmt::Formatter; -use ty_python_semantic::types::ide_support::CallSignatureDetails; use ty_python_semantic::types::{KnownInstanceType, Type, TypeVarVariance}; use ty_python_semantic::{DisplaySettings, SemanticModel}; @@ -31,7 +30,7 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option { #[derive(Debug, Clone)] pub enum HoverContent<'db> { - Signature(Vec>), + Signature(String), Type(Type<'db>, Option), Docstring(Docstring), } @@ -142,14 +141,8 @@ pub(crate) struct DisplayHoverContent<'a, 'db> { impl fmt::Display for DisplayHoverContent<'_, '_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.content { - HoverContent::Signature(signatures) => { - for signature in signatures { - self.kind - .fenced_code_block(&signature.label, "python") - .fmt(f)?; - self.kind.horizontal_line().fmt(f)?; - } - Ok(()) + HoverContent::Signature(signature) => { + self.kind.fenced_code_block(&signature, "python").fmt(f) } HoverContent::Type(ty, variance) => { let variance = match variance { @@ -972,8 +965,6 @@ def ab(a: str): ... assert_snapshot!(test.hover(), @r" (a: int) -> Unknown - --------------------------------------------- - --------------------------------------------- the int overload @@ -981,8 +972,6 @@ def ab(a: str): ... ```python (a: int) -> Unknown ``` - --- - --- ```text the int overload @@ -1039,8 +1028,6 @@ def ab(a: str): assert_snapshot!(test.hover(), @r#" (a: str) -> Unknown - --------------------------------------------- - --------------------------------------------- the int overload @@ -1048,8 +1035,6 @@ def ab(a: str): ```python (a: str) -> Unknown ``` - --- - --- ```text the int overload @@ -1105,18 +1090,20 @@ def ab(a: int): .build(); assert_snapshot!(test.hover(), @r" - (a: int, b: int) -> Unknown - --------------------------------------------- - + ( + a: int, + b: int + ) -> Unknown --------------------------------------------- the two arg overload --------------------------------------------- ```python - (a: int, b: int) -> Unknown + ( + a: int, + b: int + ) -> Unknown ``` - --- - --- ```text the two arg overload @@ -1173,8 +1160,6 @@ def ab(a: int): assert_snapshot!(test.hover(), @r" (a: int) -> Unknown - --------------------------------------------- - --------------------------------------------- the two arg overload @@ -1182,8 +1167,6 @@ def ab(a: int): ```python (a: int) -> Unknown ``` - --- - --- ```text the two arg overload @@ -1243,18 +1226,22 @@ def ab(a: int, *, c: int): .build(); assert_snapshot!(test.hover(), @r" - (a: int, *, b: int) -> Unknown - --------------------------------------------- - + ( + a: int, + *, + b: int + ) -> Unknown --------------------------------------------- keywordless overload --------------------------------------------- ```python - (a: int, *, b: int) -> Unknown + ( + a: int, + *, + b: int + ) -> Unknown ``` - --- - --- ```text keywordless overload @@ -1314,18 +1301,22 @@ def ab(a: int, *, c: int): .build(); assert_snapshot!(test.hover(), @r" - (a: int, *, c: int) -> Unknown - --------------------------------------------- - + ( + a: int, + *, + c: int + ) -> Unknown --------------------------------------------- keywordless overload --------------------------------------------- ```python - (a: int, *, c: int) -> Unknown + ( + a: int, + *, + c: int + ) -> Unknown ``` - --- - --- ```text keywordless overload @@ -1372,18 +1363,28 @@ def ab(a: int, *, c: int): ); assert_snapshot!(test.hover(), @r#" - (a: int, b) -> Unknown - --------------------------------------------- - + ( + a: int, + b + ) -> Unknown + ( + a: str, + b + ) -> Unknown --------------------------------------------- The first overload --------------------------------------------- ```python - (a: int, b) -> Unknown + ( + a: int, + b + ) -> Unknown + ( + a: str, + b + ) -> Unknown ``` - --- - --- ```text The first overload @@ -1430,17 +1431,15 @@ def ab(a: int, *, c: int): assert_snapshot!(test.hover(), @r#" (a: int) -> Unknown - --------------------------------------------- - + (a: str) -> Unknown --------------------------------------------- The first overload --------------------------------------------- ```python (a: int) -> Unknown + (a: str) -> Unknown ``` - --- - --- ```text The first overload diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 62c8f38ebddce0..6da85184eae832 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -67,6 +67,9 @@ impl<'a, 'db> CallArguments<'a, 'db> { } /// Like [`Self::from_arguments`] but fills as much typing info in as possible. + /// + /// This currently only exists for the LSP usecase, and shouldn't be used in normal + /// typechecking. pub(crate) fn from_arguments_typed( arguments: &'a ast::Arguments, mut infer_argument_type: impl FnMut(Option<&ast::Expr>, &ast::Expr) -> Type<'db>, diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index b3c0822c64e31a..8b7b44f7e39d05 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -17,7 +17,7 @@ use crate::types::{ ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type, TypeContext, TypeVarBoundOrConstraints, class::CodeGeneratorKind, }; -use crate::{Db, HasType, NameKind, SemanticModel}; +use crate::{Db, DisplaySettings, HasType, NameKind, SemanticModel}; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; @@ -973,55 +973,63 @@ pub fn call_signature_details<'db>( } } -/// Extract signature details from a function call expression using type info. +/// Given a call expression that has overloads, and whose overload is resolved to a +/// single option by its arguments, return the type of the Signature. /// -/// Unlike [`call_signature_details`][] we reduce down to the exact match if possible. -pub fn call_signature_details_typed<'db>( +/// This is only used for simplifying complex call types, so if we ever detect that +/// the given callable type *is* simple, or that our answer *won't* be simple, we +/// bail at out and return None, so that the original type can be used. +/// +/// We do this because `Type::Signature` intentionally loses a lot of context, and +/// so it has a "worse" display than say `Type::FunctionLiteral` or `Type::BoundMethod`, +/// which this analysis would naturally wipe away. The contexts this function +/// succeeds in are those where we would print a complicated/ugly type anyway. +pub fn call_type_simplified_by_overloads<'db>( db: &'db dyn Db, model: &SemanticModel<'db>, call_expr: &ast::ExprCall, -) -> Vec> { +) -> Option { let func_type = call_expr.func.inferred_type(model); // Use into_callable to handle all the complex type conversions - if let Some(callable_type) = func_type.try_upcast_to_callable(db) { - // Really shove as much type info in as we can - let call_arguments = - CallArguments::from_arguments_typed(&call_expr.arguments, |_, splatted_value| { - splatted_value.inferred_type(model) - }); + let callable_type = func_type.try_upcast_to_callable(db)?; + let bindings = callable_type.bindings(db); - // Extract signature details from all callable bindings - callable_type - .bindings(db) - .match_parameters(db, &call_arguments) - .check_types(db, &call_arguments, TypeContext::default(), &[]) - // Only use the Ok - .iter() - .flatten() - // The first matching overload is the one to use - .filter_map(|binding| binding.matching_overloads().next()) - .map(|(_, binding)| { - let argument_to_parameter_mapping = binding.argument_matches().to_vec(); - let signature = binding.signature.clone(); - let display_details = signature.display(db).to_string_parts(); - let parameter_label_offsets = display_details.parameter_ranges; - let parameter_names = display_details.parameter_names; + // If the callable is trivial this analysis is useless, bail out + if let Some(binding) = bindings.single_element() + && binding.overloads().len() < 2 + { + return None; + } - CallSignatureDetails { - definition: signature.definition(), - signature, - label: display_details.label, - parameter_label_offsets, - parameter_names, - argument_to_parameter_mapping, - } + // Hand the overload resolution system as much type info as we have + let args = CallArguments::from_arguments_typed(&call_expr.arguments, |_, splatted_value| { + splatted_value.inferred_type(model) + }); + + // Try to resolve overloads with the arguments/types we have + let mut resolved = bindings + .match_parameters(db, &args) + .check_types(db, &args, TypeContext::default(), &[]) + // Only use the Ok + .iter() + .flatten() + .flat_map(|binding| { + binding.matching_overloads().map(|(_, overload)| { + overload + .signature + .display_with(db, DisplaySettings::default().multiline()) + .to_string() }) - .collect() - } else { - // Type is not callable, return empty signatures - vec![] + }) + .collect::>(); + + // If at the end of this we still got multiple signatures (or no signatures), give up + if resolved.len() != 1 { + return None; } + + resolved.pop() } /// Returns the definitions of the binary operation along with its callable type. From 42abe02eac76d7b54171265abe460330a8f943b1 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Thu, 13 Nov 2025 16:28:32 -0500 Subject: [PATCH 4/5] print function names for bare Signatures opportunistically --- crates/ty_ide/src/hover.rs | 20 ++++----- .../ty_python_semantic/src/types/display.rs | 45 +++++++++++++++---- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index f1de7316bd59cd..d4463fdd34da07 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -1090,7 +1090,7 @@ def ab(a: int): .build(); assert_snapshot!(test.hover(), @r" - ( + def ab( a: int, b: int ) -> Unknown @@ -1099,7 +1099,7 @@ def ab(a: int): --------------------------------------------- ```python - ( + def ab( a: int, b: int ) -> Unknown @@ -1226,7 +1226,7 @@ def ab(a: int, *, c: int): .build(); assert_snapshot!(test.hover(), @r" - ( + def ab( a: int, *, b: int @@ -1236,7 +1236,7 @@ def ab(a: int, *, c: int): --------------------------------------------- ```python - ( + def ab( a: int, *, b: int @@ -1301,7 +1301,7 @@ def ab(a: int, *, c: int): .build(); assert_snapshot!(test.hover(), @r" - ( + def ab( a: int, *, c: int @@ -1311,7 +1311,7 @@ def ab(a: int, *, c: int): --------------------------------------------- ```python - ( + def ab( a: int, *, c: int @@ -1363,11 +1363,11 @@ def ab(a: int, *, c: int): ); assert_snapshot!(test.hover(), @r#" - ( + def foo( a: int, b ) -> Unknown - ( + def foo( a: str, b ) -> Unknown @@ -1376,11 +1376,11 @@ def ab(a: int, *, c: int): --------------------------------------------- ```python - ( + def foo( a: int, b ) -> Unknown - ( + def foo( a: str, b ) -> Unknown diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index b8a8a05ac4ca02..e1d9cfe303e40d 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -16,6 +16,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use crate::Db; use crate::module_resolver::file_to_module; +use crate::semantic_index::definition::Definition; use crate::semantic_index::{scope::ScopeKind, semantic_index}; use crate::types::class::{ClassLiteral, ClassType, GenericAlias}; use crate::types::function::{FunctionType, OverloadLiteral}; @@ -40,6 +41,9 @@ pub struct DisplaySettings<'db> { pub qualified: Rc>, /// Whether long unions and literals are displayed in full pub preserve_full_unions: bool, + /// Disallow Signature printing to introduce a name + /// (presumably because we rendered one already) + pub disallow_signature_name: bool, } impl<'db> DisplaySettings<'db> { @@ -59,6 +63,14 @@ impl<'db> DisplaySettings<'db> { } } + #[must_use] + pub fn disallow_signature_name(&self) -> Self { + Self { + disallow_signature_name: true, + ..self.clone() + } + } + #[must_use] pub fn truncate_long_unions(self) -> Self { Self { @@ -473,7 +485,7 @@ impl Display for DisplayRepresentation<'_> { type_parameters = type_parameters, signature = signature .bind_self(self.db, Some(typing_self_ty)) - .display_with(self.db, self.settings.clone()) + .display_with(self.db, self.settings.disallow_signature_name()) ) } signatures => { @@ -768,7 +780,7 @@ impl Display for DisplayOverloadLiteral<'_> { "def {name}{type_parameters}{signature}", name = self.literal.name(self.db), type_parameters = type_parameters, - signature = signature.display_with(self.db, self.settings.clone()) + signature = signature.display_with(self.db, self.settings.disallow_signature_name()) ) } } @@ -810,7 +822,8 @@ impl Display for DisplayFunctionType<'_> { "def {name}{type_parameters}{signature}", name = self.ty.name(self.db), type_parameters = type_parameters, - signature = signature.display_with(self.db, self.settings.clone()) + signature = + signature.display_with(self.db, self.settings.disallow_signature_name()) ) } signatures => { @@ -1081,6 +1094,7 @@ impl<'db> Signature<'db> { settings: DisplaySettings<'db>, ) -> DisplaySignature<'db> { DisplaySignature { + definition: self.definition(), parameters: self.parameters(), return_ty: self.return_ty, db, @@ -1090,6 +1104,7 @@ impl<'db> Signature<'db> { } pub(crate) struct DisplaySignature<'db> { + definition: Option>, parameters: &'db Parameters<'db>, return_ty: Option>, db: &'db dyn Db, @@ -1111,6 +1126,18 @@ impl DisplaySignature<'_> { /// Internal method to write signature with the signature writer fn write_signature(&self, writer: &mut SignatureWriter) -> fmt::Result { let multiline = self.settings.multiline && self.parameters.len() > 1; + // If we're multiline printing and a name hasn't been emitted, try to + // make one up to make things more pretty + if multiline && !self.settings.disallow_signature_name { + writer.write_str("def ")?; + if let Some(definition) = self.definition + && let Some(name) = definition.name(self.db) + { + writer.write_str(&name)?; + } else { + writer.write_str("_")?; + } + } // Opening parenthesis writer.write_char('(')?; if multiline { @@ -1979,7 +2006,7 @@ mod tests { Some(Type::none(&db)) ), @r" - ( + def _( x=int, y: str = str ) -> None @@ -1997,7 +2024,7 @@ mod tests { Some(Type::none(&db)) ), @r" - ( + def _( x, y, / @@ -2016,7 +2043,7 @@ mod tests { Some(Type::none(&db)) ), @r" - ( + def _( x, /, y @@ -2035,7 +2062,7 @@ mod tests { Some(Type::none(&db)) ), @r" - ( + def _( *, x, y @@ -2054,7 +2081,7 @@ mod tests { Some(Type::none(&db)) ), @r" - ( + def _( x, *, y @@ -2093,7 +2120,7 @@ mod tests { Some(KnownClass::Bytes.to_instance(&db)) ), @r" - ( + def _( a, b: int, c=Literal[1], From cfd75914bed80d529026d771c58c7fdec400259e Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Fri, 14 Nov 2025 16:08:45 -0500 Subject: [PATCH 5/5] render signatures --- crates/ty_ide/src/docstring.rs | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/crates/ty_ide/src/docstring.rs b/crates/ty_ide/src/docstring.rs index b749b1feb30bbc..be5adc1052588d 100644 --- a/crates/ty_ide/src/docstring.rs +++ b/crates/ty_ide/src/docstring.rs @@ -65,11 +65,10 @@ impl Docstring { /// Render the docstring for markdown display pub fn render_markdown(&self) -> String { let trimmed = documentation_trim(&self.0); - // TODO: now actually parse it and "render" it to markdown. - // - // For now we just wrap the content in a plaintext codeblock - // to avoid the contents erroneously being interpreted as markdown. - format!("```text\n{trimmed}\n```") + + // Try to parse and render the contents as markdown, + // and if we fail, wrap it in a codeblock and display it raw. + try_render_markdown(&trimmed).unwrap_or_else(|| format!("```text\n{trimmed}\n```")) } /// Extract parameter documentation from popular docstring formats. @@ -153,6 +152,26 @@ fn documentation_trim(docs: &str) -> String { output } +fn try_render_markdown(docstring: &str) -> Option { + let mut output = String::new(); + let mut first_line = true; + for line in docstring.lines() { + // We can assume leading whitespace has been normalized + let trimmed_line = line.trim_start_matches(' '); + let num_leading_spaces = line.len() - trimmed_line.len(); + + if !first_line { + output.push_str(" \n"); + } + for _ in 0..num_leading_spaces { + output.push_str(" "); + } + output.push_str(trimmed_line); + first_line = false; + } + Some(output) +} + /// Extract parameter documentation from Google-style docstrings. fn extract_google_style_params(docstring: &str) -> HashMap { let mut param_docs = HashMap::new();