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
122 changes: 107 additions & 15 deletions crates/ty_ide/src/goto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ use std::borrow::Cow;
use crate::find_node::covering_node;
use crate::stub_mapping::StubMapper;
use ruff_db::parsed::ParsedModuleRef;
use ruff_python_ast::ExprCall;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_parser::TokenKind;
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::HasDefinition;
use ty_python_semantic::ImportAliasResolution;
use ty_python_semantic::ResolvedDefinition;
use ty_python_semantic::types::Type;
use ty_python_semantic::types::definitions_for_keyword_argument;
use ty_python_semantic::types::{Type, call_signature_details};
use ty_python_semantic::{
HasType, SemanticModel, definitions_for_imported_symbol, definitions_for_name,
};
Expand Down Expand Up @@ -145,6 +146,26 @@ pub(crate) enum GotoTarget<'a> {
Globals {
identifier: &'a ast::Identifier,
},
/// Go to on the invocation of a callable
///
/// ```py
/// x = mymodule.MyClass(1, 2)
/// ^^^^^^^
/// ```
///
/// This is equivalent to `GotoTarget::Expression(callable)` 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.
Call {
/// The callable that can actually be selected by a cursor
callable: ast::ExprRef<'a>,
/// The call of the callable
call: &'a ExprCall,
},
}

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

match self {
GotoTarget::Expression(expression) => match expression {
ast::ExprRef::Name(name) => Some(DefinitionsOrTargets::Definitions(
definitions_for_name(db, file, name),
)),
ast::ExprRef::Attribute(attribute) => Some(DefinitionsOrTargets::Definitions(
ty_python_semantic::definitions_for_attribute(db, file, attribute),
)),
_ => None,
},
GotoTarget::Expression(expression) => definitions_for_expression(db, file, expression)
.map(DefinitionsOrTargets::Definitions),

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

// For callables, both the definition of the callable and the actual function impl are relevant.
//
// Prefer the function impl over the callable so that its docstrings win if defined.
GotoTarget::Call { callable, call } => {
let mut definitions = definitions_for_callable(db, file, call);
let expr_definitions =
definitions_for_expression(db, file, callable).unwrap_or_default();
definitions.extend(expr_definitions);

if definitions.is_empty() {
None
} else {
Some(DefinitionsOrTargets::Definitions(definitions))
}
}

_ => None,
}
}
Expand All @@ -427,7 +459,11 @@ impl GotoTarget<'_> {
/// to this goto target.
pub(crate) fn to_string(&self) -> Option<Cow<'_, str>> {
match self {
GotoTarget::Expression(expression) => match expression {
GotoTarget::Call {
callable: expression,
..
}
| GotoTarget::Expression(expression) => match expression {
ast::ExprRef::Name(name) => Some(Cow::Borrowed(name.id.as_str())),
ast::ExprRef::Attribute(attr) => Some(Cow::Borrowed(attr.attr.as_str())),
_ => None,
Expand Down Expand Up @@ -627,7 +663,18 @@ impl GotoTarget<'_> {
Some(GotoTarget::TypeParamTypeVarTupleName(var_tuple))
}
Some(AnyNodeRef::ExprAttribute(attribute)) => {
Some(GotoTarget::Expression(attribute.into()))
// Check if this is seemingly a callable being invoked (the `y` in `x.y(...)`)
let grandparent_expr = covering_node.ancestors().nth(2);
let attribute_expr = attribute.into();
if let Some(AnyNodeRef::ExprCall(call)) = grandparent_expr {
if ruff_python_ast::ExprRef::from(&call.func) == attribute_expr {
return Some(GotoTarget::Call {
call,
callable: attribute_expr,
});
}
}
Some(GotoTarget::Expression(attribute_expr))
}
Some(AnyNodeRef::StmtNonlocal(_)) => Some(GotoTarget::NonLocal { identifier }),
Some(AnyNodeRef::StmtGlobal(_)) => Some(GotoTarget::Globals { identifier }),
Expand All @@ -641,15 +688,31 @@ impl GotoTarget<'_> {
}
},

node => node.as_expr_ref().map(GotoTarget::Expression),
node => {
// Check if this is seemingly a callable being invoked (the `x` in `x(...)`)
let parent = covering_node.parent();
if let (Some(AnyNodeRef::ExprCall(call)), AnyNodeRef::ExprName(name)) =
(parent, node)
{
return Some(GotoTarget::Call {
call,
callable: name.into(),
});
}
node.as_expr_ref().map(GotoTarget::Expression)
}
}
}
}

impl Ranged for GotoTarget<'_> {
fn range(&self) -> TextRange {
match self {
GotoTarget::Expression(expression) => match expression {
GotoTarget::Call {
callable: expression,
..
}
| GotoTarget::Expression(expression) => match expression {
ast::ExprRef::Attribute(attribute) => attribute.attr.range,
_ => expression.range(),
},
Expand Down Expand Up @@ -711,6 +774,35 @@ fn convert_resolved_definitions_to_targets(
.collect()
}

/// Shared helper to get definitions for an expr (that is presumably a name/attr)
fn definitions_for_expression<'db>(
db: &'db dyn crate::Db,
file: ruff_db::files::File,
expression: &ruff_python_ast::ExprRef<'_>,
) -> Option<Vec<ResolvedDefinition<'db>>> {
match expression {
ast::ExprRef::Name(name) => Some(definitions_for_name(db, file, name)),
ast::ExprRef::Attribute(attribute) => Some(ty_python_semantic::definitions_for_attribute(
db, file, attribute,
)),
_ => None,
}
}

fn definitions_for_callable<'db>(
db: &'db dyn crate::Db,
file: ruff_db::files::File,
call: &ExprCall,
) -> Vec<ResolvedDefinition<'db>> {
let model = SemanticModel::new(db, file);
// Attempt to refine to a specific call
let signature_info = call_signature_details(db, &model, call);
signature_info
.into_iter()
.filter_map(|signature| signature.definition.map(ResolvedDefinition::Definition))
.collect()
}

/// Shared helper to map and convert resolved definitions into navigation targets.
fn definitions_to_navigation_targets<'db>(
db: &dyn crate::Db,
Expand Down
17 changes: 17 additions & 0 deletions crates/ty_ide/src/goto_declaration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,23 @@ mod tests {
|
4 | pass
5 |
6 | instance = MyClass()
| ^^^^^^^
|

info[goto-declaration]: Declaration
--> main.py:3:9
|
2 | class MyClass:
3 | def __init__(self):
| ^^^^^^^^
4 | pass
|
info: Source
--> main.py:6:12
|
4 | pass
5 |
6 | instance = MyClass()
| ^^^^^^^
|
Expand Down
16 changes: 16 additions & 0 deletions crates/ty_ide/src/goto_definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,22 @@ class MyOtherClass:
--> main.py:3:5
|
2 | from mymodule import MyClass
3 | x = MyClass(0)
| ^^^^^^^
|

info[goto-definition]: Definition
--> mymodule.py:3:9
|
2 | class MyClass:
3 | def __init__(self, val):
| ^^^^^^^^
4 | self.val = val
|
info: Source
--> main.py:3:5
|
2 | from mymodule import MyClass
3 | x = MyClass(0)
| ^^^^^^^
|
Expand Down
125 changes: 121 additions & 4 deletions crates/ty_ide/src/hover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,123 @@ mod tests {
"#,
);

assert_snapshot!(test.hover(), @r"
<class 'MyClass'>
---------------------------------------------
initializes MyClass (perfectly)

---------------------------------------------
```python
<class 'MyClass'>
```
---
```text
initializes MyClass (perfectly)

```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:24:5
|
22 | return 0
23 |
24 | x = MyClass(0)
| ^^^^^-^
| | |
| | Cursor offset
| source
|
");
}

#[test]
fn hover_class_init_attr() {
let test = CursorTest::builder()
.source(
"mymod.py",
r#"
class MyClass:
'''
This is such a great class!!

Don't you know?

Everyone loves my class!!

'''
def __init__(self, val):
"""initializes MyClass (perfectly)"""
self.val = val
"#,
)
.source(
"main.py",
r#"
import mymod

x = mymod.MyCla<CURSOR>ss(0)
"#,
)
.build();

assert_snapshot!(test.hover(), @r"
<class 'MyClass'>
---------------------------------------------
initializes MyClass (perfectly)

---------------------------------------------
```python
<class 'MyClass'>
```
---
```text
initializes MyClass (perfectly)

```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:11
|
2 | import mymod
3 |
4 | x = mymod.MyClass(0)
| ^^^^^-^
| | |
| | Cursor offset
| source
|
");
}

#[test]
fn hover_class_init_no_init_docs() {
let test = cursor_test(
r#"
class MyClass:
'''
This is such a great class!!

Don't you know?

Everyone loves my class!!

'''
def __init__(self, val):
self.val = val

def my_method(self, a, b):
'''This is such a great func!!

Args:
a: first for a reason
b: coming for `a`'s title
'''
return 0

x = MyCla<CURSOR>ss(0)
"#,
);

assert_snapshot!(test.hover(), @r"
<class 'MyClass'>
---------------------------------------------
Expand All @@ -489,11 +606,11 @@ mod tests {
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:24:5
--> main.py:23:5
|
22 | return 0
23 |
24 | x = MyClass(0)
21 | return 0
22 |
23 | x = MyClass(0)
| ^^^^^-^
| | |
| | Cursor offset
Expand Down
Loading