diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 52af38896b..bb085e3c3c 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -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, }; @@ -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` @@ -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 `` and not `() -> MyClass`) + GotoTarget::Call { callable, .. } => callable.inferred_type(model), // TODO: Support identifier targets GotoTarget::PatternMatchRest(_) | GotoTarget::PatternKeywordArgument(_) @@ -293,18 +317,10 @@ impl GotoTarget<'_> { alias_resolution: ImportAliasResolution, ) -> Option> { 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) => { @@ -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, } } @@ -427,7 +459,11 @@ impl GotoTarget<'_> { /// to this goto target. pub(crate) fn to_string(&self) -> Option> { 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, @@ -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 }), @@ -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<'_> { 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(), }, @@ -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>> { + 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> { + 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, diff --git a/crates/ty_ide/src/goto_declaration.rs b/crates/ty_ide/src/goto_declaration.rs index 2e25862f59..bd67246b03 100644 --- a/crates/ty_ide/src/goto_declaration.rs +++ b/crates/ty_ide/src/goto_declaration.rs @@ -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() | ^^^^^^^ | diff --git a/crates/ty_ide/src/goto_definition.rs b/crates/ty_ide/src/goto_definition.rs index 55156488fc..fb165f61a0 100644 --- a/crates/ty_ide/src/goto_definition.rs +++ b/crates/ty_ide/src/goto_definition.rs @@ -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) | ^^^^^^^ | diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index fbfc57041e..0867983e3a 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -465,6 +465,123 @@ mod tests { "#, ); + assert_snapshot!(test.hover(), @r" + + --------------------------------------------- + initializes MyClass (perfectly) + + --------------------------------------------- + ```python + + ``` + --- + ```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.MyClass(0) + "#, + ) + .build(); + + assert_snapshot!(test.hover(), @r" + + --------------------------------------------- + initializes MyClass (perfectly) + + --------------------------------------------- + ```python + + ``` + --- + ```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 = MyClass(0) + "#, + ); + assert_snapshot!(test.hover(), @r" --------------------------------------------- @@ -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