mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
[ty] Make initializer calls GotoTargets (#20014)
Some checks are pending
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / python package (push) Waiting to run
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / python package (push) Waiting to run
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
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 https://github.com/astral-sh/ty/issues/898 Fixes https://github.com/astral-sh/ty/issues/1010
This commit is contained in:
parent
d5e48a0f80
commit
ec5584219e
4 changed files with 261 additions and 19 deletions
|
@ -8,14 +8,15 @@ use std::borrow::Cow;
|
||||||
use crate::find_node::covering_node;
|
use crate::find_node::covering_node;
|
||||||
use crate::stub_mapping::StubMapper;
|
use crate::stub_mapping::StubMapper;
|
||||||
use ruff_db::parsed::ParsedModuleRef;
|
use ruff_db::parsed::ParsedModuleRef;
|
||||||
|
use ruff_python_ast::ExprCall;
|
||||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||||
use ruff_python_parser::TokenKind;
|
use ruff_python_parser::TokenKind;
|
||||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||||
use ty_python_semantic::HasDefinition;
|
use ty_python_semantic::HasDefinition;
|
||||||
use ty_python_semantic::ImportAliasResolution;
|
use ty_python_semantic::ImportAliasResolution;
|
||||||
use ty_python_semantic::ResolvedDefinition;
|
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::definitions_for_keyword_argument;
|
||||||
|
use ty_python_semantic::types::{Type, call_signature_details};
|
||||||
use ty_python_semantic::{
|
use ty_python_semantic::{
|
||||||
HasType, SemanticModel, definitions_for_imported_symbol, definitions_for_name,
|
HasType, SemanticModel, definitions_for_imported_symbol, definitions_for_name,
|
||||||
};
|
};
|
||||||
|
@ -145,6 +146,26 @@ pub(crate) enum GotoTarget<'a> {
|
||||||
Globals {
|
Globals {
|
||||||
identifier: &'a ast::Identifier,
|
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`
|
/// The resolved definitions for a `GotoTarget`
|
||||||
|
@ -258,6 +279,9 @@ impl GotoTarget<'_> {
|
||||||
GotoTarget::ImportModuleAlias { alias } => alias.inferred_type(model),
|
GotoTarget::ImportModuleAlias { alias } => alias.inferred_type(model),
|
||||||
GotoTarget::ExceptVariable(except) => except.inferred_type(model),
|
GotoTarget::ExceptVariable(except) => except.inferred_type(model),
|
||||||
GotoTarget::KeywordArgument { keyword, .. } => keyword.value.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
|
// TODO: Support identifier targets
|
||||||
GotoTarget::PatternMatchRest(_)
|
GotoTarget::PatternMatchRest(_)
|
||||||
| GotoTarget::PatternKeywordArgument(_)
|
| GotoTarget::PatternKeywordArgument(_)
|
||||||
|
@ -293,18 +317,10 @@ impl GotoTarget<'_> {
|
||||||
alias_resolution: ImportAliasResolution,
|
alias_resolution: ImportAliasResolution,
|
||||||
) -> Option<DefinitionsOrTargets<'db>> {
|
) -> Option<DefinitionsOrTargets<'db>> {
|
||||||
use crate::NavigationTarget;
|
use crate::NavigationTarget;
|
||||||
use ruff_python_ast as ast;
|
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
GotoTarget::Expression(expression) => match expression {
|
GotoTarget::Expression(expression) => definitions_for_expression(db, file, expression)
|
||||||
ast::ExprRef::Name(name) => Some(DefinitionsOrTargets::Definitions(
|
.map(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,
|
|
||||||
},
|
|
||||||
|
|
||||||
// For already-defined symbols, they are their own definitions
|
// For already-defined symbols, they are their own definitions
|
||||||
GotoTarget::FunctionDef(function) => {
|
GotoTarget::FunctionDef(function) => {
|
||||||
|
@ -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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -427,7 +459,11 @@ impl GotoTarget<'_> {
|
||||||
/// to this goto target.
|
/// to this goto target.
|
||||||
pub(crate) fn to_string(&self) -> Option<Cow<'_, str>> {
|
pub(crate) fn to_string(&self) -> Option<Cow<'_, str>> {
|
||||||
match self {
|
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::Name(name) => Some(Cow::Borrowed(name.id.as_str())),
|
||||||
ast::ExprRef::Attribute(attr) => Some(Cow::Borrowed(attr.attr.as_str())),
|
ast::ExprRef::Attribute(attr) => Some(Cow::Borrowed(attr.attr.as_str())),
|
||||||
_ => None,
|
_ => None,
|
||||||
|
@ -627,7 +663,18 @@ impl GotoTarget<'_> {
|
||||||
Some(GotoTarget::TypeParamTypeVarTupleName(var_tuple))
|
Some(GotoTarget::TypeParamTypeVarTupleName(var_tuple))
|
||||||
}
|
}
|
||||||
Some(AnyNodeRef::ExprAttribute(attribute)) => {
|
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::StmtNonlocal(_)) => Some(GotoTarget::NonLocal { identifier }),
|
||||||
Some(AnyNodeRef::StmtGlobal(_)) => Some(GotoTarget::Globals { identifier }),
|
Some(AnyNodeRef::StmtGlobal(_)) => Some(GotoTarget::Globals { identifier }),
|
||||||
|
@ -641,7 +688,19 @@ 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -649,7 +708,11 @@ impl GotoTarget<'_> {
|
||||||
impl Ranged for GotoTarget<'_> {
|
impl Ranged for GotoTarget<'_> {
|
||||||
fn range(&self) -> TextRange {
|
fn range(&self) -> TextRange {
|
||||||
match self {
|
match self {
|
||||||
GotoTarget::Expression(expression) => match expression {
|
GotoTarget::Call {
|
||||||
|
callable: expression,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| GotoTarget::Expression(expression) => match expression {
|
||||||
ast::ExprRef::Attribute(attribute) => attribute.attr.range,
|
ast::ExprRef::Attribute(attribute) => attribute.attr.range,
|
||||||
_ => expression.range(),
|
_ => expression.range(),
|
||||||
},
|
},
|
||||||
|
@ -711,6 +774,35 @@ fn convert_resolved_definitions_to_targets(
|
||||||
.collect()
|
.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.
|
/// Shared helper to map and convert resolved definitions into navigation targets.
|
||||||
fn definitions_to_navigation_targets<'db>(
|
fn definitions_to_navigation_targets<'db>(
|
||||||
db: &dyn crate::Db,
|
db: &dyn crate::Db,
|
||||||
|
|
|
@ -123,6 +123,23 @@ mod tests {
|
||||||
|
|
|
|
||||||
4 | pass
|
4 | pass
|
||||||
5 |
|
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()
|
6 | instance = MyClass()
|
||||||
| ^^^^^^^
|
| ^^^^^^^
|
||||||
|
|
|
|
||||||
|
|
|
@ -466,6 +466,22 @@ class MyOtherClass:
|
||||||
--> main.py:3:5
|
--> main.py:3:5
|
||||||
|
|
|
|
||||||
2 | from mymodule import MyClass
|
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)
|
3 | x = MyClass(0)
|
||||||
| ^^^^^^^
|
| ^^^^^^^
|
||||||
|
|
|
|
||||||
|
|
|
@ -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"
|
assert_snapshot!(test.hover(), @r"
|
||||||
<class 'MyClass'>
|
<class 'MyClass'>
|
||||||
---------------------------------------------
|
---------------------------------------------
|
||||||
|
@ -489,11 +606,11 @@ mod tests {
|
||||||
```
|
```
|
||||||
---------------------------------------------
|
---------------------------------------------
|
||||||
info[hover]: Hovered content is
|
info[hover]: Hovered content is
|
||||||
--> main.py:24:5
|
--> main.py:23:5
|
||||||
|
|
|
|
||||||
22 | return 0
|
21 | return 0
|
||||||
23 |
|
22 |
|
||||||
24 | x = MyClass(0)
|
23 | x = MyClass(0)
|
||||||
| ^^^^^-^
|
| ^^^^^-^
|
||||||
| | |
|
| | |
|
||||||
| | Cursor offset
|
| | Cursor offset
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue