Skip to main content

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