Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Assist to turn match into matches! invocation #13005

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
use syntax::ast::{self, AstNode};

use crate::{AssistContext, AssistId, AssistKind, Assists};

// Assist: convert_two_arm_bool_match_to_matches_macro
//
// Convert 2-arm match that evaluates to a boolean into the equivalent matches! invocation.
//
// ```
// fn main() {
// match scrutinee$0 {
// Some(val) if val.cond() => true,
// _ => false,
// }
// }
// ```
// ->
// ```
// fn main() {
// matches!(scrutinee, Some(val) if val.cond())
// }
// ```
pub(crate) fn convert_two_arm_bool_match_to_matches_macro(
acc: &mut Assists,
ctx: &AssistContext<'_>,
) -> Option<()> {
let match_expr = ctx.find_node_at_offset::<ast::MatchExpr>()?;
let match_arm_list = match_expr.match_arm_list()?;
let mut arms = match_arm_list.arms();
let first_arm = arms.next()?;
let second_arm = arms.next()?;
if arms.next().is_some() {
cov_mark::hit!(non_two_arm_match);
return None;
}
let first_arm_expr = first_arm.expr();
let second_arm_expr = second_arm.expr();

let invert_matches = if is_bool_literal_expr(&first_arm_expr, true)
&& is_bool_literal_expr(&second_arm_expr, false)
{
false
} else if is_bool_literal_expr(&first_arm_expr, false)
&& is_bool_literal_expr(&second_arm_expr, true)
{
true
} else {
cov_mark::hit!(non_invert_bool_literal_arms);
return None;
};

let target_range = ctx.sema.original_range(match_expr.syntax()).range;
let expr = match_expr.expr()?;

acc.add(
AssistId("convert_two_arm_bool_match_to_matches_macro", AssistKind::RefactorRewrite),
"Convert to matches!",
target_range,
|builder| {
let mut arm_str = String::new();
if let Some(ref pat) = first_arm.pat() {
arm_str += &pat.to_string();
}
if let Some(ref guard) = first_arm.guard() {
arm_str += &format!(" {}", &guard.to_string());
}
if invert_matches {
builder.replace(target_range, format!("!matches!({}, {})", expr, arm_str));
} else {
builder.replace(target_range, format!("matches!({}, {})", expr, arm_str));
}
},
)
}

fn is_bool_literal_expr(expr: &Option<ast::Expr>, expect_bool: bool) -> bool {
if let Some(ast::Expr::Literal(lit)) = expr {
if let ast::LiteralKind::Bool(b) = lit.kind() {
return b == expect_bool;
}
}

return false;
}

#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};

use super::convert_two_arm_bool_match_to_matches_macro;

#[test]
fn not_applicable_outside_of_range_left() {
check_assist_not_applicable(
convert_two_arm_bool_match_to_matches_macro,
r#"
fn foo(a: Option<u32>) -> bool {
$0 match a {
Some(_val) => true,
_ => false
}
}
"#,
);
}

#[test]
fn not_applicable_non_two_arm_match() {
cov_mark::check!(non_two_arm_match);
check_assist_not_applicable(
convert_two_arm_bool_match_to_matches_macro,
r#"
fn foo(a: Option<u32>) -> bool {
match a$0 {
Some(3) => true,
Some(4) => true,
_ => false
}
}
"#,
);
}

#[test]
fn not_applicable_non_bool_literal_arms() {
cov_mark::check!(non_invert_bool_literal_arms);
check_assist_not_applicable(
convert_two_arm_bool_match_to_matches_macro,
r#"
fn foo(a: Option<u32>) -> bool {
match a$0 {
Some(val) => val == 3,
_ => false
}
}
"#,
);
}
#[test]
fn not_applicable_both_false_arms() {
cov_mark::check!(non_invert_bool_literal_arms);
check_assist_not_applicable(
convert_two_arm_bool_match_to_matches_macro,
r#"
fn foo(a: Option<u32>) -> bool {
match a$0 {
Some(val) => false,
_ => false
}
}
"#,
);
}

#[test]
fn not_applicable_both_true_arms() {
cov_mark::check!(non_invert_bool_literal_arms);
check_assist_not_applicable(
convert_two_arm_bool_match_to_matches_macro,
r#"
fn foo(a: Option<u32>) -> bool {
match a$0 {
Some(val) => true,
_ => true
}
}
"#,
);
}

#[test]
fn convert_simple_case() {
check_assist(
convert_two_arm_bool_match_to_matches_macro,
r#"
fn foo(a: Option<u32>) -> bool {
match a$0 {
Some(_val) => true,
_ => false
}
}
"#,
r#"
fn foo(a: Option<u32>) -> bool {
matches!(a, Some(_val))
}
"#,
);
}

#[test]
fn convert_simple_invert_case() {
check_assist(
convert_two_arm_bool_match_to_matches_macro,
r#"
fn foo(a: Option<u32>) -> bool {
match a$0 {
Some(_val) => false,
_ => true
}
}
"#,
r#"
fn foo(a: Option<u32>) -> bool {
!matches!(a, Some(_val))
}
"#,
);
}

#[test]
fn convert_with_guard_case() {
check_assist(
convert_two_arm_bool_match_to_matches_macro,
r#"
fn foo(a: Option<u32>) -> bool {
match a$0 {
Some(val) if val > 3 => true,
_ => false
}
}
"#,
r#"
fn foo(a: Option<u32>) -> bool {
matches!(a, Some(val) if val > 3)
}
"#,
);
}

#[test]
fn convert_enum_match_cases() {
check_assist(
convert_two_arm_bool_match_to_matches_macro,
r#"
enum X { A, B }

fn foo(a: X) -> bool {
match a$0 {
X::A => true,
_ => false
}
}
"#,
r#"
enum X { A, B }

fn foo(a: X) -> bool {
matches!(a, X::A)
}
"#,
);
}

#[test]
fn convert_target_simple() {
check_assist_target(
convert_two_arm_bool_match_to_matches_macro,
r#"
fn foo(a: Option<u32>) -> bool {
match a$0 {
Some(val) => true,
_ => false
}
}
"#,
r#"match a {
Some(val) => true,
_ => false
}"#,
);
}

#[test]
fn convert_target_complex() {
check_assist_target(
convert_two_arm_bool_match_to_matches_macro,
r#"
enum E { X, Y }

fn main() {
match E::X$0 {
E::X => true,
_ => false,
}
}
"#,
"match E::X {
E::X => true,
_ => false,
}",
);
}
jonas-schievink marked this conversation as resolved.
Show resolved Hide resolved
}
2 changes: 2 additions & 0 deletions crates/ide-assists/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ mod handlers {
mod convert_let_else_to_match;
mod convert_tuple_struct_to_named_struct;
mod convert_to_guarded_return;
mod convert_two_arm_bool_match_to_matches_macro;
mod convert_while_to_loop;
mod destructure_tuple_binding;
mod expand_glob_import;
Expand Down Expand Up @@ -216,6 +217,7 @@ mod handlers {
convert_let_else_to_match::convert_let_else_to_match,
convert_to_guarded_return::convert_to_guarded_return,
convert_tuple_struct_to_named_struct::convert_tuple_struct_to_named_struct,
convert_two_arm_bool_match_to_matches_macro::convert_two_arm_bool_match_to_matches_macro,
convert_while_to_loop::convert_while_to_loop,
destructure_tuple_binding::destructure_tuple_binding,
expand_glob_import::expand_glob_import,
Expand Down
20 changes: 20 additions & 0 deletions crates/ide-assists/src/tests/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,26 @@ impl Point {
)
}

#[test]
fn doctest_convert_two_arm_bool_match_to_matches_macro() {
check_doc_test(
"convert_two_arm_bool_match_to_matches_macro",
r#####"
fn main() {
match scrutinee$0 {
Some(val) if val.cond() => true,
_ => false,
}
}
"#####,
r#####"
fn main() {
matches!(scrutinee, Some(val) if val.cond())
}
"#####,
)
}

#[test]
fn doctest_convert_while_to_loop() {
check_doc_test(
Expand Down