Skip to content

Commit 7ca45dc

Browse files
committed
Auto merge of rust-lang#15410 - alibektas:15240/invalid-demorgan, r=Veykril
internal : rewrite DeMorgan assist fixes rust-lang#15239 , rust-lang#15240 . This PR is a rewrite of the DeMorgan assist that essentially rids of all the string manipulation and modifies syntax trees to apply demorgan on a binary expr. The main reason for the rewrite is that I wanted to use `Expr::needs_parens_in` method to see if the expr on which the assist is applied would still need the parens it had once the parent expression's operator had equal precedence with that of the expression. I used `.clone_(subtree|for_update)` left and right and probably more than I should have, so I would also be happy to hear how I could have prevented redundant cloning.
2 parents c18ce9a + 17f3055 commit 7ca45dc

File tree

2 files changed

+108
-84
lines changed

2 files changed

+108
-84
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
use std::collections::VecDeque;
22

3-
use syntax::ast::{self, AstNode};
3+
use syntax::{
4+
ast::{self, AstNode, Expr::BinExpr},
5+
ted::{self, Position},
6+
SyntaxKind,
7+
};
48

59
use crate::{utils::invert_boolean_expression, AssistContext, AssistId, AssistKind, Assists};
610

@@ -23,131 +27,126 @@ use crate::{utils::invert_boolean_expression, AssistContext, AssistId, AssistKin
2327
// }
2428
// ```
2529
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();
30+
let mut bin_expr = ctx.find_node_at_offset::<ast::BinExpr>()?;
31+
let op = bin_expr.op_kind()?;
32+
let op_range = bin_expr.op_token()?.text_range();
2933

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 {
34+
// Is the cursor on the expression's logical operator?
35+
if !op_range.contains_range(ctx.selection_trimmed()) {
3836
return None;
3937
}
4038

41-
let mut expr = expr;
42-
4339
// 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() {
40+
while let Some(parent_expr) = bin_expr.syntax().parent().and_then(ast::BinExpr::cast) {
41+
match parent_expr.op_kind() {
4642
Some(parent_op) if parent_op == op => {
47-
expr = parent_expr;
43+
bin_expr = parent_expr;
4844
}
4945
_ => break,
5046
}
5147
}
5248

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-
}
49+
let op = bin_expr.op_kind()?;
50+
let inv_token = match op {
51+
ast::BinaryOp::LogicOp(ast::LogicOp::And) => SyntaxKind::PIPE2,
52+
ast::BinaryOp::LogicOp(ast::LogicOp::Or) => SyntaxKind::AMP2,
53+
_ => return None,
54+
};
55+
56+
let demorganed = bin_expr.clone_subtree().clone_for_update();
57+
58+
ted::replace(demorganed.op_token()?, ast::make::token(inv_token));
59+
let mut exprs = VecDeque::from(vec![
60+
(bin_expr.lhs()?, demorganed.lhs()?),
61+
(bin_expr.rhs()?, demorganed.rhs()?),
62+
]);
63+
64+
while let Some((expr, dm)) = exprs.pop_front() {
65+
if let BinExpr(bin_expr) = &expr {
66+
if let BinExpr(cbin_expr) = &dm {
67+
if op == bin_expr.op_kind()? {
68+
ted::replace(cbin_expr.op_token()?, ast::make::token(inv_token));
69+
exprs.push_back((bin_expr.lhs()?, cbin_expr.lhs()?));
70+
exprs.push_back((bin_expr.rhs()?, cbin_expr.rhs()?));
6771
} else {
68-
terms.push(ast::Expr::BinExpr(bin_expr));
72+
let mut inv = invert_boolean_expression(expr);
73+
if inv.needs_parens_in(dm.syntax().parent()?) {
74+
inv = ast::make::expr_paren(inv).clone_for_update();
75+
}
76+
ted::replace(dm.syntax(), inv.syntax());
6977
}
7078
} else {
71-
terms.push(expr);
79+
return None;
7280
}
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()?);
81+
} else {
82+
let mut inv = invert_boolean_expression(dm.clone_subtree()).clone_for_update();
83+
if inv.needs_parens_in(dm.syntax().parent()?) {
84+
inv = ast::make::expr_paren(inv).clone_for_update();
85+
}
86+
ted::replace(dm.syntax(), inv.syntax());
87+
}
7888
}
7989

90+
let dm_lhs = demorganed.lhs()?;
91+
8092
acc.add(
8193
AssistId("apply_demorgan", AssistKind::RefactorRewrite),
8294
"Apply De Morgan's law",
8395
op_range,
8496
|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-
97+
let paren_expr = bin_expr.syntax().parent().and_then(ast::ParenExpr::cast);
9098
let neg_expr = paren_expr
9199
.clone()
92100
.and_then(|paren_expr| paren_expr.syntax().parent())
93101
.and_then(ast::PrefixExpr::cast)
94102
.and_then(|prefix_expr| {
95-
if prefix_expr.op_kind().unwrap() == ast::UnaryOp::Not {
103+
if prefix_expr.op_kind()? == ast::UnaryOp::Not {
96104
Some(prefix_expr)
97105
} else {
98106
None
99107
}
100108
});
101109

102-
for op_range in op_ranges {
103-
edit.replace(op_range, opposite_op);
104-
}
105-
106110
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-
114111
if let Some(neg_expr) = neg_expr {
115112
cov_mark::hit!(demorgan_double_negation);
116-
edit.replace(neg_expr.op_token().unwrap().text_range(), "");
113+
edit.replace_ast(ast::Expr::PrefixExpr(neg_expr), demorganed.into());
117114
} else {
118115
cov_mark::hit!(demorgan_double_parens);
119-
edit.replace(paren_expr.l_paren_token().unwrap().text_range(), "!(");
116+
ted::insert_all_raw(
117+
Position::before(dm_lhs.syntax()),
118+
vec![
119+
syntax::NodeOrToken::Token(ast::make::token(SyntaxKind::BANG)),
120+
syntax::NodeOrToken::Token(ast::make::token(SyntaxKind::L_PAREN)),
121+
],
122+
);
123+
124+
ted::append_child_raw(
125+
demorganed.syntax(),
126+
syntax::NodeOrToken::Token(ast::make::token(SyntaxKind::R_PAREN)),
127+
);
128+
129+
edit.replace_ast(ast::Expr::ParenExpr(paren_expr), demorganed.into());
120130
}
121131
} 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}"));
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})"));
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.to_string());
140-
}
132+
ted::insert_all_raw(
133+
Position::before(dm_lhs.syntax()),
134+
vec![
135+
syntax::NodeOrToken::Token(ast::make::token(SyntaxKind::BANG)),
136+
syntax::NodeOrToken::Token(ast::make::token(SyntaxKind::L_PAREN)),
137+
],
138+
);
139+
ted::append_child_raw(demorganed.syntax(), ast::make::token(SyntaxKind::R_PAREN));
140+
edit.replace_ast(bin_expr, demorganed);
141141
}
142142
},
143143
)
144144
}
145145

146146
#[cfg(test)]
147147
mod tests {
148-
use crate::tests::{check_assist, check_assist_not_applicable};
149-
150148
use super::*;
149+
use crate::tests::{check_assist, check_assist_not_applicable};
151150

152151
#[test]
153152
fn demorgan_handles_leq() {
@@ -213,7 +212,7 @@ fn f() { !(S <= S || S < S) }
213212
#[test]
214213
fn demorgan_doesnt_double_negation() {
215214
cov_mark::check!(demorgan_double_negation);
216-
check_assist(apply_demorgan, "fn f() { !(x ||$0 x) }", "fn f() { (!x && !x) }")
215+
check_assist(apply_demorgan, "fn f() { !(x ||$0 x) }", "fn f() { !x && !x }")
217216
}
218217

219218
#[test]
@@ -222,13 +221,38 @@ fn f() { !(S <= S || S < S) }
222221
check_assist(apply_demorgan, "fn f() { (x ||$0 x) }", "fn f() { !(!x && !x) }")
223222
}
224223

225-
// https://github.com/rust-lang/rust-analyzer/issues/10963
224+
// FIXME : This needs to go.
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+
235+
#[test]
236+
fn demorgan_keep_pars_for_op_precedence() {
237+
check_assist(
238+
apply_demorgan,
239+
"fn main() {
240+
let _ = !(!a ||$0 !(b || c));
241+
}
242+
",
243+
"fn main() {
244+
let _ = a && (b || c);
245+
}
246+
",
247+
);
248+
}
249+
226250
#[test]
227-
fn demorgan_doesnt_hang() {
251+
fn demorgan_removes_pars_in_eq_precedence() {
228252
check_assist(
229253
apply_demorgan,
230-
"fn f() { 1 || 3 &&$0 4 || 5 }",
231-
"fn f() { !(!1 || !3 || !4) || 5 }",
254+
"fn() { let x = a && !(!b |$0| !c); }",
255+
"fn() { let x = a && b && c; }",
232256
)
233257
}
234258
}

crates/syntax/src/ast/make.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1100,7 +1100,7 @@ pub mod tokens {
11001100

11011101
pub(super) static SOURCE_FILE: Lazy<Parse<SourceFile>> = Lazy::new(|| {
11021102
SourceFile::parse(
1103-
"const C: <()>::Item = (1 != 1, 2 == 2, 3 < 3, 4 <= 4, 5 > 5, 6 >= 6, !true, *p, &p , &mut p)\n;\n\n",
1103+
"const C: <()>::Item = ( true && true , true || true , 1 != 1, 2 == 2, 3 < 3, 4 <= 4, 5 > 5, 6 >= 6, !true, *p, &p , &mut p)\n;\n\n",
11041104
)
11051105
});
11061106

0 commit comments

Comments
 (0)