Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion crates/ty_ide/src/goto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +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::{
call_signature_details, 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,
Expand Down Expand Up @@ -326,6 +326,18 @@ impl GotoTarget<'_> {
Some(ty)
}

/// Try to get a simplified display of this callable type by resolving overloads
pub(crate) fn call_type_simplified_by_overloads(
&self,
model: &SemanticModel,
) -> Option<String> {
if let GotoTarget::Call { call, .. } = self {
call_type_simplified_by_overloads(model.db(), model, call)
} else {
None
}
}

/// Gets the definitions for this goto target.
///
/// The `alias_resolution` parameter controls whether import aliases
Expand Down
60 changes: 13 additions & 47 deletions crates/ty_ide/src/hover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Ho
}

let model = SemanticModel::new(db, file);
let ty = goto_target.inferred_type(&model);
let docs = goto_target
.get_definition_targets(
file,
Expand All @@ -30,9 +29,10 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Ho
.and_then(|definitions| definitions.docstring(db))
.map(HoverContent::Docstring);

// TODO: Render the symbol's signature instead of just its type.
let mut contents = Vec::new();
if let Some(ty) = ty {
if let Some(signature) = goto_target.call_type_simplified_by_overloads(&model) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we only apply overload simplification for hover mode? For example, TypeScript jumps to the matching overload if you use "go to definition," which matches my expectation. https://www.typescriptlang.org/play/?#code/GYVwdgxgLglg9mABFApgZygCgIYC5FggC2ARigE4CU+hpFA3AFCiSwLLpZ4HFnkA0iEvgzkYYAObVEo8RKYto8JKgw4avCoJIB+EVDGTp2MAE9EAb0aMAvtcaqsARgAMggESP3le0A

I'm surprised that Pylance doesn't seem to do that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm strictly speaking I don't think goto-definition should jump to an overload unless we can't find the actual implementation (in which case it's actually goto-declaration). goto-declaration should ideally jump to the overload though.

contents.push(HoverContent::Signature(signature));
} else if let Some(ty) = goto_target.inferred_type(&model) {
tracing::debug!("Inferred type of covering node is {}", ty.display(db));
contents.push(match ty {
Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => typevar
Expand Down Expand Up @@ -62,7 +62,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,
Expand Down Expand Up @@ -93,13 +93,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,
}
Comment on lines -96 to 100
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is debris from versions where this was being asked to store CallSignatureDetails, which is actually invariant over 'db. Leaving it like this just simplifies any potential future work.


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 {
Expand All @@ -115,8 +115,9 @@ impl fmt::Display for DisplayHover<'_> {
}
}

#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Debug, Clone)]
pub enum HoverContent<'db> {
Signature(String),
Type(Type<'db>, Option<TypeVarVariance>),
Docstring(Docstring),
}
Expand All @@ -140,6 +141,9 @@ 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(signature) => {
self.kind.fenced_code_block(&signature, "python").fmt(f)
}
HoverContent::Type(ty, variance) => {
let variance = match variance {
Some(TypeVarVariance::Covariant) => " (covariant)",
Expand Down Expand Up @@ -961,14 +965,12 @@ 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
Expand Down Expand Up @@ -1025,14 +1027,12 @@ 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
```
---
Expand Down Expand Up @@ -1094,7 +1094,6 @@ def ab(a: int):
a: int,
b: int
) -> Unknown
(a: int) -> Unknown
---------------------------------------------
the two arg overload

Expand All @@ -1104,7 +1103,6 @@ def ab(a: int):
a: int,
b: int
) -> Unknown
(a: int) -> Unknown
```
---
```text
Expand Down Expand Up @@ -1161,20 +1159,12 @@ 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
```
---
Expand Down Expand Up @@ -1236,33 +1226,21 @@ 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
---------------------------------------------
keywordless overload

---------------------------------------------
```python
(a: int) -> Unknown
(
a: int,
*,
b: int
) -> Unknown
(
a: int,
*,
c: int
) -> Unknown
```
---
```text
Expand Down Expand Up @@ -1323,12 +1301,6 @@ def ab(a: int, *, c: int):
.build();

assert_snapshot!(test.hover(), @r"
(a: int) -> Unknown
(
a: int,
*,
b: int
) -> Unknown
(
a: int,
*,
Expand All @@ -1339,12 +1311,6 @@ def ab(a: int, *, c: int):

---------------------------------------------
```python
(a: int) -> Unknown
(
a: int,
*,
b: int
) -> Unknown
(
a: int,
*,
Expand Down
33 changes: 33 additions & 0 deletions crates/ty_python_semantic/src/types/call/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,39 @@ impl<'a, 'db> CallArguments<'a, 'db> {
.collect()
}

/// 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>,
) -> 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()
Expand Down
61 changes: 60 additions & 1 deletion crates/ty_python_semantic/src/types/ide_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -973,6 +973,65 @@ pub fn call_signature_details<'db>(
}
}

/// 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.
///
/// 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,
) -> Option<String> {
let func_type = call_expr.func.inferred_type(model);

// Use into_callable to handle all the complex type conversions
let callable_type = func_type.try_upcast_to_callable(db)?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is my understanding correct that this adds another "todo" call-site to astral-sh/ty#1086?

Copy link
Contributor

@carljm carljm Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we'll be able to do astral-sh/ty#1086; commented on that issue.

let bindings = callable_type.bindings(db);

// If the callable is trivial this analysis is useless, bail out
if let Some(binding) = bindings.single_element()
&& binding.overloads().len() < 2
{
return None;
}

// 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()
})
Comment on lines +1018 to +1023
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spent an unconscionable amount of time trying to have this return a Type or something more useful than a String but the types on Binding are haunted by the overloads when you try to render them (meaning printing them still returns the full overload set!).

Stringifying the raw Signature is the only thing that seems to be safe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(An early version returned a full CallSignatureDetails but this usecase has no use of that so I dropped it as a waste of effort. Also this version that just returns a String is easier to iterate on (I kept trying to Not have a String and had to refactor like 4 calls and 2 types over and over as I experimented between Type vs Vec<Type> vs CallSignatureDetails vs...))

})
Comment on lines +1017 to +1024
Copy link
Member

@dhruvmanila dhruvmanila Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I remember now facing a similar problem regarding using the signature_type on Binding struct that we saw during our call last week. For context, the signature_type would represent the FunctionLiteral which means the display would contain all the overloads and this is the main reason we need to use the display of Signature to display the matching overload which leads to requiring #21438.

I faced this issue while implementing #18452 and the way I resolved it is by capturing the required information (FunctionType, BoundMethodType, etc. and the matching overload index) in MatchingOverloadLiteral and implementing a .get method to return the matching OverloadLiteral.

I think we could possibly use a similar approach to get the actual OverloadLiteral that needs to be displayed as it does implement the Display trait. You might find it useful to go through this part of the code:

// TODO: This should probably be adapted to handle more
// types of callables[1]. At present, it just handles
// standard function and method calls.
//
// [1]: https://github.com/astral-sh/ty/issues/274#issuecomment-2881856028
let function_type_and_kind = match self.signature_type {
Type::FunctionLiteral(function) => Some((FunctionKind::Function, function)),
Type::BoundMethod(bound_method) => Some((
FunctionKind::BoundMethod,
bound_method.function(context.db()),
)),
Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderGet(
function,
)) => Some((FunctionKind::MethodWrapper, function)),
_ => None,
};
// If there is a single matching overload, the diagnostics should be reported
// directly for that overload.
if let Some(matching_overload_index) = self.matching_overload_index {
let callable_description =
CallableDescription::new(context.db(), self.signature_type);
let matching_overload =
function_type_and_kind.map(|(kind, function)| MatchingOverloadLiteral {
index: matching_overload_index,
kind,
function,
});
self.overloads[matching_overload_index].report_diagnostics(
context,
node,
self.signature_type,
callable_description.as_ref(),
union_diag,
matching_overload.as_ref(),
);
return;
}

Another approach could be to expand the DisplaySettings to include the matching_overload_index: MatchingOverloadIndex which can then be used directly to filter the overloads at the display level. The None variant would display all the overloads.

.collect::<Vec<_>>();

// 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.
pub fn definitions_for_bin_op<'db>(
db: &'db dyn Db,
Expand Down
Loading