Skip to main content

ide_assists/handlers/
merge_match_arms.rs

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
12// Assist: merge_match_arms
13//
14// Merges the current match arm with the following if their bodies are identical.
15//
16// ```
17// enum Action { Move { distance: u32 }, Stop }
18//
19// fn handle(action: Action) {
20//     match action {
21//         $0Action::Move(..) => foo(),
22//         Action::Stop => foo(),
23//     }
24// }
25// ```
26// ->
27// ```
28// enum Action { Move { distance: u32 }, Stop }
29//
30// fn handle(action: Action) {
31//     match action {
32//         Action::Move(..) | Action::Stop => foo(),
33//     }
34// }
35// ```
36pub(crate) fn merge_match_arms(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
37    let current_arm = ctx.find_node_at_trimmed_offset::<ast::MatchArm>()?;
38    // Don't try to handle arms with guards for now - can add support for this later
39    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, &current_arm);
45    let multi_arm_selection = !ctx.has_empty_selection()
46        && ctx.selection_trimmed().end() > current_arm.syntax().text_range().end();
47
48    // We check if the following match arms match this one. We could, but don't,
49    // compare to the previous match arm as well.
50    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                // don't include match arms that start after our selection
54                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(&current_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                        && ctx.sema.to_def(ident_pat).is_some()
165                    {
166                        let pat_type = ctx.sema.type_of_binding_in_pat(ident_pat);
167
168                        map.insert(name.text().to_string(), pat_type);
169                    }
170                }
171                _ => (),
172            }
173        }
174    }
175
176    recurse(&mut mapping, context, &arm.pat());
177    mapping
178}
179
180#[cfg(test)]
181mod tests {
182    use crate::tests::{check_assist, check_assist_not_applicable};
183
184    use super::*;
185
186    #[test]
187    fn merge_match_arms_single_patterns() {
188        check_assist(
189            merge_match_arms,
190            r#"
191#[derive(Debug)]
192enum X { A, B, C }
193
194fn main() {
195    let x = X::A;
196    let y = match x {
197        X::A => { 1i32$0 }
198        X::B => { 1i32 }
199        X::C => { 2i32 }
200    }
201}
202"#,
203            r#"
204#[derive(Debug)]
205enum X { A, B, C }
206
207fn main() {
208    let x = X::A;
209    let y = match x {
210        X::A | X::B => { 1i32 },
211        X::C => { 2i32 }
212    }
213}
214"#,
215        );
216    }
217
218    #[test]
219    fn merge_match_arms_ambiguous_ident_patterns() {
220        check_assist(
221            merge_match_arms,
222            r#"
223#[derive(Debug)]
224enum X { A, B, C }
225use X::*;
226
227fn main() {
228    let x = A;
229    let y = match x {
230        A => { 1i32$0 }
231        B => { 1i32 }
232        C => { 2i32 }
233    }
234}
235"#,
236            r#"
237#[derive(Debug)]
238enum X { A, B, C }
239use X::*;
240
241fn main() {
242    let x = A;
243    let y = match x {
244        A | B => { 1i32 },
245        C => { 2i32 }
246    }
247}
248"#,
249        );
250    }
251
252    #[test]
253    fn merge_match_arms_multiple_patterns() {
254        check_assist(
255            merge_match_arms,
256            r#"
257#[derive(Debug)]
258enum X { A, B, C, D, E }
259
260fn main() {
261    let x = X::A;
262    let y = match x {
263        X::A | X::B => {$0 1i32 },
264        X::C | X::D => { 1i32 },
265        X::E => { 2i32 },
266    }
267}
268"#,
269            r#"
270#[derive(Debug)]
271enum X { A, B, C, D, E }
272
273fn main() {
274    let x = X::A;
275    let y = match x {
276        X::A | X::B | X::C | X::D => { 1i32 },
277        X::E => { 2i32 },
278    }
279}
280"#,
281        );
282    }
283
284    #[test]
285    fn merge_match_arms_placeholder_pattern() {
286        check_assist(
287            merge_match_arms,
288            r#"
289#[derive(Debug)]
290enum X { A, B, C, D, E }
291
292fn main() {
293    let x = X::A;
294    let y = match x {
295        X::A => { 1i32 },
296        X::B => { 2i$032 },
297        _ => { 2i32 }
298    }
299}
300"#,
301            r#"
302#[derive(Debug)]
303enum X { A, B, C, D, E }
304
305fn main() {
306    let x = X::A;
307    let y = match x {
308        X::A => { 1i32 },
309        _ => { 2i32 },
310    }
311}
312"#,
313        );
314    }
315
316    #[test]
317    fn merges_all_subsequent_arms() {
318        check_assist(
319            merge_match_arms,
320            r#"
321enum X { A, B, C, D, E }
322
323fn main() {
324    match X::A {
325        X::A$0 => 92,
326        X::B => 92,
327        X::C => 92,
328        X::D => 62,
329        _ => panic!(),
330    }
331}
332"#,
333            r#"
334enum X { A, B, C, D, E }
335
336fn main() {
337    match X::A {
338        X::A | X::B | X::C => 92,
339        X::D => 62,
340        _ => panic!(),
341    }
342}
343"#,
344        )
345    }
346
347    #[test]
348    fn merge_match_arms_selection_has_leading_whitespace() {
349        check_assist(
350            merge_match_arms,
351            r#"
352#[derive(Debug)]
353enum X { A, B, C }
354
355fn main() {
356    match X::A {
357    $0    X::A => 0,
358        X::B => 0,$0
359        X::C => 1,
360    }
361}
362"#,
363            r#"
364#[derive(Debug)]
365enum X { A, B, C }
366
367fn main() {
368    match X::A {
369        X::A | X::B => 0,
370        X::C => 1,
371    }
372}
373"#,
374        );
375    }
376
377    #[test]
378    fn merge_match_arms_stops_at_end_of_selection() {
379        check_assist(
380            merge_match_arms,
381            r#"
382#[derive(Debug)]
383enum X { A, B, C }
384
385fn main() {
386    match X::A {
387    $0    X::A => 0,
388        X::B => 0,
389        $0X::C => 0,
390    }
391}
392"#,
393            r#"
394#[derive(Debug)]
395enum X { A, B, C }
396
397fn main() {
398    match X::A {
399        X::A | X::B => 0,
400        X::C => 0,
401    }
402}
403"#,
404        );
405    }
406
407    #[test]
408    fn merge_match_arms_works_despite_accidental_selection() {
409        check_assist(
410            merge_match_arms,
411            r#"
412#[derive(Debug)]
413enum X { A, B, C }
414
415fn main() {
416    match X::A {
417        X::$0A$0 => 0,
418        X::B => 0,
419        X::C => 1,
420    }
421}
422"#,
423            r#"
424#[derive(Debug)]
425enum X { A, B, C }
426
427fn main() {
428    match X::A {
429        X::A | X::B => 0,
430        X::C => 1,
431    }
432}
433"#,
434        );
435    }
436
437    #[test]
438    fn merge_match_arms_rejects_guards() {
439        check_assist_not_applicable(
440            merge_match_arms,
441            r#"
442#[derive(Debug)]
443enum X {
444    A(i32),
445    B,
446    C
447}
448
449fn main() {
450    let x = X::A;
451    let y = match x {
452        X::A(a) if a > 5 => { $01i32 },
453        X::B => { 1i32 },
454        X::C => { 2i32 }
455    }
456}
457"#,
458        );
459    }
460
461    #[test]
462    fn merge_match_arms_different_type() {
463        check_assist_not_applicable(
464            merge_match_arms,
465            r#"
466//- minicore: result
467fn func() {
468    match Result::<f64, f32>::Ok(0f64) {
469        Ok(x) => $0x.classify(),
470        Err(x) => x.classify()
471    };
472}
473"#,
474        );
475    }
476
477    #[test]
478    fn merge_match_arms_different_type_multiple_fields() {
479        check_assist_not_applicable(
480            merge_match_arms,
481            r#"
482//- minicore: result
483fn func() {
484    match Result::<(f64, f64), (f32, f32)>::Ok((0f64, 0f64)) {
485        Ok(x) => $0x.1.classify(),
486        Err(x) => x.1.classify()
487    };
488}
489"#,
490        );
491    }
492
493    #[test]
494    fn merge_match_arms_same_type_multiple_fields() {
495        check_assist(
496            merge_match_arms,
497            r#"
498//- minicore: result
499fn func() {
500    match Result::<(f64, f64), (f64, f64)>::Ok((0f64, 0f64)) {
501        Ok(x) => $0x.1.classify(),
502        Err(x) => x.1.classify()
503    };
504}
505"#,
506            r#"
507fn func() {
508    match Result::<(f64, f64), (f64, f64)>::Ok((0f64, 0f64)) {
509        Ok(x) | Err(x) => x.1.classify(),
510    };
511}
512"#,
513        );
514    }
515
516    #[test]
517    fn merge_match_arms_same_type_subsequent_arm_with_different_type_in_other() {
518        check_assist(
519            merge_match_arms,
520            r#"
521enum MyEnum {
522    OptionA(f32),
523    OptionB(f32),
524    OptionC(f64)
525}
526
527fn func(e: MyEnum) {
528    match e {
529        MyEnum::OptionA(x) => $0x.classify(),
530        MyEnum::OptionB(x) => x.classify(),
531        MyEnum::OptionC(x) => x.classify(),
532    };
533}
534"#,
535            r#"
536enum MyEnum {
537    OptionA(f32),
538    OptionB(f32),
539    OptionC(f64)
540}
541
542fn func(e: MyEnum) {
543    match e {
544        MyEnum::OptionA(x) | MyEnum::OptionB(x) => x.classify(),
545        MyEnum::OptionC(x) => x.classify(),
546    };
547}
548"#,
549        );
550    }
551
552    #[test]
553    fn merge_match_arms_same_type_skip_arm_with_different_type_in_between() {
554        check_assist_not_applicable(
555            merge_match_arms,
556            r#"
557enum MyEnum {
558    OptionA(f32),
559    OptionB(f64),
560    OptionC(f32)
561}
562
563fn func(e: MyEnum) {
564    match e {
565        MyEnum::OptionA(x) => $0x.classify(),
566        MyEnum::OptionB(x) => x.classify(),
567        MyEnum::OptionC(x) => x.classify(),
568    };
569}
570"#,
571        );
572    }
573
574    #[test]
575    fn merge_match_arms_same_type_different_number_of_fields() {
576        check_assist_not_applicable(
577            merge_match_arms,
578            r#"
579//- minicore: result
580fn func() {
581    match Result::<(f64, f64, f64), (f64, f64)>::Ok((0f64, 0f64, 0f64)) {
582        Ok(x) => $0x.1.classify(),
583        Err(x) => x.1.classify()
584    };
585}
586"#,
587        );
588    }
589
590    #[test]
591    fn merge_match_same_destructuring_different_types() {
592        check_assist_not_applicable(
593            merge_match_arms,
594            r#"
595struct Point {
596    x: i32,
597    y: i32,
598}
599
600fn func() {
601    let p = Point { x: 0, y: 7 };
602
603    match p {
604        Point { x, y: 0 } => $0"",
605        Point { x: 0, y } => "",
606        Point { x, y } => "",
607    };
608}
609"#,
610        );
611    }
612
613    #[test]
614    fn merge_match_arms_range() {
615        check_assist(
616            merge_match_arms,
617            r#"
618fn func() {
619    let x = 'c';
620
621    match x {
622        'a'..='j' => $0"",
623        'c'..='z' => "",
624        _ => "other",
625    };
626}
627"#,
628            r#"
629fn func() {
630    let x = 'c';
631
632    match x {
633        'a'..='j' | 'c'..='z' => "",
634        _ => "other",
635    };
636}
637"#,
638        );
639    }
640
641    #[test]
642    fn merge_match_arms_enum_without_field() {
643        check_assist_not_applicable(
644            merge_match_arms,
645            r#"
646enum MyEnum {
647    NoField,
648    AField(u8)
649}
650
651fn func(x: MyEnum) {
652    match x {
653        MyEnum::NoField => $0"",
654        MyEnum::AField(x) => ""
655    };
656}
657        "#,
658        )
659    }
660
661    #[test]
662    fn merge_match_arms_enum_destructuring_different_types() {
663        check_assist_not_applicable(
664            merge_match_arms,
665            r#"
666enum MyEnum {
667    Move { x: i32, y: i32 },
668    Write(String),
669}
670
671fn func(x: MyEnum) {
672    match x {
673        MyEnum::Move { x, y } => $0"",
674        MyEnum::Write(text) => "",
675    };
676}
677        "#,
678        )
679    }
680
681    #[test]
682    fn merge_match_arms_enum_destructuring_same_types() {
683        check_assist(
684            merge_match_arms,
685            r#"
686enum MyEnum {
687    Move { x: i32, y: i32 },
688    Crawl { x: i32, y: i32 }
689}
690
691fn func(x: MyEnum) {
692    match x {
693        MyEnum::Move { x, y } => $0"",
694        MyEnum::Crawl { x, y } => "",
695    };
696}
697        "#,
698            r#"
699enum MyEnum {
700    Move { x: i32, y: i32 },
701    Crawl { x: i32, y: i32 }
702}
703
704fn func(x: MyEnum) {
705    match x {
706        MyEnum::Move { x, y } | MyEnum::Crawl { x, y } => "",
707    };
708}
709        "#,
710        )
711    }
712
713    #[test]
714    fn merge_match_arms_enum_destructuring_same_types_different_name() {
715        check_assist_not_applicable(
716            merge_match_arms,
717            r#"
718enum MyEnum {
719    Move { x: i32, y: i32 },
720    Crawl { a: i32, b: i32 }
721}
722
723fn func(x: MyEnum) {
724    match x {
725        MyEnum::Move { x, y } => $0"",
726        MyEnum::Crawl { a, b } => "",
727    };
728}
729        "#,
730        )
731    }
732
733    #[test]
734    fn merge_match_arms_enum_nested_pattern_different_names() {
735        check_assist_not_applicable(
736            merge_match_arms,
737            r#"
738enum Color {
739    Rgb(i32, i32, i32),
740    Hsv(i32, i32, i32),
741}
742
743enum Message {
744    Quit,
745    Move { x: i32, y: i32 },
746    Write(String),
747    ChangeColor(Color),
748}
749
750fn main(msg: Message) {
751    match msg {
752        Message::ChangeColor(Color::Rgb(r, g, b)) => $0"",
753        Message::ChangeColor(Color::Hsv(h, s, v)) => "",
754        _ => "other"
755    };
756}
757        "#,
758        )
759    }
760
761    #[test]
762    fn merge_match_arms_enum_nested_pattern_same_names() {
763        check_assist(
764            merge_match_arms,
765            r#"
766enum Color {
767    Rgb(i32, i32, i32),
768    Hsv(i32, i32, i32),
769}
770
771enum Message {
772    Quit,
773    Move { x: i32, y: i32 },
774    Write(String),
775    ChangeColor(Color),
776}
777
778fn main(msg: Message) {
779    match msg {
780        Message::ChangeColor(Color::Rgb(a, b, c)) => $0"",
781        Message::ChangeColor(Color::Hsv(a, b, c)) => "",
782        _ => "other"
783    };
784}
785        "#,
786            r#"
787enum Color {
788    Rgb(i32, i32, i32),
789    Hsv(i32, i32, i32),
790}
791
792enum Message {
793    Quit,
794    Move { x: i32, y: i32 },
795    Write(String),
796    ChangeColor(Color),
797}
798
799fn main(msg: Message) {
800    match msg {
801        Message::ChangeColor(Color::Rgb(a, b, c)) | Message::ChangeColor(Color::Hsv(a, b, c)) => "",
802        _ => "other"
803    };
804}
805        "#,
806        )
807    }
808
809    #[test]
810    fn merge_match_arms_enum_destructuring_with_ignore() {
811        check_assist(
812            merge_match_arms,
813            r#"
814enum MyEnum {
815    Move { x: i32, a: i32 },
816    Crawl { x: i32, b: i32 }
817}
818
819fn func(x: MyEnum) {
820    match x {
821        MyEnum::Move { x, .. } => $0"",
822        MyEnum::Crawl { x, .. } => "",
823    };
824}
825        "#,
826            r#"
827enum MyEnum {
828    Move { x: i32, a: i32 },
829    Crawl { x: i32, b: i32 }
830}
831
832fn func(x: MyEnum) {
833    match x {
834        MyEnum::Move { x, .. } | MyEnum::Crawl { x, .. } => "",
835    };
836}
837        "#,
838        )
839    }
840
841    #[test]
842    fn merge_match_arms_nested_with_conflicting_identifier() {
843        check_assist_not_applicable(
844            merge_match_arms,
845            r#"
846enum Color {
847    Rgb(i32, i32, i32),
848    Hsv(i32, i32, i32),
849}
850
851enum Message {
852    Move { x: i32, y: i32 },
853    ChangeColor(u8, Color),
854}
855
856fn main(msg: Message) {
857    match msg {
858        Message::ChangeColor(x, Color::Rgb(y, b, c)) => $0"",
859        Message::ChangeColor(y, Color::Hsv(x, b, c)) => "",
860        _ => "other"
861    };
862}
863        "#,
864        )
865    }
866
867    #[test]
868    fn merge_match_arms_tuple() {
869        check_assist_not_applicable(
870            merge_match_arms,
871            r#"
872fn func() {
873    match (0, "boo") {
874        (x, y) => $0"",
875        (y, x) => "",
876    };
877}
878        "#,
879        )
880    }
881
882    #[test]
883    fn merge_match_arms_parentheses() {
884        check_assist_not_applicable(
885            merge_match_arms,
886            r#"
887fn func(x: i32) {
888    let variable = 2;
889    match x {
890        1 => $0"",
891        ((((variable)))) => "",
892        _ => "other"
893    };
894}
895        "#,
896        )
897    }
898
899    #[test]
900    fn merge_match_arms_refpat() {
901        check_assist_not_applicable(
902            merge_match_arms,
903            r#"
904fn func() {
905    let name = Some(String::from(""));
906    let n = String::from("");
907    match name {
908        Some(ref n) => $0"",
909        Some(n) => "",
910        _ => "other",
911    };
912}
913        "#,
914        )
915    }
916
917    #[test]
918    fn merge_match_arms_slice() {
919        check_assist_not_applicable(
920            merge_match_arms,
921            r#"
922fn func(binary: &[u8]) {
923    let space = b' ';
924    match binary {
925        [0x7f, b'E', b'L', b'F', ..] => $0"",
926        [space] => "",
927        _ => "other",
928    };
929}
930        "#,
931        )
932    }
933
934    #[test]
935    fn merge_match_arms_slice_identical() {
936        check_assist(
937            merge_match_arms,
938            r#"
939fn func(binary: &[u8]) {
940    let space = b' ';
941    match binary {
942        [space, 5u8] => $0"",
943        [space] => "",
944        _ => "other",
945    };
946}
947        "#,
948            r#"
949fn func(binary: &[u8]) {
950    let space = b' ';
951    match binary {
952        [space, 5u8] | [space] => "",
953        _ => "other",
954    };
955}
956        "#,
957        )
958    }
959}