1use hir::HasVisibility;
2use ide_db::{
3 FxHashMap, FxHashSet,
4 assists::AssistId,
5 defs::Definition,
6 helpers::mod_path_to_ast,
7 search::{FileReference, SearchScope},
8};
9use itertools::Itertools;
10use syntax::ast::{HasName, syntax_factory::SyntaxFactory};
11use syntax::syntax_editor::SyntaxEditor;
12use syntax::{AstNode, Edition, SmolStr, SyntaxNode, ToSmolStr, ast};
13
14use crate::{
15 assist_context::{AssistContext, Assists, SourceChangeBuilder},
16 utils::ref_field_expr::determine_ref_and_parens,
17};
18
19pub(crate) fn destructure_struct_binding(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
47 let ident_pat = ctx.find_node_at_offset::<ast::IdentPat>()?;
48 let data = collect_data(ident_pat, ctx)?;
49
50 acc.add(
51 AssistId::refactor_rewrite("destructure_struct_binding"),
52 "Destructure struct binding",
53 data.ident_pat.syntax().text_range(),
54 |edit| destructure_struct_binding_impl(ctx, edit, &data),
55 );
56
57 Some(())
58}
59
60fn destructure_struct_binding_impl(
61 ctx: &AssistContext<'_>,
62 builder: &mut SourceChangeBuilder,
63 data: &StructEditData,
64) {
65 let field_names = generate_field_names(ctx, data);
66 let mut editor = builder.make_editor(data.ident_pat.syntax());
67 destructure_pat(ctx, &mut editor, data, &field_names);
68 update_usages(ctx, &mut editor, data, &field_names.into_iter().collect());
69 builder.add_file_edits(ctx.vfs_file_id(), editor);
70}
71
72struct StructEditData {
73 ident_pat: ast::IdentPat,
74 name: ast::Name,
75 kind: hir::StructKind,
76 struct_def_path: hir::ModPath,
77 visible_fields: Vec<hir::Field>,
78 usages: Vec<FileReference>,
79 names_in_scope: FxHashSet<SmolStr>,
80 has_private_members: bool,
81 need_record_field_name: bool,
82 is_ref: bool,
83 edition: Edition,
84}
85
86fn collect_data(ident_pat: ast::IdentPat, ctx: &AssistContext<'_>) -> Option<StructEditData> {
87 let ty = ctx.sema.type_of_binding_in_pat(&ident_pat)?;
88 let hir::Adt::Struct(struct_type) = ty.strip_references().as_adt()? else { return None };
89
90 let module = ctx.sema.scope(ident_pat.syntax())?.module();
91 let cfg = ctx.config.find_path_config(ctx.sema.is_nightly(module.krate(ctx.db())));
92 let struct_def = hir::ModuleDef::from(struct_type);
93 let kind = struct_type.kind(ctx.db());
94 let struct_def_path = module.find_path(ctx.db(), struct_def, cfg)?;
95
96 let is_non_exhaustive = struct_def.attrs(ctx.db())?.is_non_exhaustive();
97 let is_foreign_crate =
98 struct_def.module(ctx.db()).is_some_and(|m| m.krate(ctx.db()) != module.krate(ctx.db()));
99
100 let fields = struct_type.fields(ctx.db());
101 let n_fields = fields.len();
102
103 let visible_fields =
104 fields.into_iter().filter(|field| field.is_visible_from(ctx.db(), module)).collect_vec();
105
106 if visible_fields.is_empty() {
107 return None;
108 }
109
110 let has_private_members =
111 (is_non_exhaustive && is_foreign_crate) || visible_fields.len() < n_fields;
112
113 if !matches!(kind, hir::StructKind::Record) && has_private_members {
115 return None;
116 }
117
118 let is_ref = ty.is_reference();
119 let need_record_field_name = ident_pat
120 .syntax()
121 .parent()
122 .and_then(ast::RecordPatField::cast)
123 .is_some_and(|field| field.colon_token().is_none());
124
125 let usages = ctx
126 .sema
127 .to_def(&ident_pat)
128 .and_then(|def| {
129 Definition::Local(def)
130 .usages(&ctx.sema)
131 .in_scope(&SearchScope::single_file(ctx.file_id()))
132 .all()
133 .iter()
134 .next()
135 .map(|(_, refs)| refs.to_vec())
136 })
137 .unwrap_or_default();
138
139 let names_in_scope = get_names_in_scope(ctx, &ident_pat, &usages).unwrap_or_default();
140
141 Some(StructEditData {
142 name: ident_pat.name()?,
143 ident_pat,
144 kind,
145 struct_def_path,
146 usages,
147 has_private_members,
148 visible_fields,
149 names_in_scope,
150 need_record_field_name,
151 is_ref,
152 edition: module.krate(ctx.db()).edition(ctx.db()),
153 })
154}
155
156fn get_names_in_scope(
157 ctx: &AssistContext<'_>,
158 ident_pat: &ast::IdentPat,
159 usages: &[FileReference],
160) -> Option<FxHashSet<SmolStr>> {
161 fn last_usage(usages: &[FileReference]) -> Option<SyntaxNode> {
162 usages.last()?.name.syntax().into_node()
163 }
164
165 let last_usage = last_usage(usages);
168 let node = last_usage.as_ref().unwrap_or(ident_pat.syntax());
169 let scope = ctx.sema.scope(node)?;
170
171 let mut names = FxHashSet::default();
172 scope.process_all_names(&mut |name, scope| {
173 if let hir::ScopeDef::Local(_) = scope {
174 names.insert(name.as_str().into());
175 }
176 });
177 Some(names)
178}
179
180fn destructure_pat(
181 _ctx: &AssistContext<'_>,
182 editor: &mut SyntaxEditor,
183 data: &StructEditData,
184 field_names: &[(SmolStr, SmolStr)],
185) {
186 let ident_pat = &data.ident_pat;
187 let name = &data.name;
188
189 let struct_path = mod_path_to_ast(&data.struct_def_path, data.edition);
190 let is_ref = ident_pat.ref_token().is_some();
191 let is_mut = ident_pat.mut_token().is_some();
192
193 let make = SyntaxFactory::with_mappings();
194 let new_pat = match data.kind {
195 hir::StructKind::Tuple => {
196 let ident_pats = field_names.iter().map(|(_, new_name)| {
197 let name = make.name(new_name);
198 ast::Pat::from(make.ident_pat(is_ref, is_mut, name))
199 });
200 ast::Pat::TupleStructPat(make.tuple_struct_pat(struct_path, ident_pats))
201 }
202 hir::StructKind::Record => {
203 let fields = field_names.iter().map(|(old_name, new_name)| {
204 if old_name == new_name {
206 make.record_pat_field_shorthand(
207 make.ident_pat(is_ref, is_mut, make.name(old_name)).into(),
208 )
209 } else {
210 make.record_pat_field(
211 make.name_ref(old_name),
212 ast::Pat::IdentPat(make.ident_pat(is_ref, is_mut, make.name(new_name))),
213 )
214 }
215 });
216 let field_list = make
217 .record_pat_field_list(fields, data.has_private_members.then_some(make.rest_pat()));
218
219 ast::Pat::RecordPat(make.record_pat_with_fields(struct_path, field_list))
220 }
221 hir::StructKind::Unit => make.path_pat(struct_path),
222 };
223
224 let destructured_pat = if data.need_record_field_name {
227 make.record_pat_field(make.name_ref(&name.to_string()), new_pat).syntax().clone()
228 } else {
229 new_pat.syntax().clone()
230 };
231
232 editor.add_mappings(make.finish_with_mappings());
233 editor.replace(data.ident_pat.syntax(), destructured_pat);
234}
235
236fn generate_field_names(ctx: &AssistContext<'_>, data: &StructEditData) -> Vec<(SmolStr, SmolStr)> {
237 match data.kind {
238 hir::StructKind::Tuple => data
239 .visible_fields
240 .iter()
241 .enumerate()
242 .map(|(index, _)| {
243 let new_name = new_field_name((format!("_{index}")).into(), &data.names_in_scope);
244 (index.to_string().into(), new_name)
245 })
246 .collect(),
247 hir::StructKind::Record => data
248 .visible_fields
249 .iter()
250 .map(|field| {
251 let field_name = field.name(ctx.db()).display_no_db(data.edition).to_smolstr();
252 let new_name = new_field_name(field_name.clone(), &data.names_in_scope);
253 (field_name, new_name)
254 })
255 .collect(),
256 hir::StructKind::Unit => Vec::new(),
257 }
258}
259
260fn new_field_name(base_name: SmolStr, names_in_scope: &FxHashSet<SmolStr>) -> SmolStr {
261 let mut name = base_name.clone();
262 let mut i = 1;
263 while names_in_scope.contains(&name) {
264 name = format!("{base_name}_{i}").into();
265 i += 1;
266 }
267 name
268}
269
270fn update_usages(
271 ctx: &AssistContext<'_>,
272 editor: &mut SyntaxEditor,
273 data: &StructEditData,
274 field_names: &FxHashMap<SmolStr, SmolStr>,
275) {
276 let make = SyntaxFactory::with_mappings();
277 let edits = data
278 .usages
279 .iter()
280 .filter_map(|r| build_usage_edit(ctx, &make, data, r, field_names))
281 .collect_vec();
282 editor.add_mappings(make.finish_with_mappings());
283 for (old, new) in edits {
284 editor.replace(old, new);
285 }
286}
287
288fn build_usage_edit(
289 ctx: &AssistContext<'_>,
290 make: &SyntaxFactory,
291 data: &StructEditData,
292 usage: &FileReference,
293 field_names: &FxHashMap<SmolStr, SmolStr>,
294) -> Option<(SyntaxNode, SyntaxNode)> {
295 match usage.name.syntax().ancestors().find_map(ast::FieldExpr::cast) {
296 Some(field_expr) => Some({
297 let field_name: SmolStr = field_expr.name_ref()?.to_string().into();
298 let new_field_name = field_names.get(&field_name)?;
299 let new_expr = ast::make::expr_path(ast::make::ext::ident_path(new_field_name));
300
301 if data.is_ref {
303 let (replace_expr, ref_data) = determine_ref_and_parens(ctx, &field_expr);
304 (
305 replace_expr.syntax().clone_for_update(),
306 ref_data.wrap_expr(new_expr).syntax().clone_for_update(),
307 )
308 } else {
309 (field_expr.syntax().clone(), new_expr.syntax().clone_for_update())
310 }
311 }),
312 None => Some((
313 usage.name.syntax().as_node().unwrap().clone(),
314 make.expr_macro(
315 ast::make::ext::ident_path("todo"),
316 make.token_tree(syntax::SyntaxKind::L_PAREN, []),
317 )
318 .syntax()
319 .clone(),
320 )),
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 use crate::tests::{check_assist, check_assist_not_applicable};
329
330 #[test]
331 fn record_struct() {
332 check_assist(
333 destructure_struct_binding,
334 r#"
335 struct Foo { bar: i32, baz: i32 }
336
337 fn main() {
338 let $0foo = Foo { bar: 1, baz: 2 };
339 let bar2 = foo.bar;
340 let baz2 = &foo.baz;
341
342 let foo2 = foo;
343 }
344 "#,
345 r#"
346 struct Foo { bar: i32, baz: i32 }
347
348 fn main() {
349 let Foo { bar, baz } = Foo { bar: 1, baz: 2 };
350 let bar2 = bar;
351 let baz2 = &baz;
352
353 let foo2 = todo!();
354 }
355 "#,
356 )
357 }
358
359 #[test]
360 fn tuple_struct() {
361 check_assist(
362 destructure_struct_binding,
363 r#"
364 struct Foo(i32, i32);
365
366 fn main() {
367 let $0foo = Foo(1, 2);
368 let bar2 = foo.0;
369 let baz2 = foo.1;
370
371 let foo2 = foo;
372 }
373 "#,
374 r#"
375 struct Foo(i32, i32);
376
377 fn main() {
378 let Foo(_0, _1) = Foo(1, 2);
379 let bar2 = _0;
380 let baz2 = _1;
381
382 let foo2 = todo!();
383 }
384 "#,
385 )
386 }
387
388 #[test]
389 fn unit_struct() {
390 check_assist_not_applicable(
391 destructure_struct_binding,
392 r#"
393 struct Foo;
394
395 fn main() {
396 let $0foo = Foo;
397 }
398 "#,
399 )
400 }
401
402 #[test]
403 fn in_foreign_crate() {
404 check_assist(
405 destructure_struct_binding,
406 r#"
407 //- /lib.rs crate:dep
408 pub struct Foo { pub bar: i32 };
409
410 //- /main.rs crate:main deps:dep
411 fn main() {
412 let $0foo = dep::Foo { bar: 1 };
413 let bar2 = foo.bar;
414 }
415 "#,
416 r#"
417 fn main() {
418 let dep::Foo { bar } = dep::Foo { bar: 1 };
419 let bar2 = bar;
420 }
421 "#,
422 )
423 }
424
425 #[test]
426 fn non_exhaustive_record_appends_rest() {
427 check_assist(
428 destructure_struct_binding,
429 r#"
430 //- /lib.rs crate:dep
431 #[non_exhaustive]
432 pub struct Foo { pub bar: i32 };
433
434 //- /main.rs crate:main deps:dep
435 fn main($0foo: dep::Foo) {
436 let bar2 = foo.bar;
437 }
438 "#,
439 r#"
440 fn main(dep::Foo { bar, .. }: dep::Foo) {
441 let bar2 = bar;
442 }
443 "#,
444 )
445 }
446
447 #[test]
448 fn non_exhaustive_tuple_not_applicable() {
449 check_assist_not_applicable(
450 destructure_struct_binding,
451 r#"
452 //- /lib.rs crate:dep
453 #[non_exhaustive]
454 pub struct Foo(pub i32, pub i32);
455
456 //- /main.rs crate:main deps:dep
457 fn main(foo: dep::Foo) {
458 let $0foo2 = foo;
459 let bar = foo2.0;
460 let baz = foo2.1;
461 }
462 "#,
463 )
464 }
465
466 #[test]
467 fn non_exhaustive_unit_not_applicable() {
468 check_assist_not_applicable(
469 destructure_struct_binding,
470 r#"
471 //- /lib.rs crate:dep
472 #[non_exhaustive]
473 pub struct Foo;
474
475 //- /main.rs crate:main deps:dep
476 fn main(foo: dep::Foo) {
477 let $0foo2 = foo;
478 }
479 "#,
480 )
481 }
482
483 #[test]
484 fn record_private_fields_appends_rest() {
485 check_assist(
486 destructure_struct_binding,
487 r#"
488 //- /lib.rs crate:dep
489 pub struct Foo { pub bar: i32, baz: i32 };
490
491 //- /main.rs crate:main deps:dep
492 fn main(foo: dep::Foo) {
493 let $0foo2 = foo;
494 let bar2 = foo2.bar;
495 }
496 "#,
497 r#"
498 fn main(foo: dep::Foo) {
499 let dep::Foo { bar, .. } = foo;
500 let bar2 = bar;
501 }
502 "#,
503 )
504 }
505
506 #[test]
507 fn tuple_private_fields_not_applicable() {
508 check_assist_not_applicable(
509 destructure_struct_binding,
510 r#"
511 //- /lib.rs crate:dep
512 pub struct Foo(pub i32, i32);
513
514 //- /main.rs crate:main deps:dep
515 fn main(foo: dep::Foo) {
516 let $0foo2 = foo;
517 let bar2 = foo2.0;
518 }
519 "#,
520 )
521 }
522
523 #[test]
524 fn nested_inside_record() {
525 check_assist(
526 destructure_struct_binding,
527 r#"
528 struct Foo { fizz: Fizz }
529 struct Fizz { buzz: i32 }
530
531 fn main() {
532 let Foo { $0fizz } = Foo { fizz: Fizz { buzz: 1 } };
533 let buzz2 = fizz.buzz;
534 }
535 "#,
536 r#"
537 struct Foo { fizz: Fizz }
538 struct Fizz { buzz: i32 }
539
540 fn main() {
541 let Foo { fizz: Fizz { buzz } } = Foo { fizz: Fizz { buzz: 1 } };
542 let buzz2 = buzz;
543 }
544 "#,
545 )
546 }
547
548 #[test]
549 fn nested_inside_tuple() {
550 check_assist(
551 destructure_struct_binding,
552 r#"
553 struct Foo(Fizz);
554 struct Fizz { buzz: i32 }
555
556 fn main() {
557 let Foo($0fizz) = Foo(Fizz { buzz: 1 });
558 let buzz2 = fizz.buzz;
559 }
560 "#,
561 r#"
562 struct Foo(Fizz);
563 struct Fizz { buzz: i32 }
564
565 fn main() {
566 let Foo(Fizz { buzz }) = Foo(Fizz { buzz: 1 });
567 let buzz2 = buzz;
568 }
569 "#,
570 )
571 }
572
573 #[test]
574 fn mut_record() {
575 check_assist(
576 destructure_struct_binding,
577 r#"
578 struct Foo { bar: i32, baz: i32 }
579
580 fn main() {
581 let mut $0foo = Foo { bar: 1, baz: 2 };
582 let bar2 = foo.bar;
583 let baz2 = &foo.baz;
584 }
585 "#,
586 r#"
587 struct Foo { bar: i32, baz: i32 }
588
589 fn main() {
590 let Foo { mut bar, mut baz } = Foo { bar: 1, baz: 2 };
591 let bar2 = bar;
592 let baz2 = &baz;
593 }
594 "#,
595 )
596 }
597
598 #[test]
599 fn mut_record_field() {
600 check_assist(
601 destructure_struct_binding,
602 r#"
603 struct Foo { x: () }
604 struct Bar { foo: Foo }
605 fn f(Bar { mut $0foo }: Bar) {}
606 "#,
607 r#"
608 struct Foo { x: () }
609 struct Bar { foo: Foo }
610 fn f(Bar { foo: Foo { mut x } }: Bar) {}
611 "#,
612 )
613 }
614
615 #[test]
616 fn ref_record_field() {
617 check_assist(
618 destructure_struct_binding,
619 r#"
620 struct Foo { x: () }
621 struct Bar { foo: Foo }
622 fn f(Bar { ref $0foo }: Bar) {
623 let _ = foo.x;
624 }
625 "#,
626 r#"
627 struct Foo { x: () }
628 struct Bar { foo: Foo }
629 fn f(Bar { foo: Foo { ref x } }: Bar) {
630 let _ = *x;
631 }
632 "#,
633 )
634 }
635
636 #[test]
637 fn ref_mut_record_field() {
638 check_assist(
639 destructure_struct_binding,
640 r#"
641 struct Foo { x: () }
642 struct Bar { foo: Foo }
643 fn f(Bar { ref mut $0foo }: Bar) {
644 let _ = foo.x;
645 }
646 "#,
647 r#"
648 struct Foo { x: () }
649 struct Bar { foo: Foo }
650 fn f(Bar { foo: Foo { ref mut x } }: Bar) {
651 let _ = *x;
652 }
653 "#,
654 )
655 }
656
657 #[test]
658 fn ref_mut_record_renamed_field() {
659 check_assist(
660 destructure_struct_binding,
661 r#"
662 struct Foo { x: () }
663 struct Bar { foo: Foo }
664 fn f(Bar { foo: ref mut $0foo1 }: Bar) {
665 let _ = foo1.x;
666 }
667 "#,
668 r#"
669 struct Foo { x: () }
670 struct Bar { foo: Foo }
671 fn f(Bar { foo: Foo { ref mut x } }: Bar) {
672 let _ = *x;
673 }
674 "#,
675 )
676 }
677
678 #[test]
679 fn mut_ref() {
680 check_assist(
681 destructure_struct_binding,
682 r#"
683 struct Foo { bar: i32, baz: i32 }
684
685 fn main() {
686 let $0foo = &mut Foo { bar: 1, baz: 2 };
687 foo.bar = 5;
688 }
689 "#,
690 r#"
691 struct Foo { bar: i32, baz: i32 }
692
693 fn main() {
694 let Foo { bar, baz } = &mut Foo { bar: 1, baz: 2 };
695 *bar = 5;
696 }
697 "#,
698 )
699 }
700
701 #[test]
702 fn ref_not_add_parenthesis_and_deref_record() {
703 check_assist(
704 destructure_struct_binding,
705 r#"
706 struct Foo { bar: i32, baz: i32 }
707
708 fn main() {
709 let $0foo = &Foo { bar: 1, baz: 2 };
710 let _ = &foo.bar;
711 }
712 "#,
713 r#"
714 struct Foo { bar: i32, baz: i32 }
715
716 fn main() {
717 let Foo { bar, baz } = &Foo { bar: 1, baz: 2 };
718 let _ = bar;
719 }
720 "#,
721 )
722 }
723
724 #[test]
725 fn ref_not_add_parenthesis_and_deref_tuple() {
726 check_assist(
727 destructure_struct_binding,
728 r#"
729 struct Foo(i32, i32);
730
731 fn main() {
732 let $0foo = &Foo(1, 2);
733 let _ = &foo.0;
734 }
735 "#,
736 r#"
737 struct Foo(i32, i32);
738
739 fn main() {
740 let Foo(_0, _1) = &Foo(1, 2);
741 let _ = _0;
742 }
743 "#,
744 )
745 }
746
747 #[test]
748 fn record_struct_name_collision() {
749 check_assist(
750 destructure_struct_binding,
751 r#"
752 struct Foo { bar: i32, baz: i32 }
753
754 fn main(baz: i32) {
755 let bar = true;
756 let $0foo = Foo { bar: 1, baz: 2 };
757 let baz_1 = 7;
758 let bar_usage = foo.bar;
759 let baz_usage = foo.baz;
760 }
761 "#,
762 r#"
763 struct Foo { bar: i32, baz: i32 }
764
765 fn main(baz: i32) {
766 let bar = true;
767 let Foo { bar: bar_1, baz: baz_2 } = Foo { bar: 1, baz: 2 };
768 let baz_1 = 7;
769 let bar_usage = bar_1;
770 let baz_usage = baz_2;
771 }
772 "#,
773 )
774 }
775
776 #[test]
777 fn tuple_struct_name_collision() {
778 check_assist(
779 destructure_struct_binding,
780 r#"
781 struct Foo(i32, i32);
782
783 fn main() {
784 let _0 = true;
785 let $0foo = Foo(1, 2);
786 let bar = foo.0;
787 let baz = foo.1;
788 }
789 "#,
790 r#"
791 struct Foo(i32, i32);
792
793 fn main() {
794 let _0 = true;
795 let Foo(_0_1, _1) = Foo(1, 2);
796 let bar = _0_1;
797 let baz = _1;
798 }
799 "#,
800 )
801 }
802
803 #[test]
804 fn record_struct_name_collision_nested_scope() {
805 check_assist(
806 destructure_struct_binding,
807 r#"
808 struct Foo { bar: i32 }
809
810 fn main(foo: Foo) {
811 let bar = 5;
812
813 let new_bar = {
814 let $0foo2 = foo;
815 let bar_1 = 5;
816 foo2.bar
817 };
818 }
819 "#,
820 r#"
821 struct Foo { bar: i32 }
822
823 fn main(foo: Foo) {
824 let bar = 5;
825
826 let new_bar = {
827 let Foo { bar: bar_2 } = foo;
828 let bar_1 = 5;
829 bar_2
830 };
831 }
832 "#,
833 )
834 }
835
836 #[test]
837 fn record_struct_no_public_members() {
838 check_assist_not_applicable(
839 destructure_struct_binding,
840 r#"
841 //- /lib.rs crate:dep
842 pub struct Foo { bar: i32, baz: i32 };
843
844 //- /main.rs crate:main deps:dep
845 fn main($0foo: dep::Foo) {}
846 "#,
847 )
848 }
849}