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                        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}