use either::Either;
use hir::{CaptureKind, ClosureCapture, FileRangeWrapper, HirDisplay};
use ide_db::{
assists::{AssistId, AssistKind},
base_db::SourceDatabase,
defs::Definition,
search::FileReferenceNode,
source_change::SourceChangeBuilder,
FxHashSet,
};
use stdx::format_to;
use syntax::{
algo::{skip_trivia_token, skip_whitespace_token},
ast::{
self,
edit::{AstNodeEdit, IndentLevel},
make, HasArgList, HasGenericParams, HasName,
},
hacks::parse_expr_from_str,
ted, AstNode, Direction, SyntaxKind, SyntaxNode, TextSize, ToSmolStr, T,
};
use crate::assist_context::{AssistContext, Assists};
pub(crate) fn convert_closure_to_fn(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let closure = ctx.find_node_at_offset::<ast::ClosureExpr>()?;
if ctx.find_node_at_offset::<ast::Expr>() != Some(ast::Expr::ClosureExpr(closure.clone())) {
return None;
}
let closure_name = closure.syntax().parent().and_then(|parent| {
let closure_decl = ast::LetStmt::cast(parent)?;
match closure_decl.pat()? {
ast::Pat::IdentPat(pat) => Some((closure_decl, pat.clone(), pat.name()?)),
_ => None,
}
});
let module = ctx.sema.scope(closure.syntax())?.module();
let closure_ty = ctx.sema.type_of_expr(&closure.clone().into())?;
let callable = closure_ty.original.as_callable(ctx.db())?;
let closure_ty = closure_ty.original.as_closure()?;
let mut ret_ty = callable.return_type();
let mut closure_mentioned_generic_params = ret_ty.generic_params(ctx.db());
let mut params = callable
.params()
.into_iter()
.map(|param| {
let node = ctx.sema.source(param.clone())?.value.right()?;
let param_ty = param.ty();
closure_mentioned_generic_params.extend(param_ty.generic_params(ctx.db()));
match node.ty() {
Some(_) => Some(node),
None => {
let ty = param_ty
.display_source_code(ctx.db(), module.into(), true)
.unwrap_or_else(|_| "_".to_owned());
Some(make::param(node.pat()?, make::ty(&ty)))
}
}
})
.collect::<Option<Vec<_>>>()?;
let mut body = closure.body()?.clone_for_update();
let mut is_gen = false;
let mut is_async = closure.async_token().is_some();
if is_async {
ret_ty = ret_ty.future_output(ctx.db())?;
}
let mut wrap_body_in_block = true;
if let ast::Expr::BlockExpr(block) = &body {
if let Some(async_token) = block.async_token() {
if !is_async {
is_async = true;
ret_ty = ret_ty.future_output(ctx.db())?;
let token_idx = async_token.index();
let whitespace_tokens_after_count = async_token
.siblings_with_tokens(Direction::Next)
.skip(1)
.take_while(|token| token.kind() == SyntaxKind::WHITESPACE)
.count();
body.syntax().splice_children(
token_idx..token_idx + whitespace_tokens_after_count + 1,
Vec::new(),
);
}
}
if let Some(gen_token) = block.gen_token() {
is_gen = true;
ret_ty = ret_ty.iterator_item(ctx.db())?;
let token_idx = gen_token.index();
let whitespace_tokens_after_count = gen_token
.siblings_with_tokens(Direction::Next)
.skip(1)
.take_while(|token| token.kind() == SyntaxKind::WHITESPACE)
.count();
body.syntax().splice_children(
token_idx..token_idx + whitespace_tokens_after_count + 1,
Vec::new(),
);
}
if block.try_token().is_none()
&& block.unsafe_token().is_none()
&& block.label().is_none()
&& block.const_token().is_none()
&& block.async_token().is_none()
{
wrap_body_in_block = false;
}
};
acc.add(
AssistId("convert_closure_to_fn", AssistKind::RefactorRewrite),
"Convert closure to fn",
closure.param_list()?.syntax().text_range(),
|builder| {
let closure_name_or_default = closure_name
.as_ref()
.map(|(_, _, it)| it.clone())
.unwrap_or_else(|| make::name("fun_name"));
let captures = closure_ty.captured_items(ctx.db());
let capture_tys = closure_ty.capture_types(ctx.db());
let mut captures_as_args = Vec::with_capacity(captures.len());
let body_root = body.syntax().ancestors().last().unwrap();
let mut capture_usages_replacement_map = Vec::with_capacity(captures.len());
for (capture, capture_ty) in std::iter::zip(&captures, &capture_tys) {
let capture_name =
if capture.local().is_self(ctx.db()) && !capture.has_field_projections() {
make::name("this")
} else {
make::name(&capture.place_to_name(ctx.db()))
};
closure_mentioned_generic_params.extend(capture_ty.generic_params(ctx.db()));
let capture_ty = capture_ty
.display_source_code(ctx.db(), module.into(), true)
.unwrap_or_else(|_| "_".to_owned());
params.push(make::param(
ast::Pat::IdentPat(make::ident_pat(false, false, capture_name.clone_subtree())),
make::ty(&capture_ty),
));
for capture_usage in capture.usages().sources(ctx.db()) {
if capture_usage.file_id() != ctx.file_id() {
continue;
}
let capture_usage_source = capture_usage.source();
let capture_usage_source = capture_usage_source.to_node(&body_root);
let expr = match capture_usage_source {
Either::Left(expr) => expr,
Either::Right(pat) => {
let Some(expr) = expr_of_pat(pat) else { continue };
expr
}
};
let replacement = wrap_capture_in_deref_if_needed(
&expr,
&capture_name,
capture.kind(),
capture_usage.is_ref(),
)
.clone_for_update();
capture_usages_replacement_map.push((expr, replacement));
}
captures_as_args.push(capture_as_arg(ctx, capture));
}
let (closure_type_params, closure_where_clause) =
compute_closure_type_params(ctx, closure_mentioned_generic_params, &closure);
for (old, new) in capture_usages_replacement_map {
if old == body {
body = new;
} else {
ted::replace(old.syntax(), new.syntax());
}
}
let body = if wrap_body_in_block {
make::block_expr([], Some(body))
} else {
ast::BlockExpr::cast(body.syntax().clone()).unwrap()
};
let params = make::param_list(None, params);
let ret_ty = if ret_ty.is_unit() {
None
} else {
let ret_ty = ret_ty
.display_source_code(ctx.db(), module.into(), true)
.unwrap_or_else(|_| "_".to_owned());
Some(make::ret_type(make::ty(&ret_ty)))
};
let mut fn_ = make::fn_(
None,
closure_name_or_default.clone(),
closure_type_params,
closure_where_clause,
params,
body,
ret_ty,
is_async,
false,
false,
is_gen,
);
fn_ = fn_.dedent(IndentLevel::from_token(&fn_.syntax().last_token().unwrap()));
builder.edit_file(ctx.file_id());
match &closure_name {
Some((closure_decl, _, _)) => {
fn_ = fn_.indent(closure_decl.indent_level());
builder.replace(closure_decl.syntax().text_range(), fn_.to_string());
}
None => {
let Some(top_stmt) =
closure.syntax().ancestors().skip(1).find_map(|ancestor| {
ast::Stmt::cast(ancestor.clone()).map(Either::Left).or_else(|| {
ast::ClosureExpr::cast(ancestor.clone())
.map(Either::Left)
.or_else(|| ast::BlockExpr::cast(ancestor).map(Either::Right))
.map(Either::Right)
})
})
else {
return;
};
builder.replace(
closure.syntax().text_range(),
closure_name_or_default.to_string(),
);
match top_stmt {
Either::Left(stmt) => {
let indent = stmt.indent_level();
fn_ = fn_.indent(indent);
let range = stmt
.syntax()
.first_token()
.and_then(|token| {
skip_whitespace_token(token.prev_token()?, Direction::Prev)
})
.map(|it| it.text_range().end())
.unwrap_or_else(|| stmt.syntax().text_range().start());
builder.insert(range, format!("\n{indent}{fn_}"));
}
Either::Right(Either::Left(closure_inside_closure)) => {
let Some(closure_body) = closure_inside_closure.body() else { return };
builder.insert(
closure_body.syntax().text_range().start(),
format!("{{ {fn_} "),
);
builder
.insert(closure_body.syntax().text_range().end(), " }".to_owned());
}
Either::Right(Either::Right(block_expr)) => {
let Some(tail_expr) = block_expr.tail_expr() else { return };
let Some(insert_in) =
tail_expr.syntax().first_token().and_then(|token| {
skip_whitespace_token(token.prev_token()?, Direction::Prev)
})
else {
return;
};
let indent = tail_expr.indent_level();
fn_ = fn_.indent(indent);
builder
.insert(insert_in.text_range().end(), format!("\n{indent}{fn_}"));
}
}
}
}
handle_calls(
builder,
ctx,
closure_name.as_ref().map(|(_, it, _)| it),
&captures_as_args,
&closure,
);
},
)?;
Some(())
}
fn compute_closure_type_params(
ctx: &AssistContext<'_>,
mentioned_generic_params: FxHashSet<hir::GenericParam>,
closure: &ast::ClosureExpr,
) -> (Option<ast::GenericParamList>, Option<ast::WhereClause>) {
if mentioned_generic_params.is_empty() {
return (None, None);
}
let mut mentioned_names = mentioned_generic_params
.iter()
.filter_map(|param| match param {
hir::GenericParam::TypeParam(param) => {
Some(param.name(ctx.db()).unescaped().display(ctx.db()).to_smolstr())
}
hir::GenericParam::ConstParam(param) => {
Some(param.name(ctx.db()).unescaped().display(ctx.db()).to_smolstr())
}
hir::GenericParam::LifetimeParam(_) => None,
})
.collect::<FxHashSet<_>>();
let Some((container_params, container_where, container)) =
closure.syntax().ancestors().find_map(ast::AnyHasGenericParams::cast).and_then(
|container| {
Some((container.generic_param_list()?, container.where_clause(), container))
},
)
else {
return (None, None);
};
let containing_impl = if ast::AssocItem::can_cast(container.syntax().kind()) {
container
.syntax()
.ancestors()
.find_map(ast::Impl::cast)
.and_then(|impl_| Some((impl_.generic_param_list()?, impl_.where_clause())))
} else {
None
};
let all_params = container_params
.type_or_const_params()
.chain(containing_impl.iter().flat_map(|(param_list, _)| param_list.type_or_const_params()))
.filter_map(|param| Some(param.name()?.text().to_smolstr()))
.collect::<FxHashSet<_>>();
let mut reached_fixpoint = false;
let mut container_where_bounds_indices = Vec::new();
let mut impl_where_bounds_indices = Vec::new();
while !reached_fixpoint {
reached_fixpoint = true;
let mut insert_name = |syntax: &SyntaxNode| {
let has_name = syntax
.descendants()
.filter_map(ast::NameOrNameRef::cast)
.any(|name| mentioned_names.contains(&*name.text()));
let mut has_new_params = false;
if has_name {
syntax
.descendants()
.filter_map(ast::NameOrNameRef::cast)
.filter(|name| all_params.contains(name.text().trim_start_matches("r#")))
.for_each(|name| {
if mentioned_names.insert(name.text().trim_start_matches("r#").to_smolstr())
{
has_new_params = true;
reached_fixpoint = false;
}
});
}
has_new_params
};
for param in container_params.type_or_const_params() {
insert_name(param.syntax());
}
for (pred_index, pred) in container_where.iter().flat_map(|it| it.predicates()).enumerate()
{
if insert_name(pred.syntax()) {
container_where_bounds_indices.push(pred_index);
}
}
if let Some((impl_params, impl_where)) = &containing_impl {
for param in impl_params.type_or_const_params() {
insert_name(param.syntax());
}
for (pred_index, pred) in impl_where.iter().flat_map(|it| it.predicates()).enumerate() {
if insert_name(pred.syntax()) {
impl_where_bounds_indices.push(pred_index);
}
}
}
}
let include_params = containing_impl
.iter()
.flat_map(|(impl_params, _)| {
impl_params.type_or_const_params().filter(|param| {
param.name().is_some_and(|name| {
mentioned_names.contains(name.text().trim_start_matches("r#"))
})
})
})
.chain(container_params.type_or_const_params().filter(|param| {
param
.name()
.is_some_and(|name| mentioned_names.contains(name.text().trim_start_matches("r#")))
}))
.map(ast::TypeOrConstParam::into);
let include_where_bounds = containing_impl
.as_ref()
.and_then(|(_, it)| it.as_ref())
.into_iter()
.flat_map(|where_| {
impl_where_bounds_indices.iter().filter_map(|&index| where_.predicates().nth(index))
})
.chain(container_where.iter().flat_map(|where_| {
container_where_bounds_indices
.iter()
.filter_map(|&index| where_.predicates().nth(index))
}))
.collect::<Vec<_>>();
let where_clause =
(!include_where_bounds.is_empty()).then(|| make::where_clause(include_where_bounds));
(Some(make::generic_param_list(include_params)), where_clause)
}
fn wrap_capture_in_deref_if_needed(
expr: &ast::Expr,
capture_name: &ast::Name,
capture_kind: CaptureKind,
is_ref: bool,
) -> ast::Expr {
fn peel_parens(mut expr: ast::Expr) -> ast::Expr {
loop {
if ast::ParenExpr::can_cast(expr.syntax().kind()) {
let Some(parent) = expr.syntax().parent().and_then(ast::Expr::cast) else { break };
expr = parent;
} else {
break;
}
}
expr
}
let capture_name = make::expr_path(make::path_from_text(&capture_name.text()));
if capture_kind == CaptureKind::Move || is_ref {
return capture_name;
}
let expr_parent = expr.syntax().parent().and_then(ast::Expr::cast);
let expr_parent_peeled_parens = expr_parent.map(peel_parens);
let does_autoderef = match expr_parent_peeled_parens {
Some(
ast::Expr::AwaitExpr(_)
| ast::Expr::CallExpr(_)
| ast::Expr::FieldExpr(_)
| ast::Expr::FormatArgsExpr(_)
| ast::Expr::MethodCallExpr(_),
) => true,
Some(ast::Expr::IndexExpr(parent_expr)) if parent_expr.base().as_ref() == Some(expr) => {
true
}
_ => false,
};
if does_autoderef {
return capture_name;
}
make::expr_prefix(T![*], capture_name)
}
fn capture_as_arg(ctx: &AssistContext<'_>, capture: &ClosureCapture) -> ast::Expr {
let place =
parse_expr_from_str(&capture.display_place_source_code(ctx.db()), ctx.file_id().edition())
.expect("`display_place_source_code()` produced an invalid expr");
let needs_mut = match capture.kind() {
CaptureKind::SharedRef => false,
CaptureKind::MutableRef | CaptureKind::UniqueSharedRef => true,
CaptureKind::Move => return place,
};
if let ast::Expr::PrefixExpr(expr) = &place {
if expr.op_kind() == Some(ast::UnaryOp::Deref) {
return expr.expr().expect("`display_place_source_code()` produced an invalid expr");
}
}
make::expr_ref(place, needs_mut)
}
fn handle_calls(
builder: &mut SourceChangeBuilder,
ctx: &AssistContext<'_>,
closure_name: Option<&ast::IdentPat>,
captures_as_args: &[ast::Expr],
closure: &ast::ClosureExpr,
) {
if captures_as_args.is_empty() {
return;
}
match closure_name {
Some(closure_name) => {
let Some(closure_def) = ctx.sema.to_def(closure_name) else { return };
let closure_usages = Definition::from(closure_def).usages(&ctx.sema).all();
for (_, usages) in closure_usages {
for usage in usages {
let name = match usage.name {
FileReferenceNode::Name(name) => name.syntax().clone(),
FileReferenceNode::NameRef(name_ref) => name_ref.syntax().clone(),
FileReferenceNode::FormatStringEntry(..) => continue,
FileReferenceNode::Lifetime(_) => {
unreachable!("impossible usage")
}
};
let Some(expr) = name.parent().and_then(|it| {
ast::Expr::cast(
ast::PathSegment::cast(it)?.parent_path().syntax().parent()?,
)
}) else {
continue;
};
handle_call(builder, ctx, expr, captures_as_args);
}
}
}
None => {
handle_call(builder, ctx, ast::Expr::ClosureExpr(closure.clone()), captures_as_args);
}
}
}
fn handle_call(
builder: &mut SourceChangeBuilder,
ctx: &AssistContext<'_>,
closure_ref: ast::Expr,
captures_as_args: &[ast::Expr],
) -> Option<()> {
let call =
ast::CallExpr::cast(peel_blocks_and_refs_and_parens(closure_ref).syntax().parent()?)?;
let args = call.arg_list()?;
let has_trailing_comma = args.syntax().last_token()?.prev_token().is_some_and(|token| {
skip_trivia_token(token, Direction::Prev).is_some_and(|token| token.kind() == T![,])
});
let has_existing_args = args.args().next().is_some();
let FileRangeWrapper { file_id, range } = ctx.sema.original_range_opt(args.syntax())?;
let first_arg_indent = args.args().next().map(|it| it.indent_level());
let arg_list_indent = args.indent_level();
let insert_newlines =
first_arg_indent.is_some_and(|first_arg_indent| first_arg_indent != arg_list_indent);
let indent =
if insert_newlines { first_arg_indent.unwrap().to_string() } else { String::new() };
let text = ctx.db().file_text(file_id.file_id());
let mut text = text[..u32::from(range.end()).try_into().unwrap()].trim_end();
if !text.ends_with(')') {
return None;
}
text = text[..text.len() - 1].trim_end();
let offset = TextSize::new(text.len().try_into().unwrap());
let mut to_insert = String::new();
if has_existing_args && !has_trailing_comma {
to_insert.push(',');
}
if insert_newlines {
to_insert.push('\n');
}
let (last_arg, rest_args) =
captures_as_args.split_last().expect("already checked has captures");
if !insert_newlines && has_existing_args {
to_insert.push(' ');
}
if let Some((first_arg, rest_args)) = rest_args.split_first() {
format_to!(to_insert, "{indent}{first_arg},",);
if insert_newlines {
to_insert.push('\n');
}
for new_arg in rest_args {
if !insert_newlines {
to_insert.push(' ');
}
format_to!(to_insert, "{indent}{new_arg},",);
if insert_newlines {
to_insert.push('\n');
}
}
if !insert_newlines {
to_insert.push(' ');
}
}
format_to!(to_insert, "{indent}{last_arg}");
if has_trailing_comma {
to_insert.push(',');
}
builder.edit_file(file_id);
builder.insert(offset, to_insert);
Some(())
}
fn peel_blocks_and_refs_and_parens(mut expr: ast::Expr) -> ast::Expr {
loop {
let Some(parent) = expr.syntax().parent() else { break };
if matches!(parent.kind(), SyntaxKind::PAREN_EXPR | SyntaxKind::REF_EXPR) {
expr = ast::Expr::cast(parent).unwrap();
continue;
}
if let Some(stmt_list) = ast::StmtList::cast(parent) {
if let Some(block) = stmt_list.syntax().parent().and_then(ast::BlockExpr::cast) {
expr = ast::Expr::BlockExpr(block);
continue;
}
}
break;
}
expr
}
fn expr_of_pat(pat: ast::Pat) -> Option<ast::Expr> {
'find_expr: {
for ancestor in pat.syntax().ancestors() {
if let Some(let_stmt) = ast::LetStmt::cast(ancestor.clone()) {
break 'find_expr let_stmt.initializer();
}
if ast::MatchArm::can_cast(ancestor.kind()) {
if let Some(match_) =
ancestor.parent().and_then(|it| it.parent()).and_then(ast::MatchExpr::cast)
{
break 'find_expr match_.expr();
}
}
if ast::ExprStmt::can_cast(ancestor.kind()) {
break;
}
}
None
}
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn handles_unique_captures() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore:copy
fn main() {
let s = &mut true;
let closure = |$0| { *s = false; };
closure();
}
"#,
r#"
fn main() {
let s = &mut true;
fn closure(s: &mut bool) { *s = false; }
closure(s);
}
"#,
);
}
#[test]
fn multiple_capture_usages() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore:copy
struct A { a: i32, b: bool }
fn main() {
let mut a = A { a: 123, b: false };
let closure = |$0| {
let b = a.b;
a = A { a: 456, b: true };
};
closure();
}
"#,
r#"
struct A { a: i32, b: bool }
fn main() {
let mut a = A { a: 123, b: false };
fn closure(a: &mut A) {
let b = a.b;
*a = A { a: 456, b: true };
}
closure(&mut a);
}
"#,
);
}
#[test]
fn changes_names_of_place() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore:copy
struct A { b: &'static B, c: i32 }
struct B(bool, i32);
struct C;
impl C {
fn foo(&self) {
let a = A { b: &B(false, 0), c: 123 };
let closure = |$0| {
let b = a.b.1;
let c = &*self;
};
closure();
}
}
"#,
r#"
struct A { b: &'static B, c: i32 }
struct B(bool, i32);
struct C;
impl C {
fn foo(&self) {
let a = A { b: &B(false, 0), c: 123 };
fn closure(this: &C, a_b_1: &i32) {
let b = *a_b_1;
let c = this;
}
closure(self, &a.b.1);
}
}
"#,
);
}
#[test]
fn self_with_fields_does_not_change_to_this() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore:copy
struct A { b: &'static B, c: i32 }
struct B(bool, i32);
impl A {
fn foo(&self) {
let closure = |$0| {
let b = self.b.1;
};
closure();
}
}
"#,
r#"
struct A { b: &'static B, c: i32 }
struct B(bool, i32);
impl A {
fn foo(&self) {
fn closure(self_b_1: &i32) {
let b = *self_b_1;
}
closure(&self.b.1);
}
}
"#,
);
}
#[test]
fn replaces_async_closure_with_async_fn() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy, future
fn foo(&self) {
let closure = async |$0| 1;
closure();
}
"#,
r#"
fn foo(&self) {
async fn closure() -> i32 {
1
}
closure();
}
"#,
);
}
#[test]
fn replaces_async_block_with_async_fn() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy, future
fn foo() {
let closure = |$0| async { 1 };
closure();
}
"#,
r#"
fn foo() {
async fn closure() -> i32 { 1 }
closure();
}
"#,
);
}
#[test]
#[ignore = "FIXME: we do not do type inference for gen blocks yet"]
fn replaces_gen_block_with_gen_fn() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy, iterator
//- /lib.rs edition:2024
fn foo() {
let closure = |$0| gen {
yield 1;
};
closure();
}
"#,
r#"
fn foo() {
gen fn closure() -> i32 {
yield 1;
}
closure();
}
"#,
);
}
#[test]
fn leaves_block_in_place() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy
fn foo() {
let closure = |$0| {};
closure();
}
"#,
r#"
fn foo() {
fn closure() {}
closure();
}
"#,
);
}
#[test]
fn wraps_in_block_if_needed() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy
fn foo() {
let a = 1;
let closure = |$0| a;
closure();
}
"#,
r#"
fn foo() {
let a = 1;
fn closure(a: &i32) -> i32 {
*a
}
closure(&a);
}
"#,
);
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy
fn foo() {
let closure = |$0| 'label: {};
closure();
}
"#,
r#"
fn foo() {
fn closure() {
'label: {}
}
closure();
}
"#,
);
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy
fn foo() {
let closure = |$0| {
const { () }
};
closure();
}
"#,
r#"
fn foo() {
fn closure() {
const { () }
}
closure();
}
"#,
);
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy
fn foo() {
let closure = |$0| unsafe { };
closure();
}
"#,
r#"
fn foo() {
fn closure() {
unsafe { }
}
closure();
}
"#,
);
}
#[test]
fn closure_in_closure() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy
fn foo() {
let a = 1;
|| |$0| { let b = &a; };
}
"#,
r#"
fn foo() {
let a = 1;
|| { fn fun_name(a: &i32) { let b = a; } fun_name };
}
"#,
);
}
#[test]
fn closure_in_block() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy
fn foo() {
{
let a = 1;
|$0| { let b = &a; }
};
}
"#,
r#"
fn foo() {
{
let a = 1;
fn fun_name(a: &i32) { let b = a; }
fun_name
};
}
"#,
);
}
#[test]
fn finds_pat_for_expr() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy
struct A { b: B }
struct B(bool, i32);
fn foo() {
let mut a = A { b: B(true, 0) };
let closure = |$0| {
let A { b: B(_, ref mut c) } = a;
};
closure();
}
"#,
r#"
struct A { b: B }
struct B(bool, i32);
fn foo() {
let mut a = A { b: B(true, 0) };
fn closure(a_b_1: &mut i32) {
let A { b: B(_, ref mut c) } = a_b_1;
}
closure(&mut a.b.1);
}
"#,
);
}
#[test]
fn with_existing_params() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy
fn foo() {
let (mut a, b) = (0.1, "abc");
let closure = |$0p1: i32, p2: &mut bool| {
a = 1.2;
let c = b;
};
closure(0, &mut false);
}
"#,
r#"
fn foo() {
let (mut a, b) = (0.1, "abc");
fn closure(p1: i32, p2: &mut bool, a: &mut f64, b: &&str) {
*a = 1.2;
let c = *b;
}
closure(0, &mut false, &mut a, &b);
}
"#,
);
}
#[test]
fn with_existing_params_newlines() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy
fn foo() {
let (mut a, b) = (0.1, "abc");
let closure = |$0p1: i32, p2| {
let _: &mut bool = p2;
a = 1.2;
let c = b;
};
closure(
0,
&mut false
);
}
"#,
r#"
fn foo() {
let (mut a, b) = (0.1, "abc");
fn closure(p1: i32, p2: &mut bool, a: &mut f64, b: &&str) {
let _: &mut bool = p2;
*a = 1.2;
let c = *b;
}
closure(
0,
&mut false,
&mut a,
&b
);
}
"#,
);
}
#[test]
fn with_existing_params_trailing_comma() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy
fn foo() {
let (mut a, b) = (0.1, "abc");
let closure = |$0p1: i32, p2| {
let _: &mut bool = p2;
a = 1.2;
let c = b;
};
closure(
0,
&mut false,
);
}
"#,
r#"
fn foo() {
let (mut a, b) = (0.1, "abc");
fn closure(p1: i32, p2: &mut bool, a: &mut f64, b: &&str) {
let _: &mut bool = p2;
*a = 1.2;
let c = *b;
}
closure(
0,
&mut false,
&mut a,
&b,
);
}
"#,
);
}
#[test]
fn closure_using_generic_params() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy
struct Foo<A, B, const C: usize>(A, B);
impl<A, B: From<A>, const C: usize> Foo<A, B, C> {
fn foo<D, E, F, G>(a: A, b: D)
where
E: From<D>,
{
let closure = |$0c: F| {
let a = B::from(a);
let b = E::from(b);
};
}
}
"#,
r#"
struct Foo<A, B, const C: usize>(A, B);
impl<A, B: From<A>, const C: usize> Foo<A, B, C> {
fn foo<D, E, F, G>(a: A, b: D)
where
E: From<D>,
{
fn closure<A, B: From<A>, D, E, F>(c: F, a: A, b: D) where E: From<D> {
let a = B::from(a);
let b = E::from(b);
}
}
}
"#,
);
}
#[test]
fn closure_in_stmt() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore: copy
fn bar(_: impl FnOnce() -> i32) {}
fn foo() {
let a = 123;
bar(|$0| a);
}
"#,
r#"
fn bar(_: impl FnOnce() -> i32) {}
fn foo() {
let a = 123;
fn fun_name(a: &i32) -> i32 {
*a
}
bar(fun_name);
}
"#,
);
}
#[test]
fn unique_and_imm() {
check_assist(
convert_closure_to_fn,
r#"
//- minicore:copy
fn main() {
let a = &mut true;
let closure = |$0| {
let b = &a;
*a = false;
};
closure();
}
"#,
r#"
fn main() {
let a = &mut true;
fn closure(a: &mut &mut bool) {
let b = a;
**a = false;
}
closure(&mut a);
}
"#,
);
}
#[test]
fn only_applicable_in_param_list() {
check_assist_not_applicable(
convert_closure_to_fn,
r#"
//- minicore:copy
fn main() {
let closure = || { $0 };
}
"#,
);
check_assist_not_applicable(
convert_closure_to_fn,
r#"
//- minicore:copy
fn main() {
let $0closure = || { };
}
"#,
);
}
}