Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide-assists/src/handlers/convert_from_to_tryfrom.rs')
| -rw-r--r-- | crates/ide-assists/src/handlers/convert_from_to_tryfrom.rs | 250 |
1 files changed, 250 insertions, 0 deletions
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 0000000000..5459bd334c --- /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<usize> for Thing { +// fn from(val: usize) -> Self { +// Thing { +// b: val.to_string(), +// a: val +// } +// } +// } +// ``` +// -> +// ``` +// impl TryFrom<usize> for Thing { +// type Error = ${0:()}; +// +// fn try_from(val: usize) -> Result<Self, Self::Error> { +// 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::<ast::Impl>()?; + 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<Self, Self::Error>").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<String> for Foo { + fn from(val: String) -> Self { + if val == "bar" { + return Foo(val); + } + Self(val) + } +} + "#, + r#" +struct Foo(String); + +impl TryFrom<String> for Foo { + type Error = ${0:()}; + + fn try_from(val: String) -> Result<Self, Self::Error> { + 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<Option<String>> for Foo { + fn from(val: Option<String>) -> Self { + match val { + Some(val) => Foo(val), + None => Foo("".to_string()) + } + } +} + "#, + r#" +struct Foo(String); + +impl TryFrom<Option<String>> for Foo { + type Error = ${0:()}; + + fn try_from(val: Option<String>) -> Result<Self, Self::Error> { + 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<Self, Self::Error> { + Ok(Self(val)) + } +} + "#, + ); + } + + #[test] + fn other_trait_not_applicable() { + check_assist_not_applicable( + convert_from_to_tryfrom, + r#" +struct Foo(String); + +impl $0TryFrom<String> for Foo { + fn try_from(val: String) -> Result<Self, Self::Error> { + Ok(Self(val)) + } +} + "#, + ); + } +} |