diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index f5b87bcdd7..ab589ba5c5 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -331,15 +331,6 @@ impl GotoTarget<'_> { let module = import_name(module_name, *component_index); model.resolve_module_type(Some(module), *level)? } - // TODO: Support identifier targets - GotoTarget::PatternMatchRest(_) - | GotoTarget::PatternKeywordArgument(_) - | GotoTarget::PatternMatchStarName(_) - | GotoTarget::PatternMatchAsName(_) - | GotoTarget::TypeParamParamSpecName(_) - | GotoTarget::TypeParamTypeVarTupleName(_) - | GotoTarget::NonLocal { .. } - | GotoTarget::Globals { .. } => return None, GotoTarget::StringAnnotationSubexpr { string_expr, subrange, @@ -366,6 +357,15 @@ impl GotoTarget<'_> { let (_, ty) = ty_python_semantic::definitions_for_unary_op(model, expression)?; ty } + // TODO: Support identifier targets + GotoTarget::PatternMatchRest(_) + | GotoTarget::PatternKeywordArgument(_) + | GotoTarget::PatternMatchStarName(_) + | GotoTarget::PatternMatchAsName(_) + | GotoTarget::TypeParamParamSpecName(_) + | GotoTarget::TypeParamTypeVarTupleName(_) + | GotoTarget::NonLocal { .. } + | GotoTarget::Globals { .. } => return None, }; Some(ty) @@ -402,9 +402,8 @@ impl GotoTarget<'_> { ) -> Option> { use crate::NavigationTarget; match self { - GotoTarget::Expression(expression) => { - definitions_for_expression(model, expression).map(DefinitionsOrTargets::Definitions) - } + GotoTarget::Expression(expression) => definitions_for_expression(model, *expression) + .map(DefinitionsOrTargets::Definitions), // For already-defined symbols, they are their own definitions GotoTarget::FunctionDef(function) => Some(DefinitionsOrTargets::Definitions(vec![ ResolvedDefinition::Definition(function.definition(model)), @@ -511,7 +510,7 @@ impl GotoTarget<'_> { GotoTarget::Call { callable, call } => { let mut definitions = definitions_for_callable(model, call); let expr_definitions = - definitions_for_expression(model, callable).unwrap_or_default(); + definitions_for_expression(model, *callable).unwrap_or_default(); definitions.extend(expr_definitions); if definitions.is_empty() { @@ -545,18 +544,23 @@ impl GotoTarget<'_> { let subexpr = covering_node(subast.syntax().into(), *subrange) .node() .as_expr_ref()?; - definitions_for_expression(&submodel, &subexpr) + definitions_for_expression(&submodel, subexpr) .map(DefinitionsOrTargets::Definitions) } + GotoTarget::NonLocal { identifier } | GotoTarget::Globals { identifier } => { + Some(DefinitionsOrTargets::Definitions(definitions_for_name( + model, + identifier.as_str(), + AnyNodeRef::Identifier(identifier), + ))) + } // TODO: implement these GotoTarget::PatternKeywordArgument(..) | GotoTarget::PatternMatchStarName(..) | GotoTarget::TypeParamTypeVarName(..) | GotoTarget::TypeParamParamSpecName(..) - | GotoTarget::TypeParamTypeVarTupleName(..) - | GotoTarget::NonLocal { .. } - | GotoTarget::Globals { .. } => None, + | GotoTarget::TypeParamTypeVarTupleName(..) => None, } } @@ -916,9 +920,9 @@ impl Ranged for GotoTarget<'_> { } /// Converts a collection of `ResolvedDefinition` items into `NavigationTarget` items. -fn convert_resolved_definitions_to_targets( - db: &dyn ty_python_semantic::Db, - definitions: Vec>, +fn convert_resolved_definitions_to_targets<'db>( + db: &'db dyn ty_python_semantic::Db, + definitions: Vec>, ) -> Vec { definitions .into_iter() @@ -953,10 +957,14 @@ fn convert_resolved_definitions_to_targets( /// Shared helper to get definitions for an expr (that is presumably a name/attr) fn definitions_for_expression<'db>( model: &SemanticModel<'db>, - expression: &ruff_python_ast::ExprRef<'_>, + expression: ruff_python_ast::ExprRef<'_>, ) -> Option>> { match expression { - ast::ExprRef::Name(name) => Some(definitions_for_name(model, name)), + ast::ExprRef::Name(name) => Some(definitions_for_name( + model, + name.id.as_str(), + expression.into(), + )), ast::ExprRef::Attribute(attribute) => Some(ty_python_semantic::definitions_for_attribute( model, attribute, )), diff --git a/crates/ty_ide/src/goto_declaration.rs b/crates/ty_ide/src/goto_declaration.rs index e598f86922..296a667f53 100644 --- a/crates/ty_ide/src/goto_declaration.rs +++ b/crates/ty_ide/src/goto_declaration.rs @@ -1254,6 +1254,45 @@ def outer(): "#); } + #[test] + fn goto_declaration_nonlocal_stmt() { + let test = cursor_test( + r#" +def outer(): + xy = "outer_value" + + def inner(): + nonlocal xy + xy = "modified" + return x # Should find the nonlocal x declaration in outer scope + + return inner +"#, + ); + + // Should find the variable declaration in the outer scope, not the nonlocal statement + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> main.py:3:5 + | + 2 | def outer(): + 3 | xy = "outer_value" + | ^^ + 4 | + 5 | def inner(): + | + info: Source + --> main.py:6:18 + | + 5 | def inner(): + 6 | nonlocal xy + | ^^ + 7 | xy = "modified" + 8 | return x # Should find the nonlocal x declaration in outer scope + | + "#); + } + #[test] fn goto_declaration_global_binding() { let test = cursor_test( @@ -1288,6 +1327,41 @@ def function(): "#); } + #[test] + fn goto_declaration_global_stmt() { + let test = cursor_test( + r#" +global_var = "global_value" + +def function(): + global global_var + global_var = "modified" + return global_var # Should find the global variable declaration +"#, + ); + + // Should find the global variable declaration, not the global statement + assert_snapshot!(test.goto_declaration(), @r#" + info[goto-declaration]: Declaration + --> main.py:2:1 + | + 2 | global_var = "global_value" + | ^^^^^^^^^^ + 3 | + 4 | def function(): + | + info: Source + --> main.py:5:12 + | + 4 | def function(): + 5 | global global_var + | ^^^^^^^^^^ + 6 | global_var = "modified" + 7 | return global_var # Should find the global variable declaration + | + "#); + } + #[test] fn goto_declaration_inherited_attribute() { let test = cursor_test( diff --git a/crates/ty_ide/src/goto_references.rs b/crates/ty_ide/src/goto_references.rs index aa3f2dba96..e8be7b35c3 100644 --- a/crates/ty_ide/src/goto_references.rs +++ b/crates/ty_ide/src/goto_references.rs @@ -149,7 +149,6 @@ result = calculate_sum(value=42) } #[test] - #[ignore] // TODO: Enable when nonlocal support is fully implemented in goto.rs fn test_nonlocal_variable_references() { let test = cursor_test( " @@ -183,7 +182,7 @@ def outer_function(): 2 | def outer_function(): 3 | counter = 0 | ^^^^^^^ - 4 | + 4 | 5 | def increment(): | @@ -214,7 +213,7 @@ def outer_function(): 7 | counter += 1 8 | return counter | ^^^^^^^ - 9 | + 9 | 10 | def decrement(): | @@ -245,7 +244,7 @@ def outer_function(): 12 | counter -= 1 13 | return counter | ^^^^^^^ - 14 | + 14 | 15 | # Use counter in outer scope | @@ -266,14 +265,13 @@ def outer_function(): 18 | decrement() 19 | final = counter | ^^^^^^^ - 20 | + 20 | 21 | return increment, decrement | "); } #[test] - #[ignore] // TODO: Enable when global support is fully implemented in goto.rs fn test_global_variable_references() { let test = cursor_test( " diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index deea1a4f60..3a8f80b643 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -1062,6 +1062,118 @@ f(**kwargs) "#); } + #[test] + fn goto_type_nonlocal_binding() { + let test = cursor_test( + r#" +def outer(): + x = "outer_value" + + def inner(): + nonlocal x + x = "modified" + return x # Should find the nonlocal x declaration in outer scope + + return inner +"#, + ); + + // Should find the variable declaration in the outer scope, not the nonlocal statement + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | ^^^ + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | + info: Source + --> main.py:8:16 + | + 6 | nonlocal x + 7 | x = "modified" + 8 | return x # Should find the nonlocal x declaration in outer scope + | ^ + 9 | + 10 | return inner + | + "#); + } + + #[test] + fn goto_type_nonlocal_stmt() { + let test = cursor_test( + r#" +def outer(): + xy = "outer_value" + + def inner(): + nonlocal xy + xy = "modified" + return x # Should find the nonlocal x declaration in outer scope + + return inner +"#, + ); + + // Should find the variable declaration in the outer scope, not the nonlocal statement + assert_snapshot!(test.goto_type_definition(), @"No goto target found"); + } + + #[test] + fn goto_type_global_binding() { + let test = cursor_test( + r#" +global_var = "global_value" + +def function(): + global global_var + global_var = "modified" + return global_var # Should find the global variable declaration +"#, + ); + + // Should find the global variable declaration, not the global statement + assert_snapshot!(test.goto_type_definition(), @r#" + info[goto-type-definition]: Type definition + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | ^^^ + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | + info: Source + --> main.py:7:12 + | + 5 | global global_var + 6 | global_var = "modified" + 7 | return global_var # Should find the global variable declaration + | ^^^^^^^^^^ + | + "#); + } + + #[test] + fn goto_type_global_stmt() { + let test = cursor_test( + r#" +global_var = "global_value" + +def function(): + global global_var + global_var = "modified" + return global_var # Should find the global variable declaration +"#, + ); + + // Should find the global variable declaration, not the global statement + assert_snapshot!(test.goto_type_definition(), @"No goto target found"); + } + #[test] fn goto_type_of_expression_with_builtin() { let test = cursor_test( diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 92dc695eed..2f96f7b061 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -1648,6 +1648,117 @@ def ab(a: int, *, c: int): "); } + #[test] + fn hover_nonlocal_binding() { + let test = cursor_test( + r#" +def outer(): + x = "outer_value" + + def inner(): + nonlocal x + x = "modified" + return x # Should find the nonlocal x declaration in outer scope + + return inner +"#, + ); + + // Should find the variable declaration in the outer scope, not the nonlocal statement + assert_snapshot!(test.hover(), @r#" + Literal["modified"] + --------------------------------------------- + ```python + Literal["modified"] + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:8:16 + | + 6 | nonlocal x + 7 | x = "modified" + 8 | return x # Should find the nonlocal x declaration in outer scope + | ^- Cursor offset + | | + | source + 9 | + 10 | return inner + | + "#); + } + + #[test] + fn hover_nonlocal_stmt() { + let test = cursor_test( + r#" +def outer(): + xy = "outer_value" + + def inner(): + nonlocal xy + xy = "modified" + return x # Should find the nonlocal x declaration in outer scope + + return inner +"#, + ); + + // Should find the variable declaration in the outer scope, not the nonlocal statement + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + + #[test] + fn hover_global_binding() { + let test = cursor_test( + r#" +global_var = "global_value" + +def function(): + global global_var + global_var = "modified" + return global_var # Should find the global variable declaration +"#, + ); + + // Should find the global variable declaration, not the global statement + assert_snapshot!(test.hover(), @r#" + Literal["modified"] + --------------------------------------------- + ```python + Literal["modified"] + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:7:12 + | + 5 | global global_var + 6 | global_var = "modified" + 7 | return global_var # Should find the global variable declaration + | ^^^^^^^-^^ + | | | + | | Cursor offset + | source + | + "#); + } + + #[test] + fn hover_global_stmt() { + let test = cursor_test( + r#" +global_var = "global_value" + +def function(): + global global_var + global_var = "modified" + return global_var # Should find the global variable declaration +"#, + ); + + // Should find the global variable declaration, not the global statement + assert_snapshot!(test.hover(), @"Hover provided no content"); + } + #[test] fn hover_module_import() { let mut test = cursor_test( diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 519600ed9d..d285a035fc 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -2255,6 +2255,8 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { names, }) => { for name in names { + self.scopes_by_expression + .record_expression(name, self.current_scope()); let symbol_id = self.add_symbol(name.id.clone()); let symbol = self.current_place_table().symbol(symbol_id); // Check whether the variable has already been accessed in this scope. @@ -2290,6 +2292,8 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { names, }) => { for name in names { + self.scopes_by_expression + .record_expression(name, self.current_scope()); let symbol_id = self.add_symbol(name.id.clone()); let symbol = self.current_place_table().symbol(symbol_id); // Check whether the variable has already been accessed in this scope. diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index b8bd3ba639..3f70a0d10d 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -22,7 +22,7 @@ use crate::{Db, DisplaySettings, HasType, NameKind, SemanticModel}; use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; -use ruff_python_ast::{self as ast}; +use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; @@ -515,7 +515,7 @@ pub fn definition_for_name<'db>( model: &SemanticModel<'db>, name: &ast::ExprName, ) -> Option> { - let definitions = definitions_for_name(model, name); + let definitions = definitions_for_name(model, name.id.as_str(), name.into()); // Find the first valid definition and return its kind for declaration in definitions { @@ -531,15 +531,15 @@ pub fn definition_for_name<'db>( /// are resolved (recursively) to the original definitions or module files. pub fn definitions_for_name<'db>( model: &SemanticModel<'db>, - name: &ast::ExprName, + name_str: &str, + node: AnyNodeRef<'_>, ) -> Vec> { let db = model.db(); let file = model.file(); let index = semantic_index(db, file); - let name_str = name.id.as_str(); // Get the scope for this name expression - let Some(file_scope) = model.scope(name.into()) else { + let Some(file_scope) = model.scope(node) else { return vec![]; }; @@ -648,7 +648,8 @@ pub fn definitions_for_name<'db>( // // https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex if matches!(name_str, "float" | "complex") - && let Some(union) = name.inferred_type(&SemanticModel::new(db, file)).as_union() + && let Some(expr) = node.expr_name() + && let Some(union) = expr.inferred_type(&SemanticModel::new(db, file)).as_union() && is_float_or_complex_annotation(db, union, name_str) { return union