1use hir::Type;
2use ide_db::FxHashMap;
3use std::iter::successors;
4use syntax::{
5 Direction,
6 algo::neighbor,
7 ast::{self, AstNode, HasName},
8};
9
10use crate::{AssistContext, AssistId, Assists, TextRange};
11
12pub(crate) fn merge_match_arms(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
37 let current_arm = ctx.find_node_at_trimmed_offset::<ast::MatchArm>()?;
38 if current_arm.guard().is_some() {
40 return None;
41 }
42 let current_expr = current_arm.expr()?;
43 let current_text_range = current_arm.syntax().text_range();
44 let current_arm_types = get_arm_types(ctx, ¤t_arm);
45 let multi_arm_selection = !ctx.has_empty_selection()
46 && ctx.selection_trimmed().end() > current_arm.syntax().text_range().end();
47
48 let arms_to_merge = successors(Some(current_arm), |it| neighbor(it, Direction::Next))
51 .take_while(|arm| match arm.expr() {
52 Some(expr) if arm.guard().is_none() => {
53 if multi_arm_selection
55 && arm.syntax().text_range().start() >= ctx.selection_trimmed().end()
56 {
57 return false;
58 }
59
60 let same_text = expr.syntax().text() == current_expr.syntax().text();
61 if !same_text {
62 return false;
63 }
64
65 are_same_types(¤t_arm_types, arm, ctx)
66 }
67 _ => false,
68 })
69 .collect::<Vec<_>>();
70
71 if arms_to_merge.len() <= 1 {
72 return None;
73 }
74
75 acc.add(
76 AssistId::refactor_rewrite("merge_match_arms"),
77 "Merge match arms",
78 current_text_range,
79 |edit| {
80 let pats = if arms_to_merge.iter().any(contains_placeholder) {
81 "_".into()
82 } else {
83 arms_to_merge
84 .iter()
85 .filter_map(ast::MatchArm::pat)
86 .map(|x| x.syntax().to_string())
87 .collect::<Vec<String>>()
88 .join(" | ")
89 };
90
91 let arm = format!("{pats} => {current_expr},");
92
93 if let [first, .., last] = &*arms_to_merge {
94 let start = first.syntax().text_range().start();
95 let end = last.syntax().text_range().end();
96
97 edit.replace(TextRange::new(start, end), arm);
98 }
99 },
100 )
101}
102
103fn contains_placeholder(a: &ast::MatchArm) -> bool {
104 matches!(a.pat(), Some(ast::Pat::WildcardPat(..)))
105}
106
107fn are_same_types(
108 current_arm_types: &FxHashMap<String, Option<Type<'_>>>,
109 arm: &ast::MatchArm,
110 ctx: &AssistContext<'_>,
111) -> bool {
112 let arm_types = get_arm_types(ctx, arm);
113 for (other_arm_type_name, other_arm_type) in arm_types {
114 match (current_arm_types.get(&other_arm_type_name), other_arm_type) {
115 (Some(Some(current_arm_type)), Some(other_arm_type))
116 if other_arm_type == *current_arm_type => {}
117 _ => return false,
118 }
119 }
120
121 true
122}
123
124fn get_arm_types<'db>(
125 context: &AssistContext<'db>,
126 arm: &ast::MatchArm,
127) -> FxHashMap<String, Option<Type<'db>>> {
128 let mut mapping: FxHashMap<String, Option<Type<'db>>> = FxHashMap::default();
129
130 fn recurse<'db>(
131 map: &mut FxHashMap<String, Option<Type<'db>>>,
132 ctx: &AssistContext<'db>,
133 pat: &Option<ast::Pat>,
134 ) {
135 if let Some(local_pat) = pat {
136 match local_pat {
137 ast::Pat::TupleStructPat(tuple) => {
138 for field in tuple.fields() {
139 recurse(map, ctx, &Some(field));
140 }
141 }
142 ast::Pat::TuplePat(tuple) => {
143 for field in tuple.fields() {
144 recurse(map, ctx, &Some(field));
145 }
146 }
147 ast::Pat::RecordPat(record) => {
148 if let Some(field_list) = record.record_pat_field_list() {
149 for field in field_list.fields() {
150 recurse(map, ctx, &field.pat());
151 }
152 }
153 }
154 ast::Pat::ParenPat(parentheses) => {
155 recurse(map, ctx, &parentheses.pat());
156 }
157 ast::Pat::SlicePat(slice) => {
158 for slice_pat in slice.pats() {
159 recurse(map, ctx, &Some(slice_pat));
160 }
161 }
162 ast::Pat::IdentPat(ident_pat) => {
163 if let Some(name) = ident_pat.name() {
164 let pat_type = ctx.sema.type_of_binding_in_pat(ident_pat);
165 map.insert(name.text().to_string(), pat_type);
166 }
167 }
168 _ => (),
169 }
170 }
171 }
172
173 recurse(&mut mapping, context, &arm.pat());
174 mapping
175}
176
177#[cfg(test)]
178mod tests {
179 use crate::tests::{check_assist, check_assist_not_applicable};
180
181 use super::*;
182
183 #[test]
184 fn merge_match_arms_single_patterns() {
185 check_assist(
186 merge_match_arms,
187 r#"
188#[derive(Debug)]
189enum X { A, B, C }
190
191fn main() {
192 let x = X::A;
193 let y = match x {
194 X::A => { 1i32$0 }
195 X::B => { 1i32 }
196 X::C => { 2i32 }
197 }
198}
199"#,
200 r#"
201#[derive(Debug)]
202enum X { A, B, C }
203
204fn main() {
205 let x = X::A;
206 let y = match x {
207 X::A | X::B => { 1i32 },
208 X::C => { 2i32 }
209 }
210}
211"#,
212 );
213 }
214
215 #[test]
216 fn merge_match_arms_multiple_patterns() {
217 check_assist(
218 merge_match_arms,
219 r#"
220#[derive(Debug)]
221enum X { A, B, C, D, E }
222
223fn main() {
224 let x = X::A;
225 let y = match x {
226 X::A | X::B => {$0 1i32 },
227 X::C | X::D => { 1i32 },
228 X::E => { 2i32 },
229 }
230}
231"#,
232 r#"
233#[derive(Debug)]
234enum X { A, B, C, D, E }
235
236fn main() {
237 let x = X::A;
238 let y = match x {
239 X::A | X::B | X::C | X::D => { 1i32 },
240 X::E => { 2i32 },
241 }
242}
243"#,
244 );
245 }
246
247 #[test]
248 fn merge_match_arms_placeholder_pattern() {
249 check_assist(
250 merge_match_arms,
251 r#"
252#[derive(Debug)]
253enum X { A, B, C, D, E }
254
255fn main() {
256 let x = X::A;
257 let y = match x {
258 X::A => { 1i32 },
259 X::B => { 2i$032 },
260 _ => { 2i32 }
261 }
262}
263"#,
264 r#"
265#[derive(Debug)]
266enum X { A, B, C, D, E }
267
268fn main() {
269 let x = X::A;
270 let y = match x {
271 X::A => { 1i32 },
272 _ => { 2i32 },
273 }
274}
275"#,
276 );
277 }
278
279 #[test]
280 fn merges_all_subsequent_arms() {
281 check_assist(
282 merge_match_arms,
283 r#"
284enum X { A, B, C, D, E }
285
286fn main() {
287 match X::A {
288 X::A$0 => 92,
289 X::B => 92,
290 X::C => 92,
291 X::D => 62,
292 _ => panic!(),
293 }
294}
295"#,
296 r#"
297enum X { A, B, C, D, E }
298
299fn main() {
300 match X::A {
301 X::A | X::B | X::C => 92,
302 X::D => 62,
303 _ => panic!(),
304 }
305}
306"#,
307 )
308 }
309
310 #[test]
311 fn merge_match_arms_selection_has_leading_whitespace() {
312 check_assist(
313 merge_match_arms,
314 r#"
315#[derive(Debug)]
316enum X { A, B, C }
317
318fn main() {
319 match X::A {
320 $0 X::A => 0,
321 X::B => 0,$0
322 X::C => 1,
323 }
324}
325"#,
326 r#"
327#[derive(Debug)]
328enum X { A, B, C }
329
330fn main() {
331 match X::A {
332 X::A | X::B => 0,
333 X::C => 1,
334 }
335}
336"#,
337 );
338 }
339
340 #[test]
341 fn merge_match_arms_stops_at_end_of_selection() {
342 check_assist(
343 merge_match_arms,
344 r#"
345#[derive(Debug)]
346enum X { A, B, C }
347
348fn main() {
349 match X::A {
350 $0 X::A => 0,
351 X::B => 0,
352 $0X::C => 0,
353 }
354}
355"#,
356 r#"
357#[derive(Debug)]
358enum X { A, B, C }
359
360fn main() {
361 match X::A {
362 X::A | X::B => 0,
363 X::C => 0,
364 }
365}
366"#,
367 );
368 }
369
370 #[test]
371 fn merge_match_arms_works_despite_accidental_selection() {
372 check_assist(
373 merge_match_arms,
374 r#"
375#[derive(Debug)]
376enum X { A, B, C }
377
378fn main() {
379 match X::A {
380 X::$0A$0 => 0,
381 X::B => 0,
382 X::C => 1,
383 }
384}
385"#,
386 r#"
387#[derive(Debug)]
388enum X { A, B, C }
389
390fn main() {
391 match X::A {
392 X::A | X::B => 0,
393 X::C => 1,
394 }
395}
396"#,
397 );
398 }
399
400 #[test]
401 fn merge_match_arms_rejects_guards() {
402 check_assist_not_applicable(
403 merge_match_arms,
404 r#"
405#[derive(Debug)]
406enum X {
407 A(i32),
408 B,
409 C
410}
411
412fn main() {
413 let x = X::A;
414 let y = match x {
415 X::A(a) if a > 5 => { $01i32 },
416 X::B => { 1i32 },
417 X::C => { 2i32 }
418 }
419}
420"#,
421 );
422 }
423
424 #[test]
425 fn merge_match_arms_different_type() {
426 check_assist_not_applicable(
427 merge_match_arms,
428 r#"
429//- minicore: result
430fn func() {
431 match Result::<f64, f32>::Ok(0f64) {
432 Ok(x) => $0x.classify(),
433 Err(x) => x.classify()
434 };
435}
436"#,
437 );
438 }
439
440 #[test]
441 fn merge_match_arms_different_type_multiple_fields() {
442 check_assist_not_applicable(
443 merge_match_arms,
444 r#"
445//- minicore: result
446fn func() {
447 match Result::<(f64, f64), (f32, f32)>::Ok((0f64, 0f64)) {
448 Ok(x) => $0x.1.classify(),
449 Err(x) => x.1.classify()
450 };
451}
452"#,
453 );
454 }
455
456 #[test]
457 fn merge_match_arms_same_type_multiple_fields() {
458 check_assist(
459 merge_match_arms,
460 r#"
461//- minicore: result
462fn func() {
463 match Result::<(f64, f64), (f64, f64)>::Ok((0f64, 0f64)) {
464 Ok(x) => $0x.1.classify(),
465 Err(x) => x.1.classify()
466 };
467}
468"#,
469 r#"
470fn func() {
471 match Result::<(f64, f64), (f64, f64)>::Ok((0f64, 0f64)) {
472 Ok(x) | Err(x) => x.1.classify(),
473 };
474}
475"#,
476 );
477 }
478
479 #[test]
480 fn merge_match_arms_same_type_subsequent_arm_with_different_type_in_other() {
481 check_assist(
482 merge_match_arms,
483 r#"
484enum MyEnum {
485 OptionA(f32),
486 OptionB(f32),
487 OptionC(f64)
488}
489
490fn func(e: MyEnum) {
491 match e {
492 MyEnum::OptionA(x) => $0x.classify(),
493 MyEnum::OptionB(x) => x.classify(),
494 MyEnum::OptionC(x) => x.classify(),
495 };
496}
497"#,
498 r#"
499enum MyEnum {
500 OptionA(f32),
501 OptionB(f32),
502 OptionC(f64)
503}
504
505fn func(e: MyEnum) {
506 match e {
507 MyEnum::OptionA(x) | MyEnum::OptionB(x) => x.classify(),
508 MyEnum::OptionC(x) => x.classify(),
509 };
510}
511"#,
512 );
513 }
514
515 #[test]
516 fn merge_match_arms_same_type_skip_arm_with_different_type_in_between() {
517 check_assist_not_applicable(
518 merge_match_arms,
519 r#"
520enum MyEnum {
521 OptionA(f32),
522 OptionB(f64),
523 OptionC(f32)
524}
525
526fn func(e: MyEnum) {
527 match e {
528 MyEnum::OptionA(x) => $0x.classify(),
529 MyEnum::OptionB(x) => x.classify(),
530 MyEnum::OptionC(x) => x.classify(),
531 };
532}
533"#,
534 );
535 }
536
537 #[test]
538 fn merge_match_arms_same_type_different_number_of_fields() {
539 check_assist_not_applicable(
540 merge_match_arms,
541 r#"
542//- minicore: result
543fn func() {
544 match Result::<(f64, f64, f64), (f64, f64)>::Ok((0f64, 0f64, 0f64)) {
545 Ok(x) => $0x.1.classify(),
546 Err(x) => x.1.classify()
547 };
548}
549"#,
550 );
551 }
552
553 #[test]
554 fn merge_match_same_destructuring_different_types() {
555 check_assist_not_applicable(
556 merge_match_arms,
557 r#"
558struct Point {
559 x: i32,
560 y: i32,
561}
562
563fn func() {
564 let p = Point { x: 0, y: 7 };
565
566 match p {
567 Point { x, y: 0 } => $0"",
568 Point { x: 0, y } => "",
569 Point { x, y } => "",
570 };
571}
572"#,
573 );
574 }
575
576 #[test]
577 fn merge_match_arms_range() {
578 check_assist(
579 merge_match_arms,
580 r#"
581fn func() {
582 let x = 'c';
583
584 match x {
585 'a'..='j' => $0"",
586 'c'..='z' => "",
587 _ => "other",
588 };
589}
590"#,
591 r#"
592fn func() {
593 let x = 'c';
594
595 match x {
596 'a'..='j' | 'c'..='z' => "",
597 _ => "other",
598 };
599}
600"#,
601 );
602 }
603
604 #[test]
605 fn merge_match_arms_enum_without_field() {
606 check_assist_not_applicable(
607 merge_match_arms,
608 r#"
609enum MyEnum {
610 NoField,
611 AField(u8)
612}
613
614fn func(x: MyEnum) {
615 match x {
616 MyEnum::NoField => $0"",
617 MyEnum::AField(x) => ""
618 };
619}
620 "#,
621 )
622 }
623
624 #[test]
625 fn merge_match_arms_enum_destructuring_different_types() {
626 check_assist_not_applicable(
627 merge_match_arms,
628 r#"
629enum MyEnum {
630 Move { x: i32, y: i32 },
631 Write(String),
632}
633
634fn func(x: MyEnum) {
635 match x {
636 MyEnum::Move { x, y } => $0"",
637 MyEnum::Write(text) => "",
638 };
639}
640 "#,
641 )
642 }
643
644 #[test]
645 fn merge_match_arms_enum_destructuring_same_types() {
646 check_assist(
647 merge_match_arms,
648 r#"
649enum MyEnum {
650 Move { x: i32, y: i32 },
651 Crawl { x: i32, y: i32 }
652}
653
654fn func(x: MyEnum) {
655 match x {
656 MyEnum::Move { x, y } => $0"",
657 MyEnum::Crawl { x, y } => "",
658 };
659}
660 "#,
661 r#"
662enum MyEnum {
663 Move { x: i32, y: i32 },
664 Crawl { x: i32, y: i32 }
665}
666
667fn func(x: MyEnum) {
668 match x {
669 MyEnum::Move { x, y } | MyEnum::Crawl { x, y } => "",
670 };
671}
672 "#,
673 )
674 }
675
676 #[test]
677 fn merge_match_arms_enum_destructuring_same_types_different_name() {
678 check_assist_not_applicable(
679 merge_match_arms,
680 r#"
681enum MyEnum {
682 Move { x: i32, y: i32 },
683 Crawl { a: i32, b: i32 }
684}
685
686fn func(x: MyEnum) {
687 match x {
688 MyEnum::Move { x, y } => $0"",
689 MyEnum::Crawl { a, b } => "",
690 };
691}
692 "#,
693 )
694 }
695
696 #[test]
697 fn merge_match_arms_enum_nested_pattern_different_names() {
698 check_assist_not_applicable(
699 merge_match_arms,
700 r#"
701enum Color {
702 Rgb(i32, i32, i32),
703 Hsv(i32, i32, i32),
704}
705
706enum Message {
707 Quit,
708 Move { x: i32, y: i32 },
709 Write(String),
710 ChangeColor(Color),
711}
712
713fn main(msg: Message) {
714 match msg {
715 Message::ChangeColor(Color::Rgb(r, g, b)) => $0"",
716 Message::ChangeColor(Color::Hsv(h, s, v)) => "",
717 _ => "other"
718 };
719}
720 "#,
721 )
722 }
723
724 #[test]
725 fn merge_match_arms_enum_nested_pattern_same_names() {
726 check_assist(
727 merge_match_arms,
728 r#"
729enum Color {
730 Rgb(i32, i32, i32),
731 Hsv(i32, i32, i32),
732}
733
734enum Message {
735 Quit,
736 Move { x: i32, y: i32 },
737 Write(String),
738 ChangeColor(Color),
739}
740
741fn main(msg: Message) {
742 match msg {
743 Message::ChangeColor(Color::Rgb(a, b, c)) => $0"",
744 Message::ChangeColor(Color::Hsv(a, b, c)) => "",
745 _ => "other"
746 };
747}
748 "#,
749 r#"
750enum Color {
751 Rgb(i32, i32, i32),
752 Hsv(i32, i32, i32),
753}
754
755enum Message {
756 Quit,
757 Move { x: i32, y: i32 },
758 Write(String),
759 ChangeColor(Color),
760}
761
762fn main(msg: Message) {
763 match msg {
764 Message::ChangeColor(Color::Rgb(a, b, c)) | Message::ChangeColor(Color::Hsv(a, b, c)) => "",
765 _ => "other"
766 };
767}
768 "#,
769 )
770 }
771
772 #[test]
773 fn merge_match_arms_enum_destructuring_with_ignore() {
774 check_assist(
775 merge_match_arms,
776 r#"
777enum MyEnum {
778 Move { x: i32, a: i32 },
779 Crawl { x: i32, b: i32 }
780}
781
782fn func(x: MyEnum) {
783 match x {
784 MyEnum::Move { x, .. } => $0"",
785 MyEnum::Crawl { x, .. } => "",
786 };
787}
788 "#,
789 r#"
790enum MyEnum {
791 Move { x: i32, a: i32 },
792 Crawl { x: i32, b: i32 }
793}
794
795fn func(x: MyEnum) {
796 match x {
797 MyEnum::Move { x, .. } | MyEnum::Crawl { x, .. } => "",
798 };
799}
800 "#,
801 )
802 }
803
804 #[test]
805 fn merge_match_arms_nested_with_conflicting_identifier() {
806 check_assist_not_applicable(
807 merge_match_arms,
808 r#"
809enum Color {
810 Rgb(i32, i32, i32),
811 Hsv(i32, i32, i32),
812}
813
814enum Message {
815 Move { x: i32, y: i32 },
816 ChangeColor(u8, Color),
817}
818
819fn main(msg: Message) {
820 match msg {
821 Message::ChangeColor(x, Color::Rgb(y, b, c)) => $0"",
822 Message::ChangeColor(y, Color::Hsv(x, b, c)) => "",
823 _ => "other"
824 };
825}
826 "#,
827 )
828 }
829
830 #[test]
831 fn merge_match_arms_tuple() {
832 check_assist_not_applicable(
833 merge_match_arms,
834 r#"
835fn func() {
836 match (0, "boo") {
837 (x, y) => $0"",
838 (y, x) => "",
839 };
840}
841 "#,
842 )
843 }
844
845 #[test]
846 fn merge_match_arms_parentheses() {
847 check_assist_not_applicable(
848 merge_match_arms,
849 r#"
850fn func(x: i32) {
851 let variable = 2;
852 match x {
853 1 => $0"",
854 ((((variable)))) => "",
855 _ => "other"
856 };
857}
858 "#,
859 )
860 }
861
862 #[test]
863 fn merge_match_arms_refpat() {
864 check_assist_not_applicable(
865 merge_match_arms,
866 r#"
867fn func() {
868 let name = Some(String::from(""));
869 let n = String::from("");
870 match name {
871 Some(ref n) => $0"",
872 Some(n) => "",
873 _ => "other",
874 };
875}
876 "#,
877 )
878 }
879
880 #[test]
881 fn merge_match_arms_slice() {
882 check_assist_not_applicable(
883 merge_match_arms,
884 r#"
885fn func(binary: &[u8]) {
886 let space = b' ';
887 match binary {
888 [0x7f, b'E', b'L', b'F', ..] => $0"",
889 [space] => "",
890 _ => "other",
891 };
892}
893 "#,
894 )
895 }
896
897 #[test]
898 fn merge_match_arms_slice_identical() {
899 check_assist(
900 merge_match_arms,
901 r#"
902fn func(binary: &[u8]) {
903 let space = b' ';
904 match binary {
905 [space, 5u8] => $0"",
906 [space] => "",
907 _ => "other",
908 };
909}
910 "#,
911 r#"
912fn func(binary: &[u8]) {
913 let space = b' ';
914 match binary {
915 [space, 5u8] | [space] => "",
916 _ => "other",
917 };
918}
919 "#,
920 )
921 }
922}