Skip to content

Commit f2246fe

Browse files
bors[bot]Veykril
andauthored
Merge #9837
9837: feat: Implement `bool_then_to_if` assist r=Veykril a=Veykril Other half of #8413 Co-authored-by: Lukas Wirth <lukastw97@gmail.com>
2 parents c5942c5 + b7d7dd6 commit f2246fe

File tree

5 files changed

+241
-3
lines changed

5 files changed

+241
-3
lines changed

crates/ide_assists/src/handlers/convert_bool_then.rs

Lines changed: 202 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
use hir::{known, Semantics};
1+
use hir::{known, AsAssocItem, Semantics};
22
use ide_db::{
33
helpers::{for_each_tail_expr, FamousDefs},
44
RootDatabase,
55
};
6+
use itertools::Itertools;
67
use syntax::{
7-
ast::{self, make, ArgListOwner},
8+
ast::{self, edit::AstNodeEdit, make, ArgListOwner},
89
ted, AstNode, SyntaxNode,
910
};
1011

@@ -75,6 +76,7 @@ pub(crate) fn convert_if_to_bool_then(acc: &mut Assists, ctx: &AssistContext) ->
7576
|builder| {
7677
let closure_body = closure_body.clone_for_update();
7778
// Rewrite all `Some(e)` in tail position to `e`
79+
let mut replacements = Vec::new();
7880
for_each_tail_expr(&closure_body, &mut |e| {
7981
let e = match e {
8082
ast::Expr::BreakExpr(e) => e.expr(),
@@ -84,11 +86,12 @@ pub(crate) fn convert_if_to_bool_then(acc: &mut Assists, ctx: &AssistContext) ->
8486
if let Some(ast::Expr::CallExpr(call)) = e {
8587
if let Some(arg_list) = call.arg_list() {
8688
if let Some(arg) = arg_list.args().next() {
87-
ted::replace(call.syntax(), arg.syntax());
89+
replacements.push((call.syntax().clone(), arg.syntax().clone()));
8890
}
8991
}
9092
}
9193
});
94+
replacements.into_iter().for_each(|(old, new)| ted::replace(old, new));
9295
let closure_body = match closure_body {
9396
ast::Expr::BlockExpr(block) => unwrap_trivial_block(block),
9497
e => e,
@@ -102,6 +105,95 @@ pub(crate) fn convert_if_to_bool_then(acc: &mut Assists, ctx: &AssistContext) ->
102105
)
103106
}
104107

108+
// Assist: convert_bool_then_to_if
109+
//
110+
// Converts a `bool::then` method call to an equivalent if expression.
111+
//
112+
// ```
113+
// # //- minicore: bool_impl
114+
// fn main() {
115+
// (0 == 0).then$0(|| val)
116+
// }
117+
// ```
118+
// ->
119+
// ```
120+
// fn main() {
121+
// if 0 == 0 {
122+
// Some(val)
123+
// } else {
124+
// None
125+
// }
126+
// }
127+
// ```
128+
pub(crate) fn convert_bool_then_to_if(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
129+
let name_ref = ctx.find_node_at_offset::<ast::NameRef>()?;
130+
let mcall = name_ref.syntax().parent().and_then(ast::MethodCallExpr::cast)?;
131+
let receiver = mcall.receiver()?;
132+
let closure_body = mcall.arg_list()?.args().exactly_one().ok()?;
133+
let closure_body = match closure_body {
134+
ast::Expr::ClosureExpr(expr) => expr.body()?,
135+
_ => return None,
136+
};
137+
// Verify this is `bool::then` that is being called.
138+
let func = ctx.sema.resolve_method_call(&mcall)?;
139+
if func.name(ctx.sema.db).to_string() != "then" {
140+
return None;
141+
}
142+
let assoc = func.as_assoc_item(ctx.sema.db)?;
143+
match assoc.container(ctx.sema.db) {
144+
hir::AssocItemContainer::Impl(impl_) if impl_.self_ty(ctx.sema.db).is_bool() => {}
145+
_ => return None,
146+
}
147+
148+
let target = mcall.syntax().text_range();
149+
acc.add(
150+
AssistId("convert_bool_then_to_if", AssistKind::RefactorRewrite),
151+
"Convert `bool::then` call to `if`",
152+
target,
153+
|builder| {
154+
let closure_body = match closure_body {
155+
ast::Expr::BlockExpr(block) => block,
156+
e => make::block_expr(None, Some(e)),
157+
};
158+
159+
let closure_body = closure_body.clone_for_update();
160+
// Wrap all tails in `Some(...)`
161+
let none_path = make::expr_path(make::ext::ident_path("None"));
162+
let some_path = make::expr_path(make::ext::ident_path("Some"));
163+
let mut replacements = Vec::new();
164+
for_each_tail_expr(&ast::Expr::BlockExpr(closure_body.clone()), &mut |e| {
165+
let e = match e {
166+
ast::Expr::BreakExpr(e) => e.expr(),
167+
ast::Expr::ReturnExpr(e) => e.expr(),
168+
_ => Some(e.clone()),
169+
};
170+
if let Some(expr) = e {
171+
replacements.push((
172+
expr.syntax().clone(),
173+
make::expr_call(some_path.clone(), make::arg_list(Some(expr)))
174+
.syntax()
175+
.clone_for_update(),
176+
));
177+
}
178+
});
179+
replacements.into_iter().for_each(|(old, new)| ted::replace(old, new));
180+
181+
let cond = match &receiver {
182+
ast::Expr::ParenExpr(expr) => expr.expr().unwrap_or(receiver),
183+
_ => receiver,
184+
};
185+
let if_expr = make::expr_if(
186+
make::condition(cond, None),
187+
closure_body.reset_indent(),
188+
Some(ast::ElseBranch::Block(make::block_expr(None, Some(none_path)))),
189+
)
190+
.indent(mcall.indent_level());
191+
192+
builder.replace(target, if_expr.to_string());
193+
},
194+
)
195+
}
196+
105197
fn option_variants(
106198
sema: &Semantics<RootDatabase>,
107199
expr: &SyntaxNode,
@@ -346,6 +438,113 @@ fn main() {
346438
None
347439
}
348440
}
441+
",
442+
);
443+
}
444+
445+
#[test]
446+
fn convert_bool_then_to_if_inapplicable() {
447+
check_assist_not_applicable(
448+
convert_bool_then_to_if,
449+
r"
450+
//- minicore:bool_impl
451+
fn main() {
452+
0.t$0hen(|| 15);
453+
}
454+
",
455+
);
456+
check_assist_not_applicable(
457+
convert_bool_then_to_if,
458+
r"
459+
//- minicore:bool_impl
460+
fn main() {
461+
true.t$0hen(15);
462+
}
463+
",
464+
);
465+
check_assist_not_applicable(
466+
convert_bool_then_to_if,
467+
r"
468+
//- minicore:bool_impl
469+
fn main() {
470+
true.t$0hen(|| 15, 15);
471+
}
472+
",
473+
);
474+
}
475+
476+
#[test]
477+
fn convert_bool_then_to_if_simple() {
478+
check_assist(
479+
convert_bool_then_to_if,
480+
r"
481+
//- minicore:bool_impl
482+
fn main() {
483+
true.t$0hen(|| 15)
484+
}
485+
",
486+
r"
487+
fn main() {
488+
if true {
489+
Some(15)
490+
} else {
491+
None
492+
}
493+
}
494+
",
495+
);
496+
check_assist(
497+
convert_bool_then_to_if,
498+
r"
499+
//- minicore:bool_impl
500+
fn main() {
501+
true.t$0hen(|| {
502+
15
503+
})
504+
}
505+
",
506+
r"
507+
fn main() {
508+
if true {
509+
Some(15)
510+
} else {
511+
None
512+
}
513+
}
514+
",
515+
);
516+
}
517+
518+
#[test]
519+
fn convert_bool_then_to_if_tails() {
520+
check_assist(
521+
convert_bool_then_to_if,
522+
r"
523+
//- minicore:bool_impl
524+
fn main() {
525+
true.t$0hen(|| {
526+
loop {
527+
if false {
528+
break 0;
529+
}
530+
break 15;
531+
}
532+
})
533+
}
534+
",
535+
r"
536+
fn main() {
537+
if true {
538+
loop {
539+
if false {
540+
break Some(0);
541+
}
542+
break Some(15);
543+
}
544+
} else {
545+
None
546+
}
547+
}
349548
",
350549
);
351550
}

crates/ide_assists/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ mod handlers {
127127
auto_import::auto_import,
128128
change_visibility::change_visibility,
129129
convert_bool_then::convert_if_to_bool_then,
130+
convert_bool_then::convert_bool_then_to_if,
130131
convert_comment_block::convert_comment_block,
131132
convert_integer_literal::convert_integer_literal,
132133
convert_into_to_from::convert_into_to_from,

crates/ide_assists/src/tests/generated.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,28 @@ pub(crate) fn frobnicate() {}
191191
)
192192
}
193193

194+
#[test]
195+
fn doctest_convert_bool_then_to_if() {
196+
check_doc_test(
197+
"convert_bool_then_to_if",
198+
r#####"
199+
//- minicore: bool_impl
200+
fn main() {
201+
(0 == 0).then$0(|| val)
202+
}
203+
"#####,
204+
r#####"
205+
fn main() {
206+
if 0 == 0 {
207+
Some(val)
208+
} else {
209+
None
210+
}
211+
}
212+
"#####,
213+
)
214+
}
215+
194216
#[test]
195217
fn doctest_convert_if_to_bool_then() {
196218
check_doc_test(

crates/ide_db/src/helpers.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ impl SnippetCap {
247247

248248
/// Calls `cb` on each expression inside `expr` that is at "tail position".
249249
/// Does not walk into `break` or `return` expressions.
250+
/// Note that modifying the tree while iterating it will cause undefined iteration which might
251+
/// potentially results in an out of bounds panic.
250252
pub fn for_each_tail_expr(expr: &ast::Expr, cb: &mut dyn FnMut(&ast::Expr)) {
251253
match expr {
252254
ast::Expr::BlockExpr(b) => {

crates/test_utils/src/minicore.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
//! ord: eq, option
3434
//! derive:
3535
//! fmt: result
36+
//! bool_impl: option, fn
3637
3738
pub mod marker {
3839
// region:sized
@@ -568,6 +569,19 @@ mod macros {
568569
}
569570
// endregion:derive
570571

572+
// region:bool_impl
573+
#[lang = "bool"]
574+
impl bool {
575+
pub fn then<T, F: FnOnce() -> T>(self, f: F) -> Option<T> {
576+
if self {
577+
Some(f())
578+
} else {
579+
None
580+
}
581+
}
582+
}
583+
// endregion:bool_impl
584+
571585
pub mod prelude {
572586
pub mod v1 {
573587
pub use crate::{

0 commit comments

Comments
 (0)