From 1aba0002df5af8af7ac955a84535f76309707f14 Mon Sep 17 00:00:00 2001 From: Niklas Lindorfer Date: Wed, 17 Apr 2024 15:22:54 +0100 Subject: [PATCH] Add convert From to TryFrom assist --- .../src/handlers/convert_from_to_tryfrom.rs | 250 ++++++++++++++++++ crates/ide-assists/src/lib.rs | 2 + crates/ide-assists/src/tests/generated.rs | 30 +++ 3 files changed, 282 insertions(+) create mode 100644 crates/ide-assists/src/handlers/convert_from_to_tryfrom.rs diff --git a/crates/ide-assists/src/handlers/convert_from_to_tryfrom.rs b/crates/ide-assists/src/handlers/convert_from_to_tryfrom.rs new file mode 100644 index 00000000000..5459bd334c8 --- /dev/null +++ b/crates/ide-assists/src/handlers/convert_from_to_tryfrom.rs @@ -0,0 +1,250 @@ +use ide_db::{famous_defs::FamousDefs, traits::resolve_target_trait}; +use itertools::Itertools; +use syntax::{ + ast::{self, make, AstNode, HasName}, + ted, +}; + +use crate::{AssistContext, AssistId, AssistKind, Assists}; + +// Assist: convert_from_to_tryfrom +// +// Converts a From impl to a TryFrom impl, wrapping returns in `Ok`. +// +// ``` +// # //- minicore: from +// impl $0From for Thing { +// fn from(val: usize) -> Self { +// Thing { +// b: val.to_string(), +// a: val +// } +// } +// } +// ``` +// -> +// ``` +// impl TryFrom for Thing { +// type Error = ${0:()}; +// +// fn try_from(val: usize) -> Result { +// Ok(Thing { +// b: val.to_string(), +// a: val +// }) +// } +// } +// ``` +pub(crate) fn convert_from_to_tryfrom(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { + let impl_ = ctx.find_node_at_offset::()?; + let trait_ty = impl_.trait_()?; + + let module = ctx.sema.scope(impl_.syntax())?.module(); + + let from_type = match &trait_ty { + ast::Type::PathType(path) => { + path.path()?.segment()?.generic_arg_list()?.generic_args().next()? + } + _ => return None, + }; + + let associated_items = impl_.assoc_item_list()?; + let from_fn = associated_items.assoc_items().find_map(|item| { + if let ast::AssocItem::Fn(f) = item { + if f.name()?.text() == "from" { + return Some(f); + } + }; + None + })?; + + let from_fn_name = from_fn.name()?; + let from_fn_return_type = from_fn.ret_type()?.ty()?; + + let return_exprs = from_fn.body()?.syntax().descendants().filter_map(ast::ReturnExpr::cast); + let tail_expr = from_fn.body()?.tail_expr()?; + + if resolve_target_trait(&ctx.sema, &impl_)? + != FamousDefs(&ctx.sema, module.krate()).core_convert_From()? + { + return None; + } + + acc.add( + AssistId("convert_from_to_tryfrom", AssistKind::RefactorRewrite), + "Convert From to TryFrom", + impl_.syntax().text_range(), + |builder| { + let trait_ty = builder.make_mut(trait_ty); + let from_fn_return_type = builder.make_mut(from_fn_return_type); + let from_fn_name = builder.make_mut(from_fn_name); + let tail_expr = builder.make_mut(tail_expr); + let return_exprs = return_exprs.map(|r| builder.make_mut(r)).collect_vec(); + let associated_items = builder.make_mut(associated_items).clone(); + + ted::replace( + trait_ty.syntax(), + make::ty(&format!("TryFrom<{from_type}>")).syntax().clone_for_update(), + ); + ted::replace( + from_fn_return_type.syntax(), + make::ty("Result").syntax().clone_for_update(), + ); + ted::replace(from_fn_name.syntax(), make::name("try_from").syntax().clone_for_update()); + ted::replace( + tail_expr.syntax(), + wrap_ok(tail_expr.clone()).syntax().clone_for_update(), + ); + + for r in return_exprs { + let t = r.expr().unwrap_or_else(make::expr_unit); + ted::replace(t.syntax(), wrap_ok(t.clone()).syntax().clone_for_update()); + } + + let error_type = ast::AssocItem::TypeAlias(make::ty_alias( + "Error", + None, + None, + None, + Some((make::ty_unit(), None)), + )) + .clone_for_update(); + + if let Some(cap) = ctx.config.snippet_cap { + if let ast::AssocItem::TypeAlias(type_alias) = &error_type { + if let Some(ty) = type_alias.ty() { + builder.add_placeholder_snippet(cap, ty); + } + } + } + + associated_items.add_item_at_start(error_type); + }, + ) +} + +fn wrap_ok(expr: ast::Expr) -> ast::Expr { + make::expr_call( + make::expr_path(make::ext::ident_path("Ok")), + make::arg_list(std::iter::once(expr)), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::tests::{check_assist, check_assist_not_applicable}; + + #[test] + fn converts_from_to_tryfrom() { + check_assist( + convert_from_to_tryfrom, + r#" +//- minicore: from +struct Foo(String); + +impl $0From for Foo { + fn from(val: String) -> Self { + if val == "bar" { + return Foo(val); + } + Self(val) + } +} + "#, + r#" +struct Foo(String); + +impl TryFrom for Foo { + type Error = ${0:()}; + + fn try_from(val: String) -> Result { + if val == "bar" { + return Ok(Foo(val)); + } + Ok(Self(val)) + } +} + "#, + ); + } + + #[test] + fn converts_from_to_tryfrom_nested_type() { + check_assist( + convert_from_to_tryfrom, + r#" +//- minicore: from +struct Foo(String); + +impl $0From> for Foo { + fn from(val: Option) -> Self { + match val { + Some(val) => Foo(val), + None => Foo("".to_string()) + } + } +} + "#, + r#" +struct Foo(String); + +impl TryFrom> for Foo { + type Error = ${0:()}; + + fn try_from(val: Option) -> Result { + Ok(match val { + Some(val) => Foo(val), + None => Foo("".to_string()) + }) + } +} + "#, + ); + } + + #[test] + fn converts_from_to_tryfrom_preserves_lifetimes() { + check_assist( + convert_from_to_tryfrom, + r#" +//- minicore: from +struct Foo<'a>(&'a str); + +impl<'a> $0From<&'a str> for Foo<'a> { + fn from(val: &'a str) -> Self { + Self(val) + } +} + "#, + r#" +struct Foo<'a>(&'a str); + +impl<'a> TryFrom<&'a str> for Foo<'a> { + type Error = ${0:()}; + + fn try_from(val: &'a str) -> Result { + Ok(Self(val)) + } +} + "#, + ); + } + + #[test] + fn other_trait_not_applicable() { + check_assist_not_applicable( + convert_from_to_tryfrom, + r#" +struct Foo(String); + +impl $0TryFrom for Foo { + fn try_from(val: String) -> Result { + Ok(Self(val)) + } +} + "#, + ); + } +} diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs index 024448ef1e7..0df5e913a57 100644 --- a/crates/ide-assists/src/lib.rs +++ b/crates/ide-assists/src/lib.rs @@ -116,6 +116,7 @@ mod handlers { mod change_visibility; mod convert_bool_then; mod convert_comment_block; + mod convert_from_to_tryfrom; mod convert_integer_literal; mod convert_into_to_from; mod convert_iter_for_each_to_for; @@ -238,6 +239,7 @@ mod handlers { convert_bool_then::convert_bool_then_to_if, convert_bool_then::convert_if_to_bool_then, convert_comment_block::convert_comment_block, + convert_from_to_tryfrom::convert_from_to_tryfrom, convert_integer_literal::convert_integer_literal, convert_into_to_from::convert_into_to_from, convert_iter_for_each_to_for::convert_iter_for_each_to_for, diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index d0355bd0284..937e78f8d7d 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -390,6 +390,36 @@ fn main() { ) } +#[test] +fn doctest_convert_from_to_tryfrom() { + check_doc_test( + "convert_from_to_tryfrom", + r#####" +//- minicore: from +impl $0From for Thing { + fn from(val: usize) -> Self { + Thing { + b: val.to_string(), + a: val + } + } +} +"#####, + r#####" +impl TryFrom for Thing { + type Error = ${0:()}; + + fn try_from(val: usize) -> Result { + Ok(Thing { + b: val.to_string(), + a: val + }) + } +} +"#####, + ) +} + #[test] fn doctest_convert_if_to_bool_then() { check_doc_test(