ide_assists/handlers/
convert_from_to_tryfrom.rs

1use ide_db::{famous_defs::FamousDefs, traits::resolve_target_trait};
2use syntax::ast::edit::IndentLevel;
3use syntax::ast::{self, AstNode, HasGenericArgs, HasName, make};
4use syntax::syntax_editor::{Element, Position};
5
6use crate::{AssistContext, AssistId, Assists};
7
8// Assist: convert_from_to_tryfrom
9//
10// Converts a From impl to a TryFrom impl, wrapping returns in `Ok`.
11//
12// ```
13// # //- minicore: from
14// impl $0From<usize> for Thing {
15//     fn from(val: usize) -> Self {
16//         Thing {
17//             b: val.to_string(),
18//             a: val
19//         }
20//     }
21// }
22// ```
23// ->
24// ```
25// impl TryFrom<usize> for Thing {
26//     type Error = ${0:()};
27//
28//     fn try_from(val: usize) -> Result<Self, Self::Error> {
29//         Ok(Thing {
30//             b: val.to_string(),
31//             a: val
32//         })
33//     }
34// }
35// ```
36pub(crate) fn convert_from_to_tryfrom(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
37    let impl_ = ctx.find_node_at_offset::<ast::Impl>()?;
38    let trait_ty = impl_.trait_()?;
39
40    let module = ctx.sema.scope(impl_.syntax())?.module();
41
42    let from_type = match &trait_ty {
43        ast::Type::PathType(path) => {
44            path.path()?.segment()?.generic_arg_list()?.generic_args().next()?
45        }
46        _ => return None,
47    };
48
49    let associated_items = impl_.assoc_item_list()?;
50    let associated_l_curly = associated_items.l_curly_token()?;
51    let from_fn = associated_items.assoc_items().find_map(|item| {
52        if let ast::AssocItem::Fn(f) = item
53            && f.name()?.text() == "from"
54        {
55            return Some(f);
56        };
57        None
58    })?;
59
60    let from_fn_name = from_fn.name()?;
61    let from_fn_return_type = from_fn.ret_type()?.ty()?;
62
63    let return_exprs = from_fn.body()?.syntax().descendants().filter_map(ast::ReturnExpr::cast);
64    let tail_expr = from_fn.body()?.tail_expr()?;
65
66    if resolve_target_trait(&ctx.sema, &impl_)?
67        != FamousDefs(&ctx.sema, module.krate(ctx.db())).core_convert_From()?
68    {
69        return None;
70    }
71
72    acc.add(
73        AssistId::refactor_rewrite("convert_from_to_tryfrom"),
74        "Convert From to TryFrom",
75        impl_.syntax().text_range(),
76        |builder| {
77            let mut editor = builder.make_editor(impl_.syntax());
78            editor.replace(
79                trait_ty.syntax(),
80                make::ty(&format!("TryFrom<{from_type}>")).syntax().clone_for_update(),
81            );
82            editor.replace(
83                from_fn_return_type.syntax(),
84                make::ty("Result<Self, Self::Error>").syntax().clone_for_update(),
85            );
86            editor
87                .replace(from_fn_name.syntax(), make::name("try_from").syntax().clone_for_update());
88            editor.replace(
89                tail_expr.syntax(),
90                wrap_ok(tail_expr.clone()).syntax().clone_for_update(),
91            );
92
93            for r in return_exprs {
94                let t = r.expr().unwrap_or_else(make::ext::expr_unit);
95                editor.replace(t.syntax(), wrap_ok(t.clone()).syntax().clone_for_update());
96            }
97
98            let error_type = ast::AssocItem::TypeAlias(make::ty_alias(
99                None,
100                "Error",
101                None,
102                None,
103                None,
104                Some((make::ty_unit(), None)),
105            ))
106            .clone_for_update();
107
108            if let Some(cap) = ctx.config.snippet_cap
109                && let ast::AssocItem::TypeAlias(type_alias) = &error_type
110                && let Some(ty) = type_alias.ty()
111            {
112                let placeholder = builder.make_placeholder_snippet(cap);
113                editor.add_annotation(ty.syntax(), placeholder);
114            }
115
116            let indent = IndentLevel::from_token(&associated_l_curly) + 1;
117            editor.insert_all(
118                Position::after(associated_l_curly),
119                vec![
120                    make::tokens::whitespace(&format!("\n{indent}")).syntax_element(),
121                    error_type.syntax().syntax_element(),
122                    make::tokens::whitespace("\n").syntax_element(),
123                ],
124            );
125            builder.add_file_edits(ctx.vfs_file_id(), editor);
126        },
127    )
128}
129
130fn wrap_ok(expr: ast::Expr) -> ast::Expr {
131    make::expr_call(
132        make::expr_path(make::ext::ident_path("Ok")),
133        make::arg_list(std::iter::once(expr)),
134    )
135    .into()
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    use crate::tests::{check_assist, check_assist_not_applicable};
143
144    #[test]
145    fn converts_from_to_tryfrom() {
146        check_assist(
147            convert_from_to_tryfrom,
148            r#"
149//- minicore: from
150struct Foo(String);
151
152impl $0From<String> for Foo {
153    fn from(val: String) -> Self {
154        if val == "bar" {
155            return Foo(val);
156        }
157        Self(val)
158    }
159}
160            "#,
161            r#"
162struct Foo(String);
163
164impl TryFrom<String> for Foo {
165    type Error = ${0:()};
166
167    fn try_from(val: String) -> Result<Self, Self::Error> {
168        if val == "bar" {
169            return Ok(Foo(val));
170        }
171        Ok(Self(val))
172    }
173}
174            "#,
175        );
176    }
177
178    #[test]
179    fn converts_from_to_tryfrom_nested_type() {
180        check_assist(
181            convert_from_to_tryfrom,
182            r#"
183//- minicore: from
184struct Foo(String);
185
186impl $0From<Option<String>> for Foo {
187    fn from(val: Option<String>) -> Self {
188        match val {
189            Some(val) => Foo(val),
190            None => Foo("".to_string())
191        }
192    }
193}
194            "#,
195            r#"
196struct Foo(String);
197
198impl TryFrom<Option<String>> for Foo {
199    type Error = ${0:()};
200
201    fn try_from(val: Option<String>) -> Result<Self, Self::Error> {
202        Ok(match val {
203            Some(val) => Foo(val),
204            None => Foo("".to_string())
205        })
206    }
207}
208            "#,
209        );
210    }
211
212    #[test]
213    fn converts_from_to_tryfrom_preserves_lifetimes() {
214        check_assist(
215            convert_from_to_tryfrom,
216            r#"
217//- minicore: from
218struct Foo<'a>(&'a str);
219
220impl<'a> $0From<&'a str> for Foo<'a> {
221    fn from(val: &'a str) -> Self {
222        Self(val)
223    }
224}
225            "#,
226            r#"
227struct Foo<'a>(&'a str);
228
229impl<'a> TryFrom<&'a str> for Foo<'a> {
230    type Error = ${0:()};
231
232    fn try_from(val: &'a str) -> Result<Self, Self::Error> {
233        Ok(Self(val))
234    }
235}
236            "#,
237        );
238    }
239
240    #[test]
241    fn other_trait_not_applicable() {
242        check_assist_not_applicable(
243            convert_from_to_tryfrom,
244            r#"
245struct Foo(String);
246
247impl $0TryFrom<String> for Foo {
248    fn try_from(val: String) -> Result<Self, Self::Error> {
249        Ok(Self(val))
250    }
251}
252            "#,
253        );
254    }
255}