rust/crates/ide_assists/src/handlers/merge_match_arms.rs

795 lines
16 KiB
Rust
Raw Normal View History

2022-01-11 15:39:50 -06:00
use hir::TypeInfo;
2022-01-11 15:40:57 -06:00
use std::{collections::HashMap, iter::successors};
2020-08-12 11:26:51 -05:00
use syntax::{
2020-03-19 05:38:26 -05:00
algo::neighbor,
2022-01-11 15:40:57 -06:00
ast::{self, AstNode, HasName, MatchArm, Pat},
2020-05-20 16:14:31 -05:00
Direction,
2020-02-05 04:53:33 -06:00
};
2020-02-05 04:46:05 -06:00
2020-06-28 17:36:05 -05:00
use crate::{AssistContext, AssistId, AssistKind, Assists, TextRange};
2019-10-27 03:26:46 -05:00
// Assist: merge_match_arms
//
2021-07-01 14:10:45 -05:00
// Merges the current match arm with the following if their bodies are identical.
2019-10-27 03:26:46 -05:00
//
// ```
// enum Action { Move { distance: u32 }, Stop }
//
// fn handle(action: Action) {
// match action {
2021-01-06 14:15:48 -06:00
// $0Action::Move(..) => foo(),
2019-10-27 03:26:46 -05:00
// Action::Stop => foo(),
// }
// }
// ```
// ->
// ```
// enum Action { Move { distance: u32 }, Stop }
//
// fn handle(action: Action) {
// match action {
// Action::Move(..) | Action::Stop => foo(),
// }
// }
// ```
pub(crate) fn merge_match_arms(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
2020-02-05 04:46:05 -06:00
let current_arm = ctx.find_node_at_offset::<ast::MatchArm>()?;
// Don't try to handle arms with guards for now - can add support for this later
2020-02-05 04:53:33 -06:00
if current_arm.guard().is_some() {
return None;
}
let current_expr = current_arm.expr()?;
2020-02-05 04:53:33 -06:00
let current_text_range = current_arm.syntax().text_range();
2022-01-07 17:44:14 -06:00
let current_arm_types = get_arm_types(&ctx, &current_arm);
2020-02-05 04:53:33 -06:00
// We check if the following match arms match this one. We could, but don't,
// compare to the previous match arm as well.
2020-03-19 05:38:26 -05:00
let arms_to_merge = successors(Some(current_arm), |it| neighbor(it, Direction::Next))
2021-07-01 14:10:45 -05:00
.take_while(|arm| match arm.expr() {
2022-01-10 12:14:29 -06:00
Some(expr) if arm.guard().is_none() => {
2022-01-07 17:44:14 -06:00
let same_text = expr.syntax().text() == current_expr.syntax().text();
if !same_text {
return false;
}
2022-01-10 12:30:27 -06:00
return are_same_types(&current_arm_types, arm, ctx);
2020-02-05 04:53:33 -06:00
}
2021-07-01 14:10:45 -05:00
_ => false,
2020-02-05 04:53:33 -06:00
})
.collect::<Vec<_>>();
if arms_to_merge.len() <= 1 {
return None;
}
2020-06-28 17:36:05 -05:00
acc.add(
2020-07-02 16:48:35 -05:00
AssistId("merge_match_arms", AssistKind::RefactorRewrite),
2020-06-28 17:36:05 -05:00
"Merge match arms",
current_text_range,
|edit| {
let pats = if arms_to_merge.iter().any(contains_placeholder) {
"_".into()
} else {
arms_to_merge
.iter()
.filter_map(ast::MatchArm::pat)
.map(|x| x.syntax().to_string())
.collect::<Vec<String>>()
.join(" | ")
};
let arm = format!("{} => {},", pats, current_expr.syntax().text());
2021-07-01 14:10:45 -05:00
if let [first, .., last] = &*arms_to_merge {
let start = first.syntax().text_range().start();
let end = last.syntax().text_range().end();
2021-07-01 14:10:45 -05:00
edit.replace(TextRange::new(start, end), arm);
}
2020-06-28 17:36:05 -05:00
},
)
}
2022-01-11 15:39:50 -06:00
fn contains_placeholder(a: &MatchArm) -> bool {
2020-07-31 13:07:21 -05:00
matches!(a.pat(), Some(ast::Pat::WildcardPat(..)))
2020-02-05 04:46:05 -06:00
}
2022-01-10 12:30:27 -06:00
fn are_same_types(
2022-01-11 15:39:50 -06:00
current_arm_types: &HashMap<String, Option<TypeInfo>>,
2022-01-10 12:30:27 -06:00
arm: &ast::MatchArm,
ctx: &AssistContext,
) -> bool {
let arm_types = get_arm_types(&ctx, &arm);
2022-01-11 15:50:42 -06:00
for (other_arm_type_name, other_arm_type) in arm_types {
2022-01-11 15:53:39 -06:00
if let (Some(Some(current_arm_type)), Some(other_arm_type)) =
(current_arm_types.get(&other_arm_type_name), other_arm_type)
{
if other_arm_type.original != current_arm_type.original {
return false;
2022-01-11 15:39:50 -06:00
}
2022-01-11 15:44:30 -06:00
} else {
// No corresponding field found
return false;
2022-01-10 12:30:27 -06:00
}
}
return true;
}
2022-01-11 15:39:50 -06:00
fn get_arm_types(context: &AssistContext, arm: &MatchArm) -> HashMap<String, Option<TypeInfo>> {
let mut mapping: HashMap<String, Option<TypeInfo>> = HashMap::new();
2022-01-11 15:40:57 -06:00
fn recurse(
pat: &Option<Pat>,
map: &mut HashMap<String, Option<TypeInfo>>,
ctx: &AssistContext,
) {
2022-01-11 15:39:50 -06:00
if let Some(local_pat) = pat {
match pat {
Some(ast::Pat::TupleStructPat(tuple)) => {
for field in tuple.fields() {
recurse(&Some(field), map, ctx);
}
2022-01-11 15:40:57 -06:00
}
2022-01-13 18:35:21 -06:00
Some(ast::Pat::TuplePat(tuple)) => {
for field in tuple.fields() {
recurse(&Some(field), map, ctx);
}
}
2022-01-11 15:39:50 -06:00
Some(ast::Pat::RecordPat(record)) => {
if let Some(field_list) = record.record_pat_field_list() {
for field in field_list.fields() {
recurse(&field.pat(), map, ctx);
}
}
2022-01-11 15:40:57 -06:00
}
2022-01-13 18:39:44 -06:00
Some(ast::Pat::ParenPat(parentheses)) => {
recurse(&parentheses.pat(), map, ctx);
}
2022-01-13 19:18:03 -06:00
Some(ast::Pat::SlicePat(slice)) => {
for slice_pat in slice.pats() {
recurse(&Some(slice_pat), map, ctx);
}
}
2022-01-11 15:39:50 -06:00
Some(ast::Pat::IdentPat(ident_pat)) => {
if let Some(name) = ident_pat.name() {
let pat_type = ctx.sema.type_of_pat(local_pat);
map.insert(name.text().to_string(), pat_type);
}
2022-01-11 15:40:57 -06:00
}
2022-01-11 15:39:50 -06:00
_ => (),
}
2022-01-10 12:38:17 -06:00
}
2022-01-07 17:44:14 -06:00
}
2022-01-11 15:39:50 -06:00
recurse(&arm.pat(), &mut mapping, &context);
return mapping;
2022-01-07 17:44:14 -06:00
}
#[cfg(test)]
mod tests {
2020-05-06 03:16:55 -05:00
use crate::tests::{check_assist, check_assist_not_applicable};
2020-03-19 05:38:26 -05:00
use super::*;
#[test]
fn merge_match_arms_single_patterns() {
check_assist(
merge_match_arms,
r#"
2021-07-01 14:10:45 -05:00
#[derive(Debug)]
enum X { A, B, C }
fn main() {
let x = X::A;
let y = match x {
X::A => { 1i32$0 }
X::B => { 1i32 }
X::C => { 2i32 }
}
}
"#,
r#"
2021-07-01 14:10:45 -05:00
#[derive(Debug)]
enum X { A, B, C }
fn main() {
let x = X::A;
let y = match x {
X::A | X::B => { 1i32 },
2021-07-01 14:10:45 -05:00
X::C => { 2i32 }
}
}
"#,
);
}
#[test]
fn merge_match_arms_multiple_patterns() {
check_assist(
merge_match_arms,
r#"
2021-07-01 14:10:45 -05:00
#[derive(Debug)]
enum X { A, B, C, D, E }
fn main() {
let x = X::A;
let y = match x {
X::A | X::B => {$0 1i32 },
X::C | X::D => { 1i32 },
X::E => { 2i32 },
}
}
"#,
r#"
2021-07-01 14:10:45 -05:00
#[derive(Debug)]
enum X { A, B, C, D, E }
fn main() {
let x = X::A;
let y = match x {
X::A | X::B | X::C | X::D => { 1i32 },
X::E => { 2i32 },
}
}
"#,
);
}
#[test]
fn merge_match_arms_placeholder_pattern() {
check_assist(
merge_match_arms,
r#"
2021-07-01 14:10:45 -05:00
#[derive(Debug)]
enum X { A, B, C, D, E }
fn main() {
let x = X::A;
let y = match x {
X::A => { 1i32 },
X::B => { 2i$032 },
_ => { 2i32 }
}
}
"#,
r#"
2021-07-01 14:10:45 -05:00
#[derive(Debug)]
enum X { A, B, C, D, E }
fn main() {
let x = X::A;
let y = match x {
X::A => { 1i32 },
_ => { 2i32 },
2021-07-01 14:10:45 -05:00
}
}
"#,
);
}
2020-02-05 04:53:33 -06:00
#[test]
fn merges_all_subsequent_arms() {
check_assist(
merge_match_arms,
r#"
2021-07-01 14:10:45 -05:00
enum X { A, B, C, D, E }
fn main() {
match X::A {
X::A$0 => 92,
X::B => 92,
X::C => 92,
X::D => 62,
_ => panic!(),
}
}
"#,
2020-02-05 04:53:33 -06:00
r#"
2021-07-01 14:10:45 -05:00
enum X { A, B, C, D, E }
fn main() {
match X::A {
X::A | X::B | X::C => 92,
X::D => 62,
_ => panic!(),
}
}
"#,
2020-02-05 04:53:33 -06:00
)
}
#[test]
fn merge_match_arms_rejects_guards() {
check_assist_not_applicable(
merge_match_arms,
r#"
2021-07-01 14:10:45 -05:00
#[derive(Debug)]
enum X {
A(i32),
B,
C
}
2021-07-01 14:10:45 -05:00
fn main() {
let x = X::A;
let y = match x {
X::A(a) if a > 5 => { $01i32 },
X::B => { 1i32 },
X::C => { 2i32 }
}
}
2022-01-07 17:44:14 -06:00
"#,
);
}
#[test]
fn merge_match_arms_different_type() {
check_assist_not_applicable(
merge_match_arms,
2022-01-10 06:44:45 -06:00
r#"//- minicore: result
2022-01-07 17:44:14 -06:00
fn func() {
2022-01-10 12:19:37 -06:00
match Result::<f64, f32>::Ok(0f64) {
Ok(x) => $0x.classify(),
Err(x) => x.classify()
2022-01-07 17:44:14 -06:00
};
}
2021-07-01 14:10:45 -05:00
"#,
);
}
2022-01-10 12:24:36 -06:00
#[test]
fn merge_match_arms_different_type_multiple_fields() {
check_assist_not_applicable(
merge_match_arms,
r#"//- minicore: result
fn func() {
match Result::<(f64, f64), (f32, f32)>::Ok((0f64, 0f64)) {
Ok(x) => $0x.1.classify(),
Err(x) => x.1.classify()
};
}
2022-01-10 12:24:36 -06:00
"#,
);
}
2022-01-07 17:44:14 -06:00
2022-01-10 12:24:36 -06:00
#[test]
fn merge_match_arms_same_type_multiple_fields() {
check_assist(
merge_match_arms,
r#"//- minicore: result
fn func() {
match Result::<(f64, f64), (f64, f64)>::Ok((0f64, 0f64)) {
Ok(x) => $0x.1.classify(),
Err(x) => x.1.classify()
};
}
"#,
r#"
fn func() {
match Result::<(f64, f64), (f64, f64)>::Ok((0f64, 0f64)) {
Ok(x) | Err(x) => x.1.classify(),
};
}
2022-01-10 12:46:47 -06:00
"#,
);
}
#[test]
fn merge_match_arms_same_type_subsequent_arm_with_different_type_in_other() {
check_assist(
merge_match_arms,
r#"
enum MyEnum {
OptionA(f32),
OptionB(f32),
OptionC(f64)
}
fn func(e: MyEnum) {
match e {
MyEnum::OptionA(x) => $0x.classify(),
MyEnum::OptionB(x) => x.classify(),
MyEnum::OptionC(x) => x.classify(),
};
}
"#,
r#"
enum MyEnum {
OptionA(f32),
OptionB(f32),
OptionC(f64)
}
fn func(e: MyEnum) {
match e {
MyEnum::OptionA(x) | MyEnum::OptionB(x) => x.classify(),
MyEnum::OptionC(x) => x.classify(),
};
}
"#,
);
}
#[test]
fn merge_match_arms_same_type_skip_arm_with_different_type_in_between() {
check_assist_not_applicable(
merge_match_arms,
r#"
enum MyEnum {
OptionA(f32),
OptionB(f64),
OptionC(f32)
}
fn func(e: MyEnum) {
match e {
MyEnum::OptionA(x) => $0x.classify(),
MyEnum::OptionB(x) => x.classify(),
MyEnum::OptionC(x) => x.classify(),
};
}
2022-01-10 12:54:59 -06:00
"#,
);
}
#[test]
fn merge_match_arms_same_type_different_number_of_fields() {
check_assist_not_applicable(
merge_match_arms,
r#"//- minicore: result
fn func() {
2022-01-10 17:10:09 -06:00
match Result::<(f64, f64, f64), (f64, f64)>::Ok((0f64, 0f64, 0f64)) {
2022-01-10 12:54:59 -06:00
Ok(x) => $0x.1.classify(),
Err(x) => x.1.classify()
};
}
2022-01-10 12:24:36 -06:00
"#,
);
}
2022-01-11 14:05:56 -06:00
#[test]
fn merge_match_same_destructuring_different_types() {
check_assist_not_applicable(
merge_match_arms,
r#"
struct Point {
x: i32,
y: i32,
}
fn func() {
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => $0"",
Point { x: 0, y } => "",
Point { x, y } => "",
};
}
"#,
);
}
#[test]
fn merge_match_arms_range() {
check_assist(
merge_match_arms,
r#"
2022-01-11 15:39:50 -06:00
fn func() {
let x = 'c';
2022-01-11 14:05:56 -06:00
match x {
'a'..='j' => $0"",
'c'..='z' => "",
_ => "other",
};
2022-01-11 15:39:50 -06:00
}
2022-01-11 14:05:56 -06:00
"#,
r#"
2022-01-11 15:39:50 -06:00
fn func() {
let x = 'c';
2022-01-11 14:05:56 -06:00
match x {
'a'..='j' | 'c'..='z' => "",
_ => "other",
};
2022-01-11 15:39:50 -06:00
}
2022-01-11 14:05:56 -06:00
"#,
);
}
#[test]
fn merge_match_arms_enum_without_field() {
check_assist_not_applicable(
merge_match_arms,
r#"
enum MyEnum {
NoField,
AField(u8)
}
fn func(x: MyEnum) {
match x {
MyEnum::NoField => $0"",
MyEnum::AField(x) => ""
};
}
"#,
)
}
#[test]
fn merge_match_arms_enum_destructuring_different_types() {
check_assist_not_applicable(
merge_match_arms,
r#"
enum MyEnum {
Move { x: i32, y: i32 },
Write(String),
}
fn func(x: MyEnum) {
match x {
MyEnum::Move { x, y } => $0"",
MyEnum::Write(text) => "",
};
}
"#,
)
}
#[test]
fn merge_match_arms_enum_destructuring_same_types() {
check_assist(
merge_match_arms,
r#"
enum MyEnum {
Move { x: i32, y: i32 },
Crawl { x: i32, y: i32 }
}
fn func(x: MyEnum) {
match x {
MyEnum::Move { x, y } => $0"",
MyEnum::Crawl { x, y } => "",
};
}
"#,
r#"
enum MyEnum {
Move { x: i32, y: i32 },
Crawl { x: i32, y: i32 }
}
fn func(x: MyEnum) {
match x {
MyEnum::Move { x, y } | MyEnum::Crawl { x, y } => "",
};
}
2022-01-11 15:40:57 -06:00
"#,
2022-01-11 14:05:56 -06:00
)
}
#[test]
fn merge_match_arms_enum_destructuring_same_types_different_name() {
check_assist_not_applicable(
merge_match_arms,
r#"
enum MyEnum {
Move { x: i32, y: i32 },
Crawl { a: i32, b: i32 }
}
fn func(x: MyEnum) {
match x {
MyEnum::Move { x, y } => $0"",
MyEnum::Crawl { a, b } => "",
};
}
2022-01-11 15:40:57 -06:00
"#,
2022-01-11 14:05:56 -06:00
)
}
#[test]
fn merge_match_arms_enum_nested_pattern_different_names() {
check_assist_not_applicable(
merge_match_arms,
r#"
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main(msg: Message) {
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => $0"",
Message::ChangeColor(Color::Hsv(h, s, v)) => "",
_ => "other"
};
}
"#,
)
}
#[test]
fn merge_match_arms_enum_nested_pattern_same_names() {
check_assist(
merge_match_arms,
r#"
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main(msg: Message) {
match msg {
Message::ChangeColor(Color::Rgb(a, b, c)) => $0"",
Message::ChangeColor(Color::Hsv(a, b, c)) => "",
_ => "other"
};
}
"#,
2022-01-11 15:40:57 -06:00
r#"
2022-01-11 14:05:56 -06:00
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main(msg: Message) {
match msg {
Message::ChangeColor(Color::Rgb(a, b, c)) | Message::ChangeColor(Color::Hsv(a, b, c)) => "",
_ => "other"
};
}
"#,
)
}
#[test]
fn merge_match_arms_enum_destructuring_with_ignore() {
check_assist(
merge_match_arms,
r#"
enum MyEnum {
Move { x: i32, a: i32 },
Crawl { x: i32, b: i32 }
}
fn func(x: MyEnum) {
match x {
MyEnum::Move { x, .. } => $0"",
MyEnum::Crawl { x, .. } => "",
};
}
"#,
r#"
enum MyEnum {
Move { x: i32, a: i32 },
Crawl { x: i32, b: i32 }
}
fn func(x: MyEnum) {
match x {
MyEnum::Move { x, .. } | MyEnum::Crawl { x, .. } => "",
};
}
2022-01-11 15:40:57 -06:00
"#,
2022-01-11 14:05:56 -06:00
)
}
#[test]
fn merge_match_arms_nested_with_conflicting_identifier() {
check_assist_not_applicable(
merge_match_arms,
r#"
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Move { x: i32, y: i32 },
ChangeColor(u8, Color),
2022-01-10 12:24:36 -06:00
}
fn main(msg: Message) {
match msg {
Message::ChangeColor(x, Color::Rgb(y, b, c)) => $0"",
Message::ChangeColor(y, Color::Hsv(x, b, c)) => "",
_ => "other"
};
2022-01-13 18:35:21 -06:00
}
"#,
)
}
#[test]
fn merge_match_arms_tuple() {
check_assist_not_applicable(
merge_match_arms,
r#"
fn func() {
match (0, "boo") {
(x, y) => $0"",
(y, x) => "",
};
2022-01-13 18:39:44 -06:00
}
"#,
)
}
#[test]
fn merge_match_arms_parentheses() {
check_assist_not_applicable(
merge_match_arms,
r#"
fn func(x: i32) {
let variable = 2;
match x {
1 => $0"",
((((variable)))) => "",
_ => "other"
};
2022-01-13 19:18:03 -06:00
}
"#,
)
}
#[test]
fn merge_match_arms_refpat() {
check_assist_not_applicable(
merge_match_arms,
r#"
fn func() {
let name = Some(String::from(""));
let n = String::from("");
match name {
Some(ref n) => $0"",
Some(n) => "",
_ => "other",
};
}
"#,
)
}
#[test]
fn merge_match_arms_slice() {
check_assist_not_applicable(
merge_match_arms,
r#"
fn func(binary: &[u8]) {
let space = b' ';
match binary {
[0x7f, b'E', b'L', b'F', ..] => $0"",
[space] => "",
_ => "other",
};
}
2022-01-11 15:40:57 -06:00
"#,
)
}
}