ra_ap_ide_assists/handlers/
replace_method_eager_lazy.rs

1use ide_db::assists::AssistId;
2use syntax::{
3    AstNode,
4    ast::{self, Expr, HasArgList, make},
5};
6
7use crate::{AssistContext, Assists};
8
9// Assist: replace_with_lazy_method
10//
11// Replace `unwrap_or` with `unwrap_or_else` and `ok_or` with `ok_or_else`.
12//
13// ```
14// # //- minicore:option, fn
15// fn foo() {
16//     let a = Some(1);
17//     a.unwra$0p_or(2);
18// }
19// ```
20// ->
21// ```
22// fn foo() {
23//     let a = Some(1);
24//     a.unwrap_or_else(|| 2);
25// }
26// ```
27pub(crate) fn replace_with_lazy_method(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
28    let call: ast::MethodCallExpr = ctx.find_node_at_offset()?;
29    let scope = ctx.sema.scope(call.syntax())?;
30
31    let last_arg = call.arg_list()?.args().next()?;
32    let method_name = call.name_ref()?;
33
34    let callable = ctx.sema.resolve_method_call_as_callable(&call)?;
35    let (_, receiver_ty) = callable.receiver_param(ctx.sema.db)?;
36    let n_params = callable.n_params() + 1;
37
38    let method_name_lazy = format!(
39        "{method_name}{}",
40        if method_name.text().ends_with("or") { "_else" } else { "_with" }
41    );
42
43    receiver_ty.iterate_method_candidates_with_traits(
44        ctx.sema.db,
45        &scope,
46        &scope.visible_traits().0,
47        None,
48        None,
49        |func| {
50            let valid = func.name(ctx.sema.db).as_str() == &*method_name_lazy
51                && func.num_params(ctx.sema.db) == n_params
52                && {
53                    let params = func.params_without_self(ctx.sema.db);
54                    let last_p = params.first()?;
55                    // FIXME: Check that this has the form of `() -> T` where T is the current type of the argument
56                    last_p.ty().impls_fnonce(ctx.sema.db)
57                };
58            valid.then_some(func)
59        },
60    )?;
61
62    acc.add(
63        AssistId::refactor_rewrite("replace_with_lazy_method"),
64        format!("Replace {method_name} with {method_name_lazy}"),
65        call.syntax().text_range(),
66        |builder| {
67            builder.replace(method_name.syntax().text_range(), method_name_lazy);
68            let closured = into_closure(&last_arg);
69            builder.replace_ast(last_arg, closured);
70        },
71    )
72}
73
74fn into_closure(param: &Expr) -> Expr {
75    (|| {
76        if let ast::Expr::CallExpr(call) = param {
77            if call.arg_list()?.args().count() == 0 { Some(call.expr()?) } else { None }
78        } else {
79            None
80        }
81    })()
82    .unwrap_or_else(|| make::expr_closure(None, param.clone()).into())
83}
84
85// Assist: replace_with_eager_method
86//
87// Replace `unwrap_or_else` with `unwrap_or` and `ok_or_else` with `ok_or`.
88//
89// ```
90// # //- minicore:option, fn
91// fn foo() {
92//     let a = Some(1);
93//     a.unwra$0p_or_else(|| 2);
94// }
95// ```
96// ->
97// ```
98// fn foo() {
99//     let a = Some(1);
100//     a.unwrap_or(2);
101// }
102// ```
103pub(crate) fn replace_with_eager_method(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
104    let call: ast::MethodCallExpr = ctx.find_node_at_offset()?;
105    let scope = ctx.sema.scope(call.syntax())?;
106
107    let last_arg = call.arg_list()?.args().next()?;
108    let method_name = call.name_ref()?;
109
110    let callable = ctx.sema.resolve_method_call_as_callable(&call)?;
111    let (_, receiver_ty) = callable.receiver_param(ctx.sema.db)?;
112    let n_params = callable.n_params() + 1;
113    let params = callable.params();
114
115    // FIXME: Check that the arg is of the form `() -> T`
116    if !params.first()?.ty().impls_fnonce(ctx.sema.db) {
117        return None;
118    }
119
120    let method_name_text = method_name.text();
121    let method_name_eager = method_name_text
122        .strip_suffix("_else")
123        .or_else(|| method_name_text.strip_suffix("_with"))?;
124
125    receiver_ty.iterate_method_candidates_with_traits(
126        ctx.sema.db,
127        &scope,
128        &scope.visible_traits().0,
129        None,
130        None,
131        |func| {
132            let valid = func.name(ctx.sema.db).as_str() == method_name_eager
133                && func.num_params(ctx.sema.db) == n_params;
134            valid.then_some(func)
135        },
136    )?;
137
138    acc.add(
139        AssistId::refactor_rewrite("replace_with_eager_method"),
140        format!("Replace {method_name} with {method_name_eager}"),
141        call.syntax().text_range(),
142        |builder| {
143            builder.replace(method_name.syntax().text_range(), method_name_eager);
144            let called = into_call(&last_arg);
145            builder.replace_ast(last_arg, called);
146        },
147    )
148}
149
150fn into_call(param: &Expr) -> Expr {
151    (|| {
152        if let ast::Expr::ClosureExpr(closure) = param {
153            if closure.param_list()?.params().count() == 0 { Some(closure.body()?) } else { None }
154        } else {
155            None
156        }
157    })()
158    .unwrap_or_else(|| make::expr_call(param.clone(), make::arg_list(Vec::new())).into())
159}
160
161#[cfg(test)]
162mod tests {
163    use crate::tests::check_assist;
164
165    use super::*;
166
167    #[test]
168    fn replace_or_with_or_else_simple() {
169        check_assist(
170            replace_with_lazy_method,
171            r#"
172//- minicore: option, fn
173fn foo() {
174    let foo = Some(1);
175    return foo.unwrap_$0or(2);
176}
177"#,
178            r#"
179fn foo() {
180    let foo = Some(1);
181    return foo.unwrap_or_else(|| 2);
182}
183"#,
184        )
185    }
186
187    #[test]
188    fn replace_or_with_or_else_call() {
189        check_assist(
190            replace_with_lazy_method,
191            r#"
192//- minicore: option, fn
193fn foo() {
194    let foo = Some(1);
195    return foo.unwrap_$0or(x());
196}
197"#,
198            r#"
199fn foo() {
200    let foo = Some(1);
201    return foo.unwrap_or_else(x);
202}
203"#,
204        )
205    }
206
207    #[test]
208    fn replace_or_with_or_else_block() {
209        check_assist(
210            replace_with_lazy_method,
211            r#"
212//- minicore: option, fn
213fn foo() {
214    let foo = Some(1);
215    return foo.unwrap_$0or({
216        let mut x = bar();
217        for i in 0..10 {
218            x += i;
219        }
220        x
221    });
222}
223"#,
224            r#"
225fn foo() {
226    let foo = Some(1);
227    return foo.unwrap_or_else(|| {
228        let mut x = bar();
229        for i in 0..10 {
230            x += i;
231        }
232        x
233    });
234}
235"#,
236        )
237    }
238
239    #[test]
240    fn replace_or_else_with_or_simple() {
241        check_assist(
242            replace_with_eager_method,
243            r#"
244//- minicore: option, fn
245fn foo() {
246    let foo = Some(1);
247    return foo.unwrap_$0or_else(|| 2);
248}
249"#,
250            r#"
251fn foo() {
252    let foo = Some(1);
253    return foo.unwrap_or(2);
254}
255"#,
256        )
257    }
258
259    #[test]
260    fn replace_or_else_with_or_call() {
261        check_assist(
262            replace_with_eager_method,
263            r#"
264//- minicore: option, fn
265fn foo() {
266    let foo = Some(1);
267    return foo.unwrap_$0or_else(x);
268}
269
270fn x() -> i32 { 0 }
271"#,
272            r#"
273fn foo() {
274    let foo = Some(1);
275    return foo.unwrap_or(x());
276}
277
278fn x() -> i32 { 0 }
279"#,
280        )
281    }
282
283    #[test]
284    fn replace_or_else_with_or_map() {
285        check_assist(
286            replace_with_eager_method,
287            r#"
288//- minicore: option, fn
289fn foo() {
290    let foo = Some("foo");
291    return foo.map$0_or_else(|| 42, |v| v.len());
292}
293"#,
294            r#"
295fn foo() {
296    let foo = Some("foo");
297    return foo.map_or(42, |v| v.len());
298}
299"#,
300        )
301    }
302}