mirror of https://github.com/astral-sh/ruff
Implement goto-definition and find-references for global/nonlocal statements (#21616)
## Summary The implementation here is to just record the idents of these statements in `scopes_by_expression` (which already supported idents but only ones that happened to appear in expressions), so that `definitions_for_name` Just Works. goto-type (and therefore hover) notably does not work on these statements because the typechecker does not record info for them. I am tempted to just introduce `type_for_name` which runs `definitions_for_name` to find other expressions and queries the inferred type... but that's a bit whack because it won't be the computed type at the right point in the code. It probably wouldn't be particularly expensive to just compute/record the type at those nodes, as if they were a load, because global/nonlocal is so scarce? ## Test Plan Snapshot tests added/re-enabled.
This commit is contained in:
parent
88bfc32dfc
commit
209ea06592
|
|
@ -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<DefinitionsOrTargets<'db>> {
|
||||
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<ty_python_semantic::ResolvedDefinition<'_>>,
|
||||
fn convert_resolved_definitions_to_targets<'db>(
|
||||
db: &'db dyn ty_python_semantic::Db,
|
||||
definitions: Vec<ty_python_semantic::ResolvedDefinition<'db>>,
|
||||
) -> Vec<crate::NavigationTarget> {
|
||||
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<Vec<ResolvedDefinition<'db>>> {
|
||||
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,
|
||||
)),
|
||||
|
|
|
|||
|
|
@ -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 x<CURSOR>y
|
||||
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_<CURSOR>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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
"
|
||||
|
|
|
|||
|
|
@ -1062,6 +1062,118 @@ f(**kwargs<CURSOR>)
|
|||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_nonlocal_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def outer():
|
||||
x = "outer_value"
|
||||
|
||||
def inner():
|
||||
nonlocal x
|
||||
x = "modified"
|
||||
return x<CURSOR> # 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 x<CURSOR>y
|
||||
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_<CURSOR>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_<CURSOR>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(
|
||||
|
|
|
|||
|
|
@ -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<CURSOR> # 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 x<CURSOR>y
|
||||
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_<CURSOR>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_<CURSOR>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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<Definition<'db>> {
|
||||
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<ResolvedDefinition<'db>> {
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue