Skip to content

Commit dbf869b

Browse files
Merge #1952
1952: Create an assist for applying De Morgan's Law r=matklad a=cronokirby 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. Co-authored-by: Lúcás Meier <[email protected]>
2 parents cce3271 + e06ad80 commit dbf869b

File tree

3 files changed

+118
-0
lines changed

3 files changed

+118
-0
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//! This contains the functions associated with the demorgan assist.
2+
//! This assist transforms boolean expressions of the form `!a || !b` into
3+
//! `!(a && b)`.
4+
use hir::db::HirDatabase;
5+
use ra_syntax::ast::{self, AstNode};
6+
use ra_syntax::SyntaxNode;
7+
8+
use crate::{Assist, AssistCtx, AssistId};
9+
10+
/// Assist for applying demorgan's law
11+
///
12+
/// This transforms expressions of the form `!l || !r` into `!(l && r)`.
13+
/// This also works with `&&`. This assist can only be applied with the cursor
14+
/// on either `||` or `&&`, with both operands being a negation of some kind.
15+
/// This means something of the form `!x` or `x != y`.
16+
pub(crate) fn apply_demorgan(mut ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> {
17+
let expr = ctx.node_at_offset::<ast::BinExpr>()?;
18+
let op = expr.op_kind()?;
19+
let op_range = expr.op_token()?.text_range();
20+
let opposite_op = opposite_logic_op(op)?;
21+
let cursor_in_range = ctx.frange.range.is_subrange(&op_range);
22+
if !cursor_in_range {
23+
return None;
24+
}
25+
let lhs = expr.lhs()?.syntax().clone();
26+
let lhs_range = lhs.text_range();
27+
let rhs = expr.rhs()?.syntax().clone();
28+
let rhs_range = rhs.text_range();
29+
let not_lhs = undo_negation(lhs)?;
30+
let not_rhs = undo_negation(rhs)?;
31+
32+
ctx.add_action(AssistId("apply_demorgan"), "apply demorgan's law", |edit| {
33+
edit.target(op_range);
34+
edit.replace(op_range, opposite_op);
35+
edit.replace(lhs_range, format!("!({}", not_lhs));
36+
edit.replace(rhs_range, format!("{})", not_rhs));
37+
});
38+
ctx.build()
39+
}
40+
41+
// Return the opposite text for a given logical operator, if it makes sense
42+
fn opposite_logic_op(kind: ast::BinOp) -> Option<&'static str> {
43+
match kind {
44+
ast::BinOp::BooleanOr => Some("&&"),
45+
ast::BinOp::BooleanAnd => Some("||"),
46+
_ => None,
47+
}
48+
}
49+
50+
// This function tries to undo unary negation, or inequality
51+
fn undo_negation(node: SyntaxNode) -> Option<String> {
52+
match ast::Expr::cast(node)? {
53+
ast::Expr::BinExpr(bin) => match bin.op_kind()? {
54+
ast::BinOp::NegatedEqualityTest => {
55+
let lhs = bin.lhs()?.syntax().text();
56+
let rhs = bin.rhs()?.syntax().text();
57+
Some(format!("{} == {}", lhs, rhs))
58+
}
59+
_ => None,
60+
},
61+
ast::Expr::PrefixExpr(pe) => match pe.op_kind()? {
62+
ast::PrefixOp::Not => {
63+
let child = pe.expr()?.syntax().text();
64+
Some(String::from(child))
65+
}
66+
_ => None,
67+
},
68+
_ => None,
69+
}
70+
}
71+
72+
#[cfg(test)]
73+
mod tests {
74+
use super::*;
75+
76+
use crate::helpers::{check_assist, check_assist_not_applicable};
77+
78+
#[test]
79+
fn demorgan_turns_and_into_or() {
80+
check_assist(apply_demorgan, "fn f() { !x &&<|> !x }", "fn f() { !(x ||<|> x) }")
81+
}
82+
83+
#[test]
84+
fn demorgan_turns_or_into_and() {
85+
check_assist(apply_demorgan, "fn f() { !x ||<|> !x }", "fn f() { !(x &&<|> x) }")
86+
}
87+
88+
#[test]
89+
fn demorgan_removes_inequality() {
90+
check_assist(apply_demorgan, "fn f() { x != x ||<|> !x }", "fn f() { !(x == x &&<|> x) }")
91+
}
92+
93+
#[test]
94+
fn demorgan_doesnt_apply_with_cursor_not_on_op() {
95+
check_assist_not_applicable(apply_demorgan, "fn f() { <|> !x || !x }")
96+
}
97+
98+
#[test]
99+
fn demorgan_doesnt_apply_when_operands_arent_negated_already() {
100+
check_assist_not_applicable(apply_demorgan, "fn f() { x ||<|> x }")
101+
}
102+
}

crates/ra_assists/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ mod assists {
9292
mod add_derive;
9393
mod add_explicit_type;
9494
mod add_impl;
95+
mod apply_demorgan;
9596
mod flip_comma;
9697
mod flip_binexpr;
9798
mod change_visibility;
@@ -113,6 +114,7 @@ mod assists {
113114
add_derive::add_derive,
114115
add_explicit_type::add_explicit_type,
115116
add_impl::add_impl,
117+
apply_demorgan::apply_demorgan,
116118
change_visibility::change_visibility,
117119
fill_match_arms::fill_match_arms,
118120
merge_match_arms::merge_match_arms,

docs/user/features.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,20 @@ impl Foo for S {
166166
}
167167
```
168168

169+
- Apply [De Morgan's law](https://en.wikipedia.org/wiki/De_Morgan%27s_laws)
170+
171+
```rust
172+
// before:
173+
fn example(x: bool) -> bool {
174+
!x || !x
175+
}
176+
177+
// after:
178+
fn example(x: bool) -> bool {
179+
!(x && x)
180+
}
181+
```
182+
169183
- Import path
170184

171185
```rust

0 commit comments

Comments
 (0)