Skip to content

Commit 8954b66

Browse files
Merge pull request #21979 from ChayimFriedman2/rename-ctor-field
feat: When renaming a field, rename variables in constructors as well
2 parents 0705955 + 9075155 commit 8954b66

5 files changed

Lines changed: 219 additions & 5 deletions

File tree

crates/hir/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1984,6 +1984,14 @@ impl Variant {
19841984
Variant::EnumVariant(e) => (*e).name(db),
19851985
}
19861986
}
1987+
1988+
pub fn adt(&self, db: &dyn HirDatabase) -> Adt {
1989+
match *self {
1990+
Variant::Struct(it) => it.into(),
1991+
Variant::Union(it) => it.into(),
1992+
Variant::EnumVariant(it) => it.parent_enum(db).into(),
1993+
}
1994+
}
19871995
}
19881996

19891997
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]

crates/hir/src/semantics.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,6 +1635,44 @@ impl<'db> SemanticsImpl<'db> {
16351635
.kmerge_by(|node1, node2| node1.text_range().len() < node2.text_range().len())
16361636
}
16371637

1638+
/// Returns the `return` expressions in this function's body,
1639+
/// excluding those inside closures or async blocks.
1640+
pub fn fn_return_points(&self, func: Function) -> Vec<InFile<ast::ReturnExpr>> {
1641+
let func_id = match func.id {
1642+
AnyFunctionId::FunctionId(id) => id,
1643+
_ => return vec![],
1644+
};
1645+
let (body, source_map) = Body::with_source_map(self.db, func_id.into());
1646+
1647+
fn collect_returns(
1648+
sema: &SemanticsImpl<'_>,
1649+
body: &Body,
1650+
source_map: &hir_def::expr_store::ExpressionStoreSourceMap,
1651+
expr_id: ExprId,
1652+
acc: &mut Vec<InFile<ast::ReturnExpr>>,
1653+
) {
1654+
match &body[expr_id] {
1655+
Expr::Closure { .. } | Expr::Const(_) => return,
1656+
Expr::Return { .. } => {
1657+
if let Ok(source) = source_map.expr_syntax(expr_id)
1658+
&& let Some(ret_expr) = source.value.cast::<ast::ReturnExpr>()
1659+
{
1660+
let root = sema.parse_or_expand(source.file_id);
1661+
acc.push(InFile::new(source.file_id, ret_expr.to_node(&root)));
1662+
}
1663+
}
1664+
_ => {}
1665+
}
1666+
body.walk_child_exprs(expr_id, |child| {
1667+
collect_returns(sema, body, source_map, child, acc);
1668+
});
1669+
}
1670+
1671+
let mut returns = vec![];
1672+
collect_returns(self, body, source_map, body.root_expr(), &mut returns);
1673+
returns
1674+
}
1675+
16381676
pub fn resolve_lifetime_param(&self, lifetime: &ast::Lifetime) -> Option<LifetimeParam> {
16391677
let text = lifetime.text();
16401678
let lifetime_param = lifetime.syntax().ancestors().find_map(|syn| {

crates/ide-db/src/rename.rs

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ use crate::{
2828
};
2929
use base_db::AnchoredPathBuf;
3030
use either::Either;
31-
use hir::{FieldSource, FileRange, InFile, ModuleSource, Name, Semantics, sym};
31+
use hir::{FieldSource, FileRange, HasCrate, InFile, ModuleSource, Name, Semantics, sym};
32+
use itertools::Itertools;
33+
use rustc_hash::FxHashSet;
3234
use span::{Edition, FileId, SyntaxContext};
3335
use stdx::{TupleExt, never};
3436
use syntax::{
@@ -405,6 +407,11 @@ fn rename_reference(
405407
source_edit_from_references(sema.db, references, def, &new_name, edition),
406408
)
407409
}));
410+
411+
if let Definition::Field(field) = def {
412+
rename_field_constructors(sema, field, &new_name, &mut source_change, config);
413+
}
414+
408415
if rename_definition == RenameDefinition::Yes {
409416
// This needs to come after the references edits, because we change the annotation of existing edits
410417
// if a conflict is detected.
@@ -415,6 +422,104 @@ fn rename_reference(
415422
Ok(source_change)
416423
}
417424

425+
fn rename_field_constructors(
426+
sema: &Semantics<'_, RootDatabase>,
427+
field: hir::Field,
428+
new_name: &Name,
429+
source_change: &mut SourceChange,
430+
config: &RenameConfig,
431+
) {
432+
let db = sema.db;
433+
let old_name = field.name(db);
434+
let adt = field.parent_def(db).adt(db);
435+
adt.ty(db).iterate_assoc_items(db, |assoc_item| {
436+
let ctor = assoc_item.as_function()?;
437+
if ctor.has_self_param(db) {
438+
return None;
439+
}
440+
if ctor.ret_type(db).as_adt() != Some(adt) {
441+
return None;
442+
}
443+
444+
let source = sema.source(ctor);
445+
let return_values = sema
446+
.fn_return_points(ctor)
447+
.into_iter()
448+
.filter_map(|ret| ret.value.expr())
449+
.chain(source.and_then(|source| source.value.body()?.tail_expr()));
450+
// FIXME: We could maybe skip ifs etc..
451+
452+
let get_renamed_field = |mut expr| {
453+
while let ast::Expr::ParenExpr(e) = &expr {
454+
expr = e.expr()?;
455+
}
456+
let ast::Expr::RecordExpr(expr) = expr else { return None };
457+
if sema.type_of_expr(&expr.clone().into())?.original.as_adt()? != adt {
458+
return None;
459+
};
460+
expr.record_expr_field_list()?.fields().find_map(|record_field| {
461+
if record_field.name_ref().is_none()
462+
&& Name::new_root(&record_field.field_name()?.text()) == old_name
463+
&& let ast::Expr::PathExpr(field_name) = record_field.expr()?
464+
{
465+
field_name.path()
466+
} else {
467+
None
468+
}
469+
})
470+
};
471+
let renamed_fields = return_values
472+
.map(get_renamed_field)
473+
.map(|renamed_field| {
474+
let renamed_field = renamed_field?;
475+
let hir::PathResolution::Local(local) = sema.resolve_path(&renamed_field)? else {
476+
return None;
477+
};
478+
let range = sema.original_range_opt(renamed_field.syntax())?.range;
479+
Some((range, local))
480+
})
481+
.collect::<Option<Vec<_>>>()?;
482+
483+
let edition = ctor.krate(db).edition(db);
484+
let locals = renamed_fields.iter().map(|&(_, local)| local).collect::<FxHashSet<_>>();
485+
let mut all_locals_source_change = SourceChange::default();
486+
for local in locals {
487+
let mut local_source_change = Definition::Local(local)
488+
.rename(sema, new_name.as_str(), RenameDefinition::Yes, config)
489+
.ok()?;
490+
491+
let (edit, _snippet) =
492+
local_source_change.source_file_edits.values_mut().exactly_one().ok()?;
493+
494+
// The struct literal will have an edit `old_name -> old_name: new_name`, and we need to remove
495+
// that, as we want an overlapping edit `old_name -> new_name`.
496+
for &(field_range, _) in &renamed_fields {
497+
edit.cancel_edits_touching(field_range);
498+
}
499+
500+
all_locals_source_change =
501+
std::mem::take(&mut all_locals_source_change).merge(local_source_change);
502+
}
503+
let (edit, _snippet) =
504+
all_locals_source_change.source_file_edits.values_mut().exactly_one().ok()?;
505+
for &(field_range, _) in &renamed_fields {
506+
edit.union(TextEdit::replace(field_range, new_name.display(db, edition).to_string()))
507+
.unwrap();
508+
}
509+
510+
let file_id = *all_locals_source_change.source_file_edits.keys().exactly_one().ok()?;
511+
if let Some((edit, _snippet)) = source_change.source_file_edits.get_mut(&file_id) {
512+
for &(field_range, _) in &renamed_fields {
513+
edit.cancel_edits_touching(field_range);
514+
}
515+
}
516+
517+
*source_change = std::mem::take(source_change).merge(all_locals_source_change);
518+
519+
None::<std::convert::Infallible>
520+
});
521+
}
522+
418523
pub fn source_edit_from_references(
419524
db: &RootDatabase,
420525
references: &[FileReference],

crates/ide-db/src/text_edit.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ impl TextEdit {
151151
pub fn change_annotation(&self) -> Option<ChangeAnnotationId> {
152152
self.annotation
153153
}
154+
155+
pub fn cancel_edits_touching(&mut self, touching: TextRange) {
156+
self.indels.retain(|indel| indel.delete.intersect(touching).is_none());
157+
}
154158
}
155159

156160
impl IntoIterator for TextEdit {

crates/ide/src/rename.rs

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,17 +1326,17 @@ impl Foo {
13261326
struct Foo { foo$0: i32 }
13271327
13281328
impl Foo {
1329-
fn new(foo: i32) -> Self {
1330-
Self { foo }
1329+
fn foo(foo: i32) {
1330+
Self { foo };
13311331
}
13321332
}
13331333
"#,
13341334
r#"
13351335
struct Foo { field: i32 }
13361336
13371337
impl Foo {
1338-
fn new(foo: i32) -> Self {
1339-
Self { field: foo }
1338+
fn foo(foo: i32) {
1339+
Self { field: foo };
13401340
}
13411341
}
13421342
"#,
@@ -3926,6 +3926,65 @@ impl Foo {
39263926
39273927
fn bar() {
39283928
Foo::foo(&Foo, 1);
3929+
}
3930+
"#,
3931+
);
3932+
}
3933+
3934+
#[test]
3935+
fn rename_constructor_locals() {
3936+
check(
3937+
"field",
3938+
r#"
3939+
struct Struct {
3940+
struct_field$0: String,
3941+
}
3942+
3943+
impl Struct {
3944+
fn new(struct_field: String) -> Self {
3945+
if false {
3946+
return Self { struct_field };
3947+
}
3948+
Self { struct_field }
3949+
}
3950+
}
3951+
3952+
mod foo {
3953+
macro_rules! m {
3954+
($it:expr) => { return $it };
3955+
}
3956+
3957+
impl crate::Struct {
3958+
fn with_foo(struct_field: String) -> crate::Struct {
3959+
m!(crate::Struct { struct_field });
3960+
}
3961+
}
3962+
}
3963+
"#,
3964+
r#"
3965+
struct Struct {
3966+
field: String,
3967+
}
3968+
3969+
impl Struct {
3970+
fn new(field: String) -> Self {
3971+
if false {
3972+
return Self { field };
3973+
}
3974+
Self { field }
3975+
}
3976+
}
3977+
3978+
mod foo {
3979+
macro_rules! m {
3980+
($it:expr) => { return $it };
3981+
}
3982+
3983+
impl crate::Struct {
3984+
fn with_foo(field: String) -> crate::Struct {
3985+
m!(crate::Struct { field });
3986+
}
3987+
}
39293988
}
39303989
"#,
39313990
);

0 commit comments

Comments
 (0)