Skip to content

Commit b005cdb

Browse files
UnboundVariableUnboundVariable
andauthored
[ty] Implemented support for "rename" language server feature (#19551)
This PR adds support for the "rename" language server feature. It builds upon existing functionality used for "go to references". The "rename" feature involves two language server requests. The first is a "prepare rename" request that determines whether renaming should be possible for the identifier at the current offset. The second is a "rename" request that returns a list of file ranges where the rename should be applied. Care must be taken when attempting to rename symbols that span files, especially if the symbols are defined in files that are not part of the project. We don't want to modify code in the user's Python environment or in the vendored stub files. I found a few bugs in the "go to references" feature when implementing "rename", and those bug fixes are included in this PR. --------- Co-authored-by: UnboundVariable <unbound@gmail.com>
1 parent b96aa46 commit b005cdb

16 files changed

Lines changed: 1065 additions & 73 deletions

File tree

crates/ty_ide/src/goto.rs

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use ruff_db::parsed::ParsedModuleRef;
1010
use ruff_python_ast::{self as ast, AnyNodeRef};
1111
use ruff_python_parser::TokenKind;
1212
use ruff_text_size::{Ranged, TextRange, TextSize};
13+
use ty_python_semantic::ImportAliasResolution;
1314
use ty_python_semantic::types::Type;
1415
use ty_python_semantic::types::definitions_for_keyword_argument;
1516
use ty_python_semantic::{
@@ -172,12 +173,16 @@ impl GotoTarget<'_> {
172173

173174
/// Gets the navigation ranges for this goto target.
174175
/// If a stub mapper is provided, definitions from stub files will be mapped to
175-
/// their corresponding source file implementations.
176+
/// their corresponding source file implementations. The `alias_resolution`
177+
/// parameter controls whether import aliases (i.e. "x" in "from a import b as x") are
178+
/// resolved or returned as is. We want to resolve them in some cases (like
179+
/// "goto declaration") but not in others (like find references or rename).
176180
pub(crate) fn get_definition_targets(
177181
&self,
178182
file: ruff_db::files::File,
179183
db: &dyn crate::Db,
180184
stub_mapper: Option<&StubMapper>,
185+
alias_resolution: ImportAliasResolution,
181186
) -> Option<crate::NavigationTargets> {
182187
use crate::NavigationTarget;
183188
use ruff_python_ast as ast;
@@ -229,10 +234,14 @@ impl GotoTarget<'_> {
229234
GotoTarget::ImportSymbolAlias {
230235
alias, import_from, ..
231236
} => {
232-
// Handle both original names and alias names in `from x import y as z` statements
233237
let symbol_name = alias.name.as_str();
234-
let definitions =
235-
definitions_for_imported_symbol(db, file, import_from, symbol_name);
238+
let definitions = definitions_for_imported_symbol(
239+
db,
240+
file,
241+
import_from,
242+
symbol_name,
243+
alias_resolution,
244+
);
236245

237246
definitions_to_navigation_targets(db, stub_mapper, definitions)
238247
}
@@ -254,12 +263,18 @@ impl GotoTarget<'_> {
254263

255264
// Handle import aliases (offset within 'z' in "import x.y as z")
256265
GotoTarget::ImportModuleAlias { alias } => {
257-
// For import aliases, navigate to the module being aliased
258-
// This only applies to regular import statements like "import x.y as z"
259-
let full_module_name = alias.name.as_str();
260-
261-
// Try to resolve the module
262-
resolve_module_to_navigation_target(db, full_module_name)
266+
if alias_resolution == ImportAliasResolution::ResolveAliases {
267+
let full_module_name = alias.name.as_str();
268+
// Try to resolve the module
269+
resolve_module_to_navigation_target(db, full_module_name)
270+
} else {
271+
let alias_range = alias.asname.as_ref().unwrap().range;
272+
Some(crate::NavigationTargets::single(NavigationTarget {
273+
file,
274+
focus_range: alias_range,
275+
full_range: alias.range(),
276+
}))
277+
}
263278
}
264279

265280
// Handle keyword arguments in call expressions

crates/ty_ide/src/goto_declaration.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::{Db, NavigationTargets, RangedValue};
33
use ruff_db::files::{File, FileRange};
44
use ruff_db::parsed::parsed_module;
55
use ruff_text_size::{Ranged, TextSize};
6+
use ty_python_semantic::ImportAliasResolution;
67

78
/// Navigate to the declaration of a symbol.
89
///
@@ -17,7 +18,12 @@ pub fn goto_declaration(
1718
let module = parsed_module(db, file).load(db);
1819
let goto_target = find_goto_target(&module, offset)?;
1920

20-
let declaration_targets = goto_target.get_definition_targets(file, db, None)?;
21+
let declaration_targets = goto_target.get_definition_targets(
22+
file,
23+
db,
24+
None,
25+
ImportAliasResolution::ResolveAliases,
26+
)?;
2127

2228
Some(RangedValue {
2329
range: FileRange::new(file, goto_target.range()),

crates/ty_ide/src/goto_definition.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::{Db, NavigationTargets, RangedValue};
44
use ruff_db::files::{File, FileRange};
55
use ruff_db::parsed::parsed_module;
66
use ruff_text_size::{Ranged, TextSize};
7+
use ty_python_semantic::ImportAliasResolution;
78

89
/// Navigate to the definition of a symbol.
910
///
@@ -22,7 +23,12 @@ pub fn goto_definition(
2223
// Create a StubMapper to map from stub files to source files
2324
let stub_mapper = StubMapper::new(db);
2425

25-
let definition_targets = goto_target.get_definition_targets(file, db, Some(&stub_mapper))?;
26+
let definition_targets = goto_target.get_definition_targets(
27+
file,
28+
db,
29+
Some(&stub_mapper),
30+
ImportAliasResolution::ResolveAliases,
31+
)?;
2632

2733
Some(RangedValue {
2834
range: FileRange::new(file, goto_target.range()),

crates/ty_ide/src/goto_references.rs

Lines changed: 70 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -688,34 +688,30 @@ cls = MyClass
688688
.source(
689689
"utils.py",
690690
"
691-
def helper_fun<CURSOR>ction(x):
691+
def fun<CURSOR>c(x):
692692
return x * 2
693693
",
694694
)
695695
.source(
696696
"module.py",
697697
"
698-
from utils import helper_function
698+
from utils import func
699699
700700
def process_data(data):
701-
return helper_function(data)
702-
703-
def double_process(data):
704-
result = helper_function(data)
705-
return helper_function(result)
701+
return func(data)
706702
",
707703
)
708704
.source(
709705
"app.py",
710706
"
711-
from utils import helper_function
707+
from utils import func
712708
713709
class DataProcessor:
714710
def __init__(self):
715-
self.multiplier = helper_function
711+
self.multiplier = func
716712
717713
def process(self, value):
718-
return helper_function(value)
714+
return func(value)
719715
",
720716
)
721717
.build();
@@ -724,46 +720,44 @@ class DataProcessor:
724720
info[references]: Reference 1
725721
--> utils.py:2:5
726722
|
727-
2 | def helper_function(x):
728-
| ^^^^^^^^^^^^^^^
723+
2 | def func(x):
724+
| ^^^^
729725
3 | return x * 2
730726
|
731727
732728
info[references]: Reference 2
733-
--> module.py:5:12
729+
--> module.py:2:19
734730
|
731+
2 | from utils import func
732+
| ^^^^
733+
3 |
735734
4 | def process_data(data):
736-
5 | return helper_function(data)
737-
| ^^^^^^^^^^^^^^^
738-
6 |
739-
7 | def double_process(data):
740735
|
741736
742737
info[references]: Reference 3
743-
--> module.py:8:14
738+
--> module.py:5:12
744739
|
745-
7 | def double_process(data):
746-
8 | result = helper_function(data)
747-
| ^^^^^^^^^^^^^^^
748-
9 | return helper_function(result)
740+
4 | def process_data(data):
741+
5 | return func(data)
742+
| ^^^^
749743
|
750744
751745
info[references]: Reference 4
752-
--> module.py:9:12
746+
--> app.py:2:19
753747
|
754-
7 | def double_process(data):
755-
8 | result = helper_function(data)
756-
9 | return helper_function(result)
757-
| ^^^^^^^^^^^^^^^
748+
2 | from utils import func
749+
| ^^^^
750+
3 |
751+
4 | class DataProcessor:
758752
|
759753
760754
info[references]: Reference 5
761755
--> app.py:6:27
762756
|
763757
4 | class DataProcessor:
764758
5 | def __init__(self):
765-
6 | self.multiplier = helper_function
766-
| ^^^^^^^^^^^^^^^
759+
6 | self.multiplier = func
760+
| ^^^^
767761
7 |
768762
8 | def process(self, value):
769763
|
@@ -772,8 +766,8 @@ class DataProcessor:
772766
--> app.py:9:16
773767
|
774768
8 | def process(self, value):
775-
9 | return helper_function(value)
776-
| ^^^^^^^^^^^^^^^
769+
9 | return func(value)
770+
| ^^^^
777771
|
778772
");
779773
}
@@ -855,4 +849,49 @@ def process_model():
855849
|
856850
");
857851
}
852+
853+
#[test]
854+
fn test_import_alias_references_should_not_resolve_to_original() {
855+
let test = CursorTest::builder()
856+
.source(
857+
"original.py",
858+
"
859+
def func():
860+
pass
861+
862+
func()
863+
",
864+
)
865+
.source(
866+
"importer.py",
867+
"
868+
from original import func as func_alias
869+
870+
func<CURSOR>_alias()
871+
",
872+
)
873+
.build();
874+
875+
// When finding references to the alias, we should NOT find references
876+
// to the original function in the original module
877+
assert_snapshot!(test.references(), @r"
878+
info[references]: Reference 1
879+
--> importer.py:2:30
880+
|
881+
2 | from original import func as func_alias
882+
| ^^^^^^^^^^
883+
3 |
884+
4 | func_alias()
885+
|
886+
887+
info[references]: Reference 2
888+
--> importer.py:4:1
889+
|
890+
2 | from original import func as func_alias
891+
3 |
892+
4 | func_alias()
893+
| ^^^^^^^^^^
894+
|
895+
");
896+
}
858897
}

crates/ty_ide/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ mod hover;
1212
mod inlay_hints;
1313
mod markup;
1414
mod references;
15+
mod rename;
1516
mod selection_range;
1617
mod semantic_tokens;
1718
mod signature_help;
@@ -29,6 +30,7 @@ pub use hover::hover;
2930
pub use inlay_hints::inlay_hints;
3031
pub use markup::MarkupKind;
3132
pub use references::ReferencesMode;
33+
pub use rename::{can_rename, rename};
3234
pub use selection_range::selection_range;
3335
pub use semantic_tokens::{
3436
SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, semantic_tokens,

0 commit comments

Comments
 (0)