From ad65ba40624a0f9d0ecbfbfc2671e20cb98db029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BAc=C3=A1s=20Meier?= Date: Thu, 3 Oct 2019 22:19:46 +0200 Subject: [PATCH 1/6] WIP: Add demorgan application with naive negation --- crates/ra_assists/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index 91b2a1dcef..d2376c475c 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs @@ -92,6 +92,7 @@ mod assists { mod add_derive; mod add_explicit_type; mod add_impl; + mod apply_demorgan; mod flip_comma; mod flip_binexpr; mod change_visibility; @@ -113,6 +114,7 @@ mod assists { add_derive::add_derive, add_explicit_type::add_explicit_type, add_impl::add_impl, + apply_demorgan::apply_demorgan, change_visibility::change_visibility, fill_match_arms::fill_match_arms, merge_match_arms::merge_match_arms, From e769a54502648011ede7d7434e0a16a8ab740c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BAc=C3=A1s=20Meier?= Date: Thu, 3 Oct 2019 22:48:35 +0200 Subject: [PATCH 2/6] Create an assist for applying De Morgan's law Fixes #1807 This assist can transform expressions of the form `!x || !y` into `!(x && y)`. This also works with `&&`. This assist will only trigger if the cursor is on the central logical operator. The main limitation of this current implementation is that both operands need to be an explicit negation, either of the form `!x`, or `x != y`. More operands could be accepted, but this would complicate the implementation quite a bit. --- .../ra_assists/src/assists/apply_demorgan.rs | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 crates/ra_assists/src/assists/apply_demorgan.rs diff --git a/crates/ra_assists/src/assists/apply_demorgan.rs b/crates/ra_assists/src/assists/apply_demorgan.rs new file mode 100644 index 0000000000..e4a8657ca6 --- /dev/null +++ b/crates/ra_assists/src/assists/apply_demorgan.rs @@ -0,0 +1,115 @@ +//! This contains the functions associated with the demorgan assist. +//! This assist transforms boolean expressions of the form `!a || !b` into +//! `!(a && b)`. +use hir::db::HirDatabase; +use ra_syntax::SyntaxNode; +use ra_syntax::ast::{AstNode, BinExpr, BinOp, Expr, PrefixOp}; + +use crate::{Assist, AssistCtx, AssistId}; + +// Return the opposite text for a given logical operator, if it makes sense +fn opposite_logic_op(kind: BinOp) -> Option<&'static str> { + match kind { + BinOp::BooleanOr => Some("&&"), + BinOp::BooleanAnd => Some("||"), + _ => None, + } +} + +// This function tries to undo unary negation, or inequality +fn undo_negation(node: SyntaxNode) -> Option { + match Expr::cast(node)? { + Expr::BinExpr(bin) => match bin.op_kind()? { + BinOp::NegatedEqualityTest => { + let lhs = bin.lhs()?.syntax().text(); + let rhs = bin.rhs()?.syntax().text(); + Some(format!("{} == {}", lhs, rhs)) + } + _ => None + } + Expr::PrefixExpr(pe) => match pe.op_kind()? { + PrefixOp::Not => { + let child = pe.expr()?.syntax().text(); + Some(String::from(child)) + } + _ => None + } + _ => None + } +} + +/// Assist for applying demorgan's law +/// +/// This transforms expressions of the form `!l || !r` into `!(l && r)`. +/// This also works with `&&`. This assist can only be applied with the cursor +/// on either `||` or `&&`, with both operands being a negation of some kind. +/// This means something of the form `!x` or `x != y`. +pub(crate) fn apply_demorgan(mut ctx: AssistCtx) -> Option { + let expr = ctx.node_at_offset::()?; + let op = expr.op_kind()?; + let op_range = expr.op_token()?.text_range(); + let opposite_op = opposite_logic_op(op)?; + let cursor_in_range = ctx.frange.range.is_subrange(&op_range); + if !cursor_in_range { + return None; + } + let lhs = expr.lhs()?.syntax().clone(); + let lhs_range = lhs.text_range(); + let rhs = expr.rhs()?.syntax().clone(); + let rhs_range = rhs.text_range(); + let not_lhs = undo_negation(lhs)?; + let not_rhs = undo_negation(rhs)?; + + + ctx.add_action(AssistId("apply_demorgan"), "apply demorgan's law", |edit| { + edit.target(op_range); + edit.replace(op_range, opposite_op); + edit.replace(lhs_range, format!("!({}", not_lhs)); + edit.replace(rhs_range, format!("{})", not_rhs)); + }); + ctx.build() +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::helpers::{check_assist, check_assist_not_applicable}; + + #[test] + fn demorgan_turns_and_into_or() { + check_assist( + apply_demorgan, + "fn f() { !x &&<|> !x }", + "fn f() { !(x ||<|> x) }" + ) + } + + #[test] + fn demorgan_turns_or_into_and() { + check_assist( + apply_demorgan, + "fn f() { !x ||<|> !x }", + "fn f() { !(x &&<|> x) }" + ) + } + + #[test] + fn demorgan_removes_inequality() { + check_assist( + apply_demorgan, + "fn f() { x != x ||<|> !x }", + "fn f() { !(x == x &&<|> x) }" + ) + } + + #[test] + fn demorgan_doesnt_apply_with_cursor_not_on_op() { + check_assist_not_applicable(apply_demorgan, "fn f() { <|> !x || !x }") + } + + #[test] + fn demorgan_doesnt_apply_when_operands_arent_negated_already() { + check_assist_not_applicable(apply_demorgan, "fn f() { x ||<|> x }") + } +} From 1ed1e3d4a757b8f57cb7c727d66703713cdc0bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BAc=C3=A1s=20Meier?= Date: Fri, 4 Oct 2019 08:21:24 +0200 Subject: [PATCH 3/6] Fix formatting --- .../ra_assists/src/assists/apply_demorgan.rs | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/crates/ra_assists/src/assists/apply_demorgan.rs b/crates/ra_assists/src/assists/apply_demorgan.rs index e4a8657ca6..caecc50cc8 100644 --- a/crates/ra_assists/src/assists/apply_demorgan.rs +++ b/crates/ra_assists/src/assists/apply_demorgan.rs @@ -2,8 +2,8 @@ //! This assist transforms boolean expressions of the form `!a || !b` into //! `!(a && b)`. use hir::db::HirDatabase; -use ra_syntax::SyntaxNode; use ra_syntax::ast::{AstNode, BinExpr, BinOp, Expr, PrefixOp}; +use ra_syntax::SyntaxNode; use crate::{Assist, AssistCtx, AssistId}; @@ -25,16 +25,16 @@ fn undo_negation(node: SyntaxNode) -> Option { let rhs = bin.rhs()?.syntax().text(); Some(format!("{} == {}", lhs, rhs)) } - _ => None - } + _ => None, + }, Expr::PrefixExpr(pe) => match pe.op_kind()? { PrefixOp::Not => { let child = pe.expr()?.syntax().text(); Some(String::from(child)) } - _ => None - } - _ => None + _ => None, + }, + _ => None, } } @@ -60,7 +60,6 @@ pub(crate) fn apply_demorgan(mut ctx: AssistCtx) -> Option !x }", - "fn f() { !(x ||<|> x) }" - ) + check_assist(apply_demorgan, "fn f() { !x &&<|> !x }", "fn f() { !(x ||<|> x) }") } #[test] fn demorgan_turns_or_into_and() { - check_assist( - apply_demorgan, - "fn f() { !x ||<|> !x }", - "fn f() { !(x &&<|> x) }" - ) + check_assist(apply_demorgan, "fn f() { !x ||<|> !x }", "fn f() { !(x &&<|> x) }") } #[test] fn demorgan_removes_inequality() { - check_assist( - apply_demorgan, - "fn f() { x != x ||<|> !x }", - "fn f() { !(x == x &&<|> x) }" - ) + check_assist(apply_demorgan, "fn f() { x != x ||<|> !x }", "fn f() { !(x == x &&<|> x) }") } #[test] From e17243d69893c7bba29ea5727154cb1d521fe9c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BAc=C3=A1s=20Meier?= Date: Fri, 4 Oct 2019 10:51:41 +0200 Subject: [PATCH 4/6] [#1807] Refactor file structure Use the more conventional way of importing the ast types, and put the assist at the top of the file. --- .../ra_assists/src/assists/apply_demorgan.rs | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/crates/ra_assists/src/assists/apply_demorgan.rs b/crates/ra_assists/src/assists/apply_demorgan.rs index caecc50cc8..5f2b0dd189 100644 --- a/crates/ra_assists/src/assists/apply_demorgan.rs +++ b/crates/ra_assists/src/assists/apply_demorgan.rs @@ -2,42 +2,11 @@ //! This assist transforms boolean expressions of the form `!a || !b` into //! `!(a && b)`. use hir::db::HirDatabase; -use ra_syntax::ast::{AstNode, BinExpr, BinOp, Expr, PrefixOp}; +use ra_syntax::ast::{self, AstNode}; use ra_syntax::SyntaxNode; use crate::{Assist, AssistCtx, AssistId}; -// Return the opposite text for a given logical operator, if it makes sense -fn opposite_logic_op(kind: BinOp) -> Option<&'static str> { - match kind { - BinOp::BooleanOr => Some("&&"), - BinOp::BooleanAnd => Some("||"), - _ => None, - } -} - -// This function tries to undo unary negation, or inequality -fn undo_negation(node: SyntaxNode) -> Option { - match Expr::cast(node)? { - Expr::BinExpr(bin) => match bin.op_kind()? { - BinOp::NegatedEqualityTest => { - let lhs = bin.lhs()?.syntax().text(); - let rhs = bin.rhs()?.syntax().text(); - Some(format!("{} == {}", lhs, rhs)) - } - _ => None, - }, - Expr::PrefixExpr(pe) => match pe.op_kind()? { - PrefixOp::Not => { - let child = pe.expr()?.syntax().text(); - Some(String::from(child)) - } - _ => None, - }, - _ => None, - } -} - /// Assist for applying demorgan's law /// /// This transforms expressions of the form `!l || !r` into `!(l && r)`. @@ -45,7 +14,7 @@ fn undo_negation(node: SyntaxNode) -> Option { /// on either `||` or `&&`, with both operands being a negation of some kind. /// This means something of the form `!x` or `x != y`. pub(crate) fn apply_demorgan(mut ctx: AssistCtx) -> Option { - let expr = ctx.node_at_offset::()?; + let expr = ctx.node_at_offset::()?; let op = expr.op_kind()?; let op_range = expr.op_token()?.text_range(); let opposite_op = opposite_logic_op(op)?; @@ -69,6 +38,37 @@ pub(crate) fn apply_demorgan(mut ctx: AssistCtx) -> Option Option<&'static str> { + match kind { + ast::BinOp::BooleanOr => Some("&&"), + ast::BinOp::BooleanAnd => Some("||"), + _ => None, + } +} + +// This function tries to undo unary negation, or inequality +fn undo_negation(node: SyntaxNode) -> Option { + match ast::Expr::cast(node)? { + ast::Expr::BinExpr(bin) => match bin.op_kind()? { + ast::BinOp::NegatedEqualityTest => { + let lhs = bin.lhs()?.syntax().text(); + let rhs = bin.rhs()?.syntax().text(); + Some(format!("{} == {}", lhs, rhs)) + } + _ => None, + }, + ast::Expr::PrefixExpr(pe) => match pe.op_kind()? { + ast::PrefixOp::Not => { + let child = pe.expr()?.syntax().text(); + Some(String::from(child)) + } + _ => None, + }, + _ => None, + } +} + #[cfg(test)] mod tests { use super::*; From fe8ec1c045af7a41492d0df6d271f1ce56afb51b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BAc=C3=A1s=20Meier?= Date: Fri, 4 Oct 2019 11:03:35 +0200 Subject: [PATCH 5/6] [#1807] Add entry in docs/user/features --- docs/user/features.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/user/features.md b/docs/user/features.md index eb81cba263..f89d8c4891 100644 --- a/docs/user/features.md +++ b/docs/user/features.md @@ -166,6 +166,20 @@ impl Foo for S { } ``` +- Apply [De Morgan's law](https://en.wikipedia.org/wiki/De_Morgan%27s_laws) + +```rust +// before: +fn example(x: bool) -> bool { + !x || !x +} + +// after: +fn example(x: bool) -> bool { + !(x && !x) +} +``` + - Import path ```rust From e06ad80d49f63c0c18a6447f39415a9ce900d9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BAc=C3=A1s=20Meier?= Date: Fri, 4 Oct 2019 12:45:22 +0200 Subject: [PATCH 6/6] Fix typo about De Morgan's law assist --- docs/user/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/features.md b/docs/user/features.md index f89d8c4891..0ce8f577b3 100644 --- a/docs/user/features.md +++ b/docs/user/features.md @@ -176,7 +176,7 @@ fn example(x: bool) -> bool { // after: fn example(x: bool) -> bool { - !(x && !x) + !(x && x) } ```