Skip to content

Commit ec55842

Browse files
authored
[ty] Make initializer calls GotoTargets (#20014)
This introduces `GotoTarget::Call` that represents the kind of ambiguous/overloaded click of a callable-being-called: ```py x = mymodule.MyClass(1, 2) ^^^^^^^ ``` This is equivalent to `GotoTarget::Expression` for the same span but enriched with information about the actual callable implementation. That is, if you click on `MyClass` in `MyClass()` it is *both* a reference to the class and to the initializer of the class. Therefore it would be ideal for goto-* and docstrings to be some intelligent merging of both the class and the initializer. In particular the callable-implementation (initializer) is prioritized over the callable-itself (class) so when showing docstrings we will preferentially show the docs of the initializer if it exists, and then fallback to the docs of the class. For goto-definition/goto-declaration we will yield both the class and the initializer, requiring you to pick which you want (this is perhaps needlessly pedantic but...). Fixes astral-sh/ty#898 Fixes astral-sh/ty#1010
1 parent d5e48a0 commit ec55842

File tree

4 files changed

+261
-19
lines changed

4 files changed

+261
-19
lines changed

crates/ty_ide/src/goto.rs

Lines changed: 107 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ use std::borrow::Cow;
88
use crate::find_node::covering_node;
99
use crate::stub_mapping::StubMapper;
1010
use ruff_db::parsed::ParsedModuleRef;
11+
use ruff_python_ast::ExprCall;
1112
use ruff_python_ast::{self as ast, AnyNodeRef};
1213
use ruff_python_parser::TokenKind;
1314
use ruff_text_size::{Ranged, TextRange, TextSize};
1415
use ty_python_semantic::HasDefinition;
1516
use ty_python_semantic::ImportAliasResolution;
1617
use ty_python_semantic::ResolvedDefinition;
17-
use ty_python_semantic::types::Type;
1818
use ty_python_semantic::types::definitions_for_keyword_argument;
19+
use ty_python_semantic::types::{Type, call_signature_details};
1920
use ty_python_semantic::{
2021
HasType, SemanticModel, definitions_for_imported_symbol, definitions_for_name,
2122
};
@@ -145,6 +146,26 @@ pub(crate) enum GotoTarget<'a> {
145146
Globals {
146147
identifier: &'a ast::Identifier,
147148
},
149+
/// Go to on the invocation of a callable
150+
///
151+
/// ```py
152+
/// x = mymodule.MyClass(1, 2)
153+
/// ^^^^^^^
154+
/// ```
155+
///
156+
/// This is equivalent to `GotoTarget::Expression(callable)` but enriched
157+
/// with information about the actual callable implementation.
158+
///
159+
/// That is, if you click on `MyClass` in `MyClass()` it is *both* a
160+
/// reference to the class and to the initializer of the class. Therefore
161+
/// it would be ideal for goto-* and docstrings to be some intelligent
162+
/// merging of both the class and the initializer.
163+
Call {
164+
/// The callable that can actually be selected by a cursor
165+
callable: ast::ExprRef<'a>,
166+
/// The call of the callable
167+
call: &'a ExprCall,
168+
},
148169
}
149170

150171
/// The resolved definitions for a `GotoTarget`
@@ -258,6 +279,9 @@ impl GotoTarget<'_> {
258279
GotoTarget::ImportModuleAlias { alias } => alias.inferred_type(model),
259280
GotoTarget::ExceptVariable(except) => except.inferred_type(model),
260281
GotoTarget::KeywordArgument { keyword, .. } => keyword.value.inferred_type(model),
282+
// When asking the type of a callable, usually you want the callable itself?
283+
// (i.e. the type of `MyClass` in `MyClass()` is `<class MyClass>` and not `() -> MyClass`)
284+
GotoTarget::Call { callable, .. } => callable.inferred_type(model),
261285
// TODO: Support identifier targets
262286
GotoTarget::PatternMatchRest(_)
263287
| GotoTarget::PatternKeywordArgument(_)
@@ -293,18 +317,10 @@ impl GotoTarget<'_> {
293317
alias_resolution: ImportAliasResolution,
294318
) -> Option<DefinitionsOrTargets<'db>> {
295319
use crate::NavigationTarget;
296-
use ruff_python_ast as ast;
297320

298321
match self {
299-
GotoTarget::Expression(expression) => match expression {
300-
ast::ExprRef::Name(name) => Some(DefinitionsOrTargets::Definitions(
301-
definitions_for_name(db, file, name),
302-
)),
303-
ast::ExprRef::Attribute(attribute) => Some(DefinitionsOrTargets::Definitions(
304-
ty_python_semantic::definitions_for_attribute(db, file, attribute),
305-
)),
306-
_ => None,
307-
},
322+
GotoTarget::Expression(expression) => definitions_for_expression(db, file, expression)
323+
.map(DefinitionsOrTargets::Definitions),
308324

309325
// For already-defined symbols, they are their own definitions
310326
GotoTarget::FunctionDef(function) => {
@@ -417,6 +433,22 @@ impl GotoTarget<'_> {
417433
}
418434
}
419435

436+
// For callables, both the definition of the callable and the actual function impl are relevant.
437+
//
438+
// Prefer the function impl over the callable so that its docstrings win if defined.
439+
GotoTarget::Call { callable, call } => {
440+
let mut definitions = definitions_for_callable(db, file, call);
441+
let expr_definitions =
442+
definitions_for_expression(db, file, callable).unwrap_or_default();
443+
definitions.extend(expr_definitions);
444+
445+
if definitions.is_empty() {
446+
None
447+
} else {
448+
Some(DefinitionsOrTargets::Definitions(definitions))
449+
}
450+
}
451+
420452
_ => None,
421453
}
422454
}
@@ -427,7 +459,11 @@ impl GotoTarget<'_> {
427459
/// to this goto target.
428460
pub(crate) fn to_string(&self) -> Option<Cow<'_, str>> {
429461
match self {
430-
GotoTarget::Expression(expression) => match expression {
462+
GotoTarget::Call {
463+
callable: expression,
464+
..
465+
}
466+
| GotoTarget::Expression(expression) => match expression {
431467
ast::ExprRef::Name(name) => Some(Cow::Borrowed(name.id.as_str())),
432468
ast::ExprRef::Attribute(attr) => Some(Cow::Borrowed(attr.attr.as_str())),
433469
_ => None,
@@ -627,7 +663,18 @@ impl GotoTarget<'_> {
627663
Some(GotoTarget::TypeParamTypeVarTupleName(var_tuple))
628664
}
629665
Some(AnyNodeRef::ExprAttribute(attribute)) => {
630-
Some(GotoTarget::Expression(attribute.into()))
666+
// Check if this is seemingly a callable being invoked (the `y` in `x.y(...)`)
667+
let grandparent_expr = covering_node.ancestors().nth(2);
668+
let attribute_expr = attribute.into();
669+
if let Some(AnyNodeRef::ExprCall(call)) = grandparent_expr {
670+
if ruff_python_ast::ExprRef::from(&call.func) == attribute_expr {
671+
return Some(GotoTarget::Call {
672+
call,
673+
callable: attribute_expr,
674+
});
675+
}
676+
}
677+
Some(GotoTarget::Expression(attribute_expr))
631678
}
632679
Some(AnyNodeRef::StmtNonlocal(_)) => Some(GotoTarget::NonLocal { identifier }),
633680
Some(AnyNodeRef::StmtGlobal(_)) => Some(GotoTarget::Globals { identifier }),
@@ -641,15 +688,31 @@ impl GotoTarget<'_> {
641688
}
642689
},
643690

644-
node => node.as_expr_ref().map(GotoTarget::Expression),
691+
node => {
692+
// Check if this is seemingly a callable being invoked (the `x` in `x(...)`)
693+
let parent = covering_node.parent();
694+
if let (Some(AnyNodeRef::ExprCall(call)), AnyNodeRef::ExprName(name)) =
695+
(parent, node)
696+
{
697+
return Some(GotoTarget::Call {
698+
call,
699+
callable: name.into(),
700+
});
701+
}
702+
node.as_expr_ref().map(GotoTarget::Expression)
703+
}
645704
}
646705
}
647706
}
648707

649708
impl Ranged for GotoTarget<'_> {
650709
fn range(&self) -> TextRange {
651710
match self {
652-
GotoTarget::Expression(expression) => match expression {
711+
GotoTarget::Call {
712+
callable: expression,
713+
..
714+
}
715+
| GotoTarget::Expression(expression) => match expression {
653716
ast::ExprRef::Attribute(attribute) => attribute.attr.range,
654717
_ => expression.range(),
655718
},
@@ -711,6 +774,35 @@ fn convert_resolved_definitions_to_targets(
711774
.collect()
712775
}
713776

777+
/// Shared helper to get definitions for an expr (that is presumably a name/attr)
778+
fn definitions_for_expression<'db>(
779+
db: &'db dyn crate::Db,
780+
file: ruff_db::files::File,
781+
expression: &ruff_python_ast::ExprRef<'_>,
782+
) -> Option<Vec<ResolvedDefinition<'db>>> {
783+
match expression {
784+
ast::ExprRef::Name(name) => Some(definitions_for_name(db, file, name)),
785+
ast::ExprRef::Attribute(attribute) => Some(ty_python_semantic::definitions_for_attribute(
786+
db, file, attribute,
787+
)),
788+
_ => None,
789+
}
790+
}
791+
792+
fn definitions_for_callable<'db>(
793+
db: &'db dyn crate::Db,
794+
file: ruff_db::files::File,
795+
call: &ExprCall,
796+
) -> Vec<ResolvedDefinition<'db>> {
797+
let model = SemanticModel::new(db, file);
798+
// Attempt to refine to a specific call
799+
let signature_info = call_signature_details(db, &model, call);
800+
signature_info
801+
.into_iter()
802+
.filter_map(|signature| signature.definition.map(ResolvedDefinition::Definition))
803+
.collect()
804+
}
805+
714806
/// Shared helper to map and convert resolved definitions into navigation targets.
715807
fn definitions_to_navigation_targets<'db>(
716808
db: &dyn crate::Db,

crates/ty_ide/src/goto_declaration.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,23 @@ mod tests {
123123
|
124124
4 | pass
125125
5 |
126+
6 | instance = MyClass()
127+
| ^^^^^^^
128+
|
129+
130+
info[goto-declaration]: Declaration
131+
--> main.py:3:9
132+
|
133+
2 | class MyClass:
134+
3 | def __init__(self):
135+
| ^^^^^^^^
136+
4 | pass
137+
|
138+
info: Source
139+
--> main.py:6:12
140+
|
141+
4 | pass
142+
5 |
126143
6 | instance = MyClass()
127144
| ^^^^^^^
128145
|

crates/ty_ide/src/goto_definition.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,22 @@ class MyOtherClass:
466466
--> main.py:3:5
467467
|
468468
2 | from mymodule import MyClass
469+
3 | x = MyClass(0)
470+
| ^^^^^^^
471+
|
472+
473+
info[goto-definition]: Definition
474+
--> mymodule.py:3:9
475+
|
476+
2 | class MyClass:
477+
3 | def __init__(self, val):
478+
| ^^^^^^^^
479+
4 | self.val = val
480+
|
481+
info: Source
482+
--> main.py:3:5
483+
|
484+
2 | from mymodule import MyClass
469485
3 | x = MyClass(0)
470486
| ^^^^^^^
471487
|

crates/ty_ide/src/hover.rs

Lines changed: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,123 @@ mod tests {
465465
"#,
466466
);
467467

468+
assert_snapshot!(test.hover(), @r"
469+
<class 'MyClass'>
470+
---------------------------------------------
471+
initializes MyClass (perfectly)
472+
473+
---------------------------------------------
474+
```python
475+
<class 'MyClass'>
476+
```
477+
---
478+
```text
479+
initializes MyClass (perfectly)
480+
481+
```
482+
---------------------------------------------
483+
info[hover]: Hovered content is
484+
--> main.py:24:5
485+
|
486+
22 | return 0
487+
23 |
488+
24 | x = MyClass(0)
489+
| ^^^^^-^
490+
| | |
491+
| | Cursor offset
492+
| source
493+
|
494+
");
495+
}
496+
497+
#[test]
498+
fn hover_class_init_attr() {
499+
let test = CursorTest::builder()
500+
.source(
501+
"mymod.py",
502+
r#"
503+
class MyClass:
504+
'''
505+
This is such a great class!!
506+
507+
Don't you know?
508+
509+
Everyone loves my class!!
510+
511+
'''
512+
def __init__(self, val):
513+
"""initializes MyClass (perfectly)"""
514+
self.val = val
515+
"#,
516+
)
517+
.source(
518+
"main.py",
519+
r#"
520+
import mymod
521+
522+
x = mymod.MyCla<CURSOR>ss(0)
523+
"#,
524+
)
525+
.build();
526+
527+
assert_snapshot!(test.hover(), @r"
528+
<class 'MyClass'>
529+
---------------------------------------------
530+
initializes MyClass (perfectly)
531+
532+
---------------------------------------------
533+
```python
534+
<class 'MyClass'>
535+
```
536+
---
537+
```text
538+
initializes MyClass (perfectly)
539+
540+
```
541+
---------------------------------------------
542+
info[hover]: Hovered content is
543+
--> main.py:4:11
544+
|
545+
2 | import mymod
546+
3 |
547+
4 | x = mymod.MyClass(0)
548+
| ^^^^^-^
549+
| | |
550+
| | Cursor offset
551+
| source
552+
|
553+
");
554+
}
555+
556+
#[test]
557+
fn hover_class_init_no_init_docs() {
558+
let test = cursor_test(
559+
r#"
560+
class MyClass:
561+
'''
562+
This is such a great class!!
563+
564+
Don't you know?
565+
566+
Everyone loves my class!!
567+
568+
'''
569+
def __init__(self, val):
570+
self.val = val
571+
572+
def my_method(self, a, b):
573+
'''This is such a great func!!
574+
575+
Args:
576+
a: first for a reason
577+
b: coming for `a`'s title
578+
'''
579+
return 0
580+
581+
x = MyCla<CURSOR>ss(0)
582+
"#,
583+
);
584+
468585
assert_snapshot!(test.hover(), @r"
469586
<class 'MyClass'>
470587
---------------------------------------------
@@ -489,11 +606,11 @@ mod tests {
489606
```
490607
---------------------------------------------
491608
info[hover]: Hovered content is
492-
--> main.py:24:5
609+
--> main.py:23:5
493610
|
494-
22 | return 0
495-
23 |
496-
24 | x = MyClass(0)
611+
21 | return 0
612+
22 |
613+
23 | x = MyClass(0)
497614
| ^^^^^-^
498615
| | |
499616
| | Cursor offset

0 commit comments

Comments
 (0)