rust/crates/ide_assists/src/handlers/pull_assignment_up.rs
2021-03-08 22:19:44 +02:00

443 lines
9.0 KiB
Rust

use syntax::{
ast::{self, edit::AstNodeEdit, make},
AstNode,
};
use crate::{
assist_context::{AssistContext, Assists},
AssistId, AssistKind,
};
// Assist: pull_assignment_up
//
// Extracts variable assignment to outside an if or match statement.
//
// ```
// fn main() {
// let mut foo = 6;
//
// if true {
// $0foo = 5;
// } else {
// foo = 4;
// }
// }
// ```
// ->
// ```
// fn main() {
// let mut foo = 6;
//
// foo = if true {
// 5
// } else {
// 4
// };
// }
// ```
pub(crate) fn pull_assignment_up(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let assign_expr = ctx.find_node_at_offset::<ast::BinExpr>()?;
let name_expr = if assign_expr.op_kind()? == ast::BinOp::Assignment {
assign_expr.lhs()?
} else {
return None;
};
let (old_stmt, new_stmt) = if let Some(if_expr) = ctx.find_node_at_offset::<ast::IfExpr>() {
(
ast::Expr::cast(if_expr.syntax().to_owned())?,
exprify_if(&if_expr, &ctx.sema, &name_expr)?.indent(if_expr.indent_level()),
)
} else if let Some(match_expr) = ctx.find_node_at_offset::<ast::MatchExpr>() {
(
ast::Expr::cast(match_expr.syntax().to_owned())?,
exprify_match(&match_expr, &ctx.sema, &name_expr)?,
)
} else {
return None;
};
let expr_stmt = make::expr_stmt(new_stmt);
acc.add(
AssistId("pull_assignment_up", AssistKind::RefactorExtract),
"Pull assignment up",
old_stmt.syntax().text_range(),
move |edit| {
edit.replace(old_stmt.syntax().text_range(), format!("{} = {};", name_expr, expr_stmt));
},
)
}
fn exprify_match(
match_expr: &ast::MatchExpr,
sema: &hir::Semantics<ide_db::RootDatabase>,
name: &ast::Expr,
) -> Option<ast::Expr> {
let new_arm_list = match_expr
.match_arm_list()?
.arms()
.map(|arm| {
if let ast::Expr::BlockExpr(block) = arm.expr()? {
let new_block = exprify_block(&block, sema, name)?.indent(block.indent_level());
Some(arm.replace_descendant(block, new_block))
} else {
None
}
})
.collect::<Option<Vec<_>>>()?;
let new_arm_list = match_expr
.match_arm_list()?
.replace_descendants(match_expr.match_arm_list()?.arms().zip(new_arm_list));
Some(make::expr_match(match_expr.expr()?, new_arm_list))
}
fn exprify_if(
statement: &ast::IfExpr,
sema: &hir::Semantics<ide_db::RootDatabase>,
name: &ast::Expr,
) -> Option<ast::Expr> {
let then_branch = exprify_block(&statement.then_branch()?, sema, name)?;
let else_branch = match statement.else_branch()? {
ast::ElseBranch::Block(ref block) => {
ast::ElseBranch::Block(exprify_block(block, sema, name)?)
}
ast::ElseBranch::IfExpr(expr) => {
cov_mark::hit!(test_pull_assignment_up_chained_if);
ast::ElseBranch::IfExpr(ast::IfExpr::cast(
exprify_if(&expr, sema, name)?.syntax().to_owned(),
)?)
}
};
Some(make::expr_if(statement.condition()?, then_branch, Some(else_branch)))
}
fn exprify_block(
block: &ast::BlockExpr,
sema: &hir::Semantics<ide_db::RootDatabase>,
name: &ast::Expr,
) -> Option<ast::BlockExpr> {
if block.tail_expr().is_some() {
return None;
}
let mut stmts: Vec<_> = block.statements().collect();
let stmt = stmts.pop()?;
if let ast::Stmt::ExprStmt(stmt) = stmt {
if let ast::Expr::BinExpr(expr) = stmt.expr()? {
if expr.op_kind()? == ast::BinOp::Assignment && is_equivalent(sema, &expr.lhs()?, name)
{
// The last statement in the block is an assignment to the name we want
return Some(make::block_expr(stmts, Some(expr.rhs()?)));
}
}
}
None
}
fn is_equivalent(
sema: &hir::Semantics<ide_db::RootDatabase>,
expr0: &ast::Expr,
expr1: &ast::Expr,
) -> bool {
match (expr0, expr1) {
(ast::Expr::FieldExpr(field_expr0), ast::Expr::FieldExpr(field_expr1)) => {
cov_mark::hit!(test_pull_assignment_up_field_assignment);
sema.resolve_field(field_expr0) == sema.resolve_field(field_expr1)
}
(ast::Expr::PathExpr(path0), ast::Expr::PathExpr(path1)) => {
let path0 = path0.path();
let path1 = path1.path();
if let (Some(path0), Some(path1)) = (path0, path1) {
sema.resolve_path(&path0) == sema.resolve_path(&path1)
} else {
false
}
}
(ast::Expr::PrefixExpr(prefix0), ast::Expr::PrefixExpr(prefix1))
if prefix0.op_kind() == Some(ast::PrefixOp::Deref)
&& prefix1.op_kind() == Some(ast::PrefixOp::Deref) =>
{
cov_mark::hit!(test_pull_assignment_up_deref);
if let (Some(prefix0), Some(prefix1)) = (prefix0.expr(), prefix1.expr()) {
is_equivalent(sema, &prefix0, &prefix1)
} else {
false
}
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{check_assist, check_assist_not_applicable};
#[test]
fn test_pull_assignment_up_if() {
check_assist(
pull_assignment_up,
r#"
fn foo() {
let mut a = 1;
if true {
$0a = 2;
} else {
a = 3;
}
}"#,
r#"
fn foo() {
let mut a = 1;
a = if true {
2
} else {
3
};
}"#,
);
}
#[test]
fn test_pull_assignment_up_match() {
check_assist(
pull_assignment_up,
r#"
fn foo() {
let mut a = 1;
match 1 {
1 => {
$0a = 2;
},
2 => {
a = 3;
},
3 => {
a = 4;
}
}
}"#,
r#"
fn foo() {
let mut a = 1;
a = match 1 {
1 => {
2
},
2 => {
3
},
3 => {
4
}
};
}"#,
);
}
#[test]
fn test_pull_assignment_up_not_last_not_applicable() {
check_assist_not_applicable(
pull_assignment_up,
r#"
fn foo() {
let mut a = 1;
if true {
$0a = 2;
b = a;
} else {
a = 3;
}
}"#,
)
}
#[test]
fn test_pull_assignment_up_chained_if() {
cov_mark::check!(test_pull_assignment_up_chained_if);
check_assist(
pull_assignment_up,
r#"
fn foo() {
let mut a = 1;
if true {
$0a = 2;
} else if false {
a = 3;
} else {
a = 4;
}
}"#,
r#"
fn foo() {
let mut a = 1;
a = if true {
2
} else if false {
3
} else {
4
};
}"#,
);
}
#[test]
fn test_pull_assignment_up_retains_stmts() {
check_assist(
pull_assignment_up,
r#"
fn foo() {
let mut a = 1;
if true {
let b = 2;
$0a = 2;
} else {
let b = 3;
a = 3;
}
}"#,
r#"
fn foo() {
let mut a = 1;
a = if true {
let b = 2;
2
} else {
let b = 3;
3
};
}"#,
)
}
#[test]
fn pull_assignment_up_let_stmt_not_applicable() {
check_assist_not_applicable(
pull_assignment_up,
r#"
fn foo() {
let mut a = 1;
let b = if true {
$0a = 2
} else {
a = 3
};
}"#,
)
}
#[test]
fn pull_assignment_up_if_missing_assigment_not_applicable() {
check_assist_not_applicable(
pull_assignment_up,
r#"
fn foo() {
let mut a = 1;
if true {
$0a = 2;
} else {}
}"#,
)
}
#[test]
fn pull_assignment_up_match_missing_assigment_not_applicable() {
check_assist_not_applicable(
pull_assignment_up,
r#"
fn foo() {
let mut a = 1;
match 1 {
1 => {
$0a = 2;
},
2 => {
a = 3;
},
3 => {},
}
}"#,
)
}
#[test]
fn test_pull_assignment_up_field_assignment() {
cov_mark::check!(test_pull_assignment_up_field_assignment);
check_assist(
pull_assignment_up,
r#"
struct A(usize);
fn foo() {
let mut a = A(1);
if true {
$0a.0 = 2;
} else {
a.0 = 3;
}
}"#,
r#"
struct A(usize);
fn foo() {
let mut a = A(1);
a.0 = if true {
2
} else {
3
};
}"#,
)
}
#[test]
fn test_pull_assignment_up_deref() {
cov_mark::check!(test_pull_assignment_up_deref);
check_assist(
pull_assignment_up,
r#"
fn foo() {
let mut a = 1;
let b = &mut a;
if true {
$0*b = 2;
} else {
*b = 3;
}
}
"#,
r#"
fn foo() {
let mut a = 1;
let b = &mut a;
*b = if true {
2
} else {
3
};
}
"#,
)
}
}