Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit 033e6ac

Browse files
committed
Verify name references more rigidly
Previously we didn't verify that record expressions/patterns that were found did actually point to the struct we're operating on. Moreover, when that record expressions/patterns had missing child nodes, we would continue traversing their ancestor nodes.
1 parent ab93475 commit 033e6ac

File tree

1 file changed

+124
-48
lines changed

1 file changed

+124
-48
lines changed

crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs

Lines changed: 124 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use either::Either;
2-
use ide_db::defs::Definition;
2+
use ide_db::{defs::Definition, search::FileReference};
33
use itertools::Itertools;
44
use syntax::{
55
ast::{self, AstNode, HasGenericParams, HasVisibility},
6-
match_ast, SyntaxKind, SyntaxNode,
6+
match_ast, SyntaxKind,
77
};
88

99
use crate::{assist_context::SourceChangeBuilder, AssistContext, AssistId, AssistKind, Assists};
@@ -141,57 +141,70 @@ fn edit_struct_references(
141141
};
142142
let usages = strukt_def.usages(&ctx.sema).include_self_refs().all();
143143

144-
let edit_node = |edit: &mut SourceChangeBuilder, node: SyntaxNode| -> Option<()> {
145-
match_ast! {
146-
match node {
147-
ast::RecordPat(record_struct_pat) => {
148-
let Some(fr) = ctx.sema.original_range_opt(record_struct_pat.syntax()) else {
149-
// We've found the node to replace, so we should return `Some` even if the
150-
// replacement failed to stop the ancestor node traversal.
151-
return Some(());
152-
};
153-
edit.replace(
154-
fr.range,
155-
ast::make::tuple_struct_pat(
156-
record_struct_pat.path()?,
157-
record_struct_pat
158-
.record_pat_field_list()?
159-
.fields()
160-
.filter_map(|pat| pat.pat())
161-
)
162-
.to_string()
163-
);
164-
},
165-
ast::RecordExpr(record_expr) => {
166-
let Some(fr) = ctx.sema.original_range_opt(record_expr.syntax()) else {
167-
// See the comment above.
168-
return Some(());
169-
};
170-
let path = record_expr.path()?;
171-
let args = record_expr
172-
.record_expr_field_list()?
173-
.fields()
174-
.filter_map(|f| f.expr())
175-
.join(", ");
176-
177-
edit.replace(fr.range, format!("{path}({args})"));
178-
},
179-
_ => return None,
180-
}
181-
}
182-
Some(())
183-
};
184-
185144
for (file_id, refs) in usages {
186145
edit.edit_file(file_id);
187146
for r in refs {
188-
for node in r.name.syntax().ancestors() {
189-
if edit_node(edit, node).is_some() {
190-
break;
191-
}
192-
}
147+
process_struct_name_reference(ctx, r, edit);
148+
}
149+
}
150+
}
151+
152+
fn process_struct_name_reference(
153+
ctx: &AssistContext<'_>,
154+
r: FileReference,
155+
edit: &mut SourceChangeBuilder,
156+
) -> Option<()> {
157+
// First check if it's the last semgnet of a path that directly belongs to a record
158+
// expression/pattern.
159+
let name_ref = r.name.as_name_ref()?;
160+
let path_segment = name_ref.syntax().parent().and_then(ast::PathSegment::cast)?;
161+
// A `PathSegment` always belongs to a `Path`, so there's at least one `Path` at this point.
162+
let full_path =
163+
path_segment.syntax().parent()?.ancestors().map_while(ast::Path::cast).last().unwrap();
164+
165+
if full_path.segment().unwrap().name_ref()? != *name_ref {
166+
// `name_ref` isn't the last segment of the path, so `full_path` doesn't point to the
167+
// struct we want to edit.
168+
return None;
169+
}
170+
171+
let parent = full_path.syntax().parent()?;
172+
match_ast! {
173+
match parent {
174+
ast::RecordPat(record_struct_pat) => {
175+
// When we failed to get the original range for the whole struct expression node,
176+
// we can't provide any reasonable edit. Leave it untouched.
177+
let file_range = ctx.sema.original_range_opt(record_struct_pat.syntax())?;
178+
edit.replace(
179+
file_range.range,
180+
ast::make::tuple_struct_pat(
181+
record_struct_pat.path()?,
182+
record_struct_pat
183+
.record_pat_field_list()?
184+
.fields()
185+
.filter_map(|pat| pat.pat())
186+
)
187+
.to_string()
188+
);
189+
},
190+
ast::RecordExpr(record_expr) => {
191+
// When we failed to get the original range for the whole struct pattern node,
192+
// we can't provide any reasonable edit. Leave it untouched.
193+
let file_range = ctx.sema.original_range_opt(record_expr.syntax())?;
194+
let path = record_expr.path()?;
195+
let args = record_expr
196+
.record_expr_field_list()?
197+
.fields()
198+
.filter_map(|f| f.expr())
199+
.join(", ");
200+
201+
edit.replace(file_range.range, format!("{path}({args})"));
202+
},
203+
_ => {}
193204
}
194205
}
206+
207+
Some(())
195208
}
196209

197210
fn edit_field_references(
@@ -898,6 +911,69 @@ fn test() {
898911
let Struct(inner) = s;
899912
}
900913
}
914+
"#,
915+
);
916+
}
917+
918+
#[test]
919+
fn struct_name_ref_may_not_be_part_of_struct_expr_or_struct_pat() {
920+
check_assist(
921+
convert_named_struct_to_tuple_struct,
922+
r#"
923+
struct $0Struct {
924+
inner: i32,
925+
}
926+
struct Outer<T> {
927+
value: T,
928+
}
929+
fn foo<T>() -> T { loop {} }
930+
931+
fn test() {
932+
Outer {
933+
value: foo::<Struct>();
934+
}
935+
}
936+
937+
trait HasAssoc {
938+
type Assoc;
939+
fn test();
940+
}
941+
impl HasAssoc for Struct {
942+
type Assoc = Outer<i32>;
943+
fn test() {
944+
let a = Self::Assoc {
945+
value: 42,
946+
};
947+
let Self::Assoc { value } = a;
948+
}
949+
}
950+
"#,
951+
r#"
952+
struct Struct(i32);
953+
struct Outer<T> {
954+
value: T,
955+
}
956+
fn foo<T>() -> T { loop {} }
957+
958+
fn test() {
959+
Outer {
960+
value: foo::<Struct>();
961+
}
962+
}
963+
964+
trait HasAssoc {
965+
type Assoc;
966+
fn test();
967+
}
968+
impl HasAssoc for Struct {
969+
type Assoc = Outer<i32>;
970+
fn test() {
971+
let a = Self::Assoc {
972+
value: 42,
973+
};
974+
let Self::Assoc { value } = a;
975+
}
976+
}
901977
"#,
902978
);
903979
}

0 commit comments

Comments
 (0)