Skip to content

Commit 51c3ab5

Browse files
committed
Auto merge of rust-lang#14920 - lowr:fix/overhaul-named-struct-to-tuple-struct, r=Veykril
Fix edits for `convert_named_struct_to_tuple_struct` Two fixes: - When replacing syntax nodes, macro files weren't taken into account. Edits were simply made for `node.syntax().text_range()`, which would be wrong range when `node` is inside a macro file. - We do ancestor node traversal for every struct name reference to find record expressions/patterns to edit, but we didn't verify that expressions/patterns do actually refer to the struct we're operating on. Best reviewed one commit at a time. Fixes rust-lang#13780 Fixes rust-lang#14927
2 parents bc82952 + 033e6ac commit 51c3ab5

File tree

1 file changed

+203
-42
lines changed

1 file changed

+203
-42
lines changed

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

+203-42
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};
@@ -52,6 +52,9 @@ pub(crate) fn convert_named_struct_to_tuple_struct(
5252
acc: &mut Assists,
5353
ctx: &AssistContext<'_>,
5454
) -> Option<()> {
55+
// XXX: We don't currently provide this assist for struct definitions inside macros, but if we
56+
// are to lift this limitation, don't forget to make `edit_struct_def()` consider macro files
57+
// too.
5558
let strukt = ctx.find_node_at_offset::<Either<ast::Struct, ast::Variant>>()?;
5659
let field_list = strukt.as_ref().either(|s| s.field_list(), |v| v.field_list())?;
5760
let record_fields = match field_list {
@@ -62,12 +65,11 @@ pub(crate) fn convert_named_struct_to_tuple_struct(
6265
Either::Left(s) => Either::Left(ctx.sema.to_def(s)?),
6366
Either::Right(v) => Either::Right(ctx.sema.to_def(v)?),
6467
};
65-
let target = strukt.as_ref().either(|s| s.syntax(), |v| v.syntax()).text_range();
6668

6769
acc.add(
6870
AssistId("convert_named_struct_to_tuple_struct", AssistKind::RefactorRewrite),
6971
"Convert to tuple struct",
70-
target,
72+
strukt.syntax().text_range(),
7173
|edit| {
7274
edit_field_references(ctx, edit, record_fields.fields());
7375
edit_struct_references(ctx, edit, strukt_def);
@@ -82,6 +84,8 @@ fn edit_struct_def(
8284
strukt: &Either<ast::Struct, ast::Variant>,
8385
record_fields: ast::RecordFieldList,
8486
) {
87+
// Note that we don't need to consider macro files in this function because this this is
88+
// currently not triggered for struct definitions inside macro calls.
8589
let tuple_fields = record_fields
8690
.fields()
8791
.filter_map(|f| Some(ast::make::tuple_field(f.visibility(), f.ty()?)));
@@ -137,50 +141,72 @@ fn edit_struct_references(
137141
};
138142
let usages = strukt_def.usages(&ctx.sema).include_self_refs().all();
139143

140-
let edit_node = |edit: &mut SourceChangeBuilder, node: SyntaxNode| -> Option<()> {
141-
match_ast! {
142-
match node {
143-
ast::RecordPat(record_struct_pat) => {
144-
edit.replace(
145-
record_struct_pat.syntax().text_range(),
146-
ast::make::tuple_struct_pat(
147-
record_struct_pat.path()?,
148-
record_struct_pat
149-
.record_pat_field_list()?
150-
.fields()
151-
.filter_map(|pat| pat.pat())
152-
)
153-
.to_string()
154-
);
155-
},
156-
ast::RecordExpr(record_expr) => {
157-
let path = record_expr.path()?;
158-
let args = record_expr
159-
.record_expr_field_list()?
160-
.fields()
161-
.filter_map(|f| f.expr())
162-
.join(", ");
163-
164-
edit.replace(record_expr.syntax().text_range(), format!("{path}({args})"));
165-
},
166-
_ => return None,
167-
}
168-
}
169-
Some(())
170-
};
171-
172144
for (file_id, refs) in usages {
173145
edit.edit_file(file_id);
174146
for r in refs {
175-
for node in r.name.syntax().ancestors() {
176-
if edit_node(edit, node).is_some() {
177-
break;
178-
}
179-
}
147+
process_struct_name_reference(ctx, r, edit);
180148
}
181149
}
182150
}
183151

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+
_ => {}
204+
}
205+
}
206+
207+
Some(())
208+
}
209+
184210
fn edit_field_references(
185211
ctx: &AssistContext<'_>,
186212
edit: &mut SourceChangeBuilder,
@@ -199,7 +225,7 @@ fn edit_field_references(
199225
if let Some(name_ref) = r.name.as_name_ref() {
200226
// Only edit the field reference if it's part of a `.field` access
201227
if name_ref.syntax().parent().and_then(ast::FieldExpr::cast).is_some() {
202-
edit.replace(name_ref.syntax().text_range(), index.to_string());
228+
edit.replace(r.range, index.to_string());
203229
}
204230
}
205231
}
@@ -813,6 +839,141 @@ use crate::{A::Variant, Inner};
813839
fn f() {
814840
let a = Variant(Inner);
815841
}
842+
"#,
843+
);
844+
}
845+
846+
#[test]
847+
fn field_access_inside_macro_call() {
848+
check_assist(
849+
convert_named_struct_to_tuple_struct,
850+
r#"
851+
struct $0Struct {
852+
inner: i32,
853+
}
854+
855+
macro_rules! id {
856+
($e:expr) => { $e }
857+
}
858+
859+
fn test(c: Struct) {
860+
id!(c.inner);
861+
}
862+
"#,
863+
r#"
864+
struct Struct(i32);
865+
866+
macro_rules! id {
867+
($e:expr) => { $e }
868+
}
869+
870+
fn test(c: Struct) {
871+
id!(c.0);
872+
}
873+
"#,
874+
)
875+
}
876+
877+
#[test]
878+
fn struct_usage_inside_macro_call() {
879+
check_assist(
880+
convert_named_struct_to_tuple_struct,
881+
r#"
882+
macro_rules! id {
883+
($($t:tt)*) => { $($t)* }
884+
}
885+
886+
struct $0Struct {
887+
inner: i32,
888+
}
889+
890+
fn test() {
891+
id! {
892+
let s = Struct {
893+
inner: 42,
894+
};
895+
let Struct { inner: value } = s;
896+
let Struct { inner } = s;
897+
}
898+
}
899+
"#,
900+
r#"
901+
macro_rules! id {
902+
($($t:tt)*) => { $($t)* }
903+
}
904+
905+
struct Struct(i32);
906+
907+
fn test() {
908+
id! {
909+
let s = Struct(42);
910+
let Struct(value) = s;
911+
let Struct(inner) = s;
912+
}
913+
}
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+
}
816977
"#,
817978
);
818979
}

0 commit comments

Comments
 (0)