]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/crates/ide-assists/src/handlers/apply_demorgan.rs
Rollup merge of #101420 - kraktus:doc_hir_local, r=cjgillot
[rust.git] / src / tools / rust-analyzer / crates / ide-assists / src / handlers / apply_demorgan.rs
1 use std::collections::VecDeque;
2
3 use syntax::ast::{self, AstNode};
4
5 use crate::{utils::invert_boolean_expression, AssistContext, AssistId, AssistKind, Assists};
6
7 // Assist: apply_demorgan
8 //
9 // Apply https://en.wikipedia.org/wiki/De_Morgan%27s_laws[De Morgan's law].
10 // This transforms expressions of the form `!l || !r` into `!(l && r)`.
11 // This also works with `&&`. This assist can only be applied with the cursor
12 // on either `||` or `&&`.
13 //
14 // ```
15 // fn main() {
16 //     if x != 4 ||$0 y < 3.14 {}
17 // }
18 // ```
19 // ->
20 // ```
21 // fn main() {
22 //     if !(x == 4 && y >= 3.14) {}
23 // }
24 // ```
25 pub(crate) fn apply_demorgan(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
26     let expr = ctx.find_node_at_offset::<ast::BinExpr>()?;
27     let op = expr.op_kind()?;
28     let op_range = expr.op_token()?.text_range();
29
30     let opposite_op = match op {
31         ast::BinaryOp::LogicOp(ast::LogicOp::And) => "||",
32         ast::BinaryOp::LogicOp(ast::LogicOp::Or) => "&&",
33         _ => return None,
34     };
35
36     let cursor_in_range = op_range.contains_range(ctx.selection_trimmed());
37     if !cursor_in_range {
38         return None;
39     }
40
41     let mut expr = expr;
42
43     // Walk up the tree while we have the same binary operator
44     while let Some(parent_expr) = expr.syntax().parent().and_then(ast::BinExpr::cast) {
45         match expr.op_kind() {
46             Some(parent_op) if parent_op == op => {
47                 expr = parent_expr;
48             }
49             _ => break,
50         }
51     }
52
53     let mut expr_stack = vec![expr.clone()];
54     let mut terms = Vec::new();
55     let mut op_ranges = Vec::new();
56
57     // Find all the children with the same binary operator
58     while let Some(expr) = expr_stack.pop() {
59         let mut traverse_bin_expr_arm = |expr| {
60             if let ast::Expr::BinExpr(bin_expr) = expr {
61                 if let Some(expr_op) = bin_expr.op_kind() {
62                     if expr_op == op {
63                         expr_stack.push(bin_expr);
64                     } else {
65                         terms.push(ast::Expr::BinExpr(bin_expr));
66                     }
67                 } else {
68                     terms.push(ast::Expr::BinExpr(bin_expr));
69                 }
70             } else {
71                 terms.push(expr);
72             }
73         };
74
75         op_ranges.extend(expr.op_token().map(|t| t.text_range()));
76         traverse_bin_expr_arm(expr.lhs()?);
77         traverse_bin_expr_arm(expr.rhs()?);
78     }
79
80     acc.add(
81         AssistId("apply_demorgan", AssistKind::RefactorRewrite),
82         "Apply De Morgan's law",
83         op_range,
84         |edit| {
85             terms.sort_by_key(|t| t.syntax().text_range().start());
86             let mut terms = VecDeque::from(terms);
87
88             let paren_expr = expr.syntax().parent().and_then(ast::ParenExpr::cast);
89
90             let neg_expr = paren_expr
91                 .clone()
92                 .and_then(|paren_expr| paren_expr.syntax().parent())
93                 .and_then(ast::PrefixExpr::cast)
94                 .and_then(|prefix_expr| {
95                     if prefix_expr.op_kind().unwrap() == ast::UnaryOp::Not {
96                         Some(prefix_expr)
97                     } else {
98                         None
99                     }
100                 });
101
102             for op_range in op_ranges {
103                 edit.replace(op_range, opposite_op);
104             }
105
106             if let Some(paren_expr) = paren_expr {
107                 for term in terms {
108                     let range = term.syntax().text_range();
109                     let not_term = invert_boolean_expression(term);
110
111                     edit.replace(range, not_term.syntax().text());
112                 }
113
114                 if let Some(neg_expr) = neg_expr {
115                     cov_mark::hit!(demorgan_double_negation);
116                     edit.replace(neg_expr.op_token().unwrap().text_range(), "");
117                 } else {
118                     cov_mark::hit!(demorgan_double_parens);
119                     edit.replace(paren_expr.l_paren_token().unwrap().text_range(), "!(");
120                 }
121             } else {
122                 if let Some(lhs) = terms.pop_front() {
123                     let lhs_range = lhs.syntax().text_range();
124                     let not_lhs = invert_boolean_expression(lhs);
125
126                     edit.replace(lhs_range, format!("!({}", not_lhs.syntax().text()));
127                 }
128
129                 if let Some(rhs) = terms.pop_back() {
130                     let rhs_range = rhs.syntax().text_range();
131                     let not_rhs = invert_boolean_expression(rhs);
132
133                     edit.replace(rhs_range, format!("{})", not_rhs.syntax().text()));
134                 }
135
136                 for term in terms {
137                     let term_range = term.syntax().text_range();
138                     let not_term = invert_boolean_expression(term);
139                     edit.replace(term_range, not_term.syntax().text());
140                 }
141             }
142         },
143     )
144 }
145
146 #[cfg(test)]
147 mod tests {
148     use crate::tests::{check_assist, check_assist_not_applicable};
149
150     use super::*;
151
152     #[test]
153     fn demorgan_handles_leq() {
154         check_assist(
155             apply_demorgan,
156             r#"
157 struct S;
158 fn f() { S < S &&$0 S <= S }
159 "#,
160             r#"
161 struct S;
162 fn f() { !(S >= S || S > S) }
163 "#,
164         );
165     }
166
167     #[test]
168     fn demorgan_handles_geq() {
169         check_assist(
170             apply_demorgan,
171             r#"
172 struct S;
173 fn f() { S > S &&$0 S >= S }
174 "#,
175             r#"
176 struct S;
177 fn f() { !(S <= S || S < S) }
178 "#,
179         );
180     }
181
182     #[test]
183     fn demorgan_turns_and_into_or() {
184         check_assist(apply_demorgan, "fn f() { !x &&$0 !x }", "fn f() { !(x || x) }")
185     }
186
187     #[test]
188     fn demorgan_turns_or_into_and() {
189         check_assist(apply_demorgan, "fn f() { !x ||$0 !x }", "fn f() { !(x && x) }")
190     }
191
192     #[test]
193     fn demorgan_removes_inequality() {
194         check_assist(apply_demorgan, "fn f() { x != x ||$0 !x }", "fn f() { !(x == x && x) }")
195     }
196
197     #[test]
198     fn demorgan_general_case() {
199         check_assist(apply_demorgan, "fn f() { x ||$0 x }", "fn f() { !(!x && !x) }")
200     }
201
202     #[test]
203     fn demorgan_multiple_terms() {
204         check_assist(apply_demorgan, "fn f() { x ||$0 y || z }", "fn f() { !(!x && !y && !z) }");
205         check_assist(apply_demorgan, "fn f() { x || y ||$0 z }", "fn f() { !(!x && !y && !z) }");
206     }
207
208     #[test]
209     fn demorgan_doesnt_apply_with_cursor_not_on_op() {
210         check_assist_not_applicable(apply_demorgan, "fn f() { $0 !x || !x }")
211     }
212
213     #[test]
214     fn demorgan_doesnt_double_negation() {
215         cov_mark::check!(demorgan_double_negation);
216         check_assist(apply_demorgan, "fn f() { !(x ||$0 x) }", "fn f() { (!x && !x) }")
217     }
218
219     #[test]
220     fn demorgan_doesnt_double_parens() {
221         cov_mark::check!(demorgan_double_parens);
222         check_assist(apply_demorgan, "fn f() { (x ||$0 x) }", "fn f() { !(!x && !x) }")
223     }
224
225     // https://github.com/rust-lang/rust-analyzer/issues/10963
226     #[test]
227     fn demorgan_doesnt_hang() {
228         check_assist(
229             apply_demorgan,
230             "fn f() { 1 || 3 &&$0 4 || 5 }",
231             "fn f() { !(!1 || !3 || !4) || 5 }",
232         )
233     }
234 }