Skip to main content

ra_ap_ide_assists/handlers/
replace_method_eager_lazy.rs

1use hir::Semantics;
2use ide_db::{RootDatabase, assists::AssistId, defs::Definition};
3use syntax::{
4    AstNode,
5    ast::{self, Expr, HasArgList, make, syntax_factory::SyntaxFactory},
6};
7
8use crate::{AssistContext, Assists, utils::wrap_paren_in_call};
9
10// Assist: replace_with_lazy_method
11//
12// Replace `unwrap_or` with `unwrap_or_else` and `ok_or` with `ok_or_else`.
13//
14// ```
15// # //- minicore:option, fn
16// fn foo() {
17//     let a = Some(1);
18//     a.unwra$0p_or(2);
19// }
20// ```
21// ->
22// ```
23// fn foo() {
24//     let a = Some(1);
25//     a.unwrap_or_else(|| 2);
26// }
27// ```
28pub(crate) fn replace_with_lazy_method(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
29    let call: ast::MethodCallExpr = ctx.find_node_at_offset()?;
30    let scope = ctx.sema.scope(call.syntax())?;
31
32    let last_arg = call.arg_list()?.args().next()?;
33    let method_name = call.name_ref()?;
34
35    let callable = ctx.sema.resolve_method_call_as_callable(&call)?;
36    let (_, receiver_ty) = callable.receiver_param(ctx.sema.db)?;
37    let n_params = callable.n_params() + 1;
38
39    let method_name_lazy = lazy_method_name(&method_name.text());
40
41    receiver_ty.iterate_method_candidates_with_traits(
42        ctx.sema.db,
43        &scope,
44        &scope.visible_traits().0,
45        None,
46        |func| {
47            let valid = func.name(ctx.sema.db).as_str() == &*method_name_lazy
48                && func.num_params(ctx.sema.db) == n_params
49                && {
50                    let params = func.params_without_self(ctx.sema.db);
51                    let last_p = params.first()?;
52                    // FIXME: Check that this has the form of `() -> T` where T is the current type of the argument
53                    last_p.ty().impls_fnonce(ctx.sema.db)
54                };
55            valid.then_some(func)
56        },
57    )?;
58
59    acc.add(
60        AssistId::refactor_rewrite("replace_with_lazy_method"),
61        format!("Replace {method_name} with {method_name_lazy}"),
62        call.syntax().text_range(),
63        |builder| {
64            let closured = into_closure(&last_arg, &method_name_lazy);
65            builder.replace(method_name.syntax().text_range(), method_name_lazy);
66            builder.replace_ast(last_arg, closured);
67        },
68    )
69}
70
71fn lazy_method_name(name: &str) -> String {
72    if ends_is(name, "or") {
73        format!("{name}_else")
74    } else if ends_is(name, "and") {
75        format!("{name}_then")
76    } else if ends_is(name, "then_some") {
77        name.strip_suffix("_some").unwrap().to_owned()
78    } else {
79        format!("{name}_with")
80    }
81}
82
83fn into_closure(param: &Expr, name_lazy: &str) -> Expr {
84    (|| {
85        if let ast::Expr::CallExpr(call) = param {
86            if call.arg_list()?.args().count() == 0 { Some(call.expr()?) } else { None }
87        } else {
88            None
89        }
90    })()
91    .unwrap_or_else(|| {
92        let pats = (name_lazy == "and_then")
93            .then(|| make::untyped_param(make::ext::simple_ident_pat(make::name("it")).into()));
94        make::expr_closure(pats, param.clone()).into()
95    })
96}
97
98// Assist: replace_with_eager_method
99//
100// Replace `unwrap_or_else` with `unwrap_or` and `ok_or_else` with `ok_or`.
101//
102// ```
103// # //- minicore:option, fn
104// fn foo() {
105//     let a = Some(1);
106//     a.unwra$0p_or_else(|| 2);
107// }
108// ```
109// ->
110// ```
111// fn foo() {
112//     let a = Some(1);
113//     a.unwrap_or(2);
114// }
115// ```
116pub(crate) fn replace_with_eager_method(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
117    let call: ast::MethodCallExpr = ctx.find_node_at_offset()?;
118    let scope = ctx.sema.scope(call.syntax())?;
119
120    let last_arg = call.arg_list()?.args().next()?;
121    let method_name = call.name_ref()?;
122
123    let callable = ctx.sema.resolve_method_call_as_callable(&call)?;
124    let (_, receiver_ty) = callable.receiver_param(ctx.sema.db)?;
125    let n_params = callable.n_params() + 1;
126    let params = callable.params();
127
128    // FIXME: Check that the arg is of the form `() -> T`
129    if !params.first()?.ty().impls_fnonce(ctx.sema.db) {
130        return None;
131    }
132
133    let method_name_text = method_name.text();
134    let method_name_eager = eager_method_name(&method_name_text)?;
135
136    receiver_ty.iterate_method_candidates_with_traits(
137        ctx.sema.db,
138        &scope,
139        &scope.visible_traits().0,
140        None,
141        |func| {
142            let valid = func.name(ctx.sema.db).as_str() == method_name_eager
143                && func.num_params(ctx.sema.db) == n_params;
144            valid.then_some(func)
145        },
146    )?;
147
148    acc.add(
149        AssistId::refactor_rewrite("replace_with_eager_method"),
150        format!("Replace {method_name} with {method_name_eager}"),
151        call.syntax().text_range(),
152        |builder| {
153            builder.replace(method_name.syntax().text_range(), method_name_eager);
154            let called = into_call(&last_arg, &ctx.sema);
155            builder.replace_ast(last_arg, called);
156        },
157    )
158}
159
160fn into_call(param: &Expr, sema: &Semantics<'_, RootDatabase>) -> Expr {
161    (|| {
162        if let ast::Expr::ClosureExpr(closure) = param {
163            let mut params = closure.param_list()?.params();
164            match params.next() {
165                Some(_) if params.next().is_none() => {
166                    let params = sema.resolve_expr_as_callable(param)?.params();
167                    let used_param = Definition::Local(params.first()?.as_local(sema.db)?)
168                        .usages(sema)
169                        .at_least_one();
170                    if used_param { None } else { Some(closure.body()?) }
171                }
172                None => Some(closure.body()?),
173                Some(_) => None,
174            }
175        } else {
176            None
177        }
178    })()
179    .unwrap_or_else(|| {
180        let callable = wrap_paren_in_call(param.clone(), &SyntaxFactory::without_mappings());
181        make::expr_call(callable, make::arg_list(Vec::new())).into()
182    })
183}
184
185fn eager_method_name(name: &str) -> Option<&str> {
186    if name == "then" {
187        return Some("then_some");
188    }
189
190    name.strip_suffix("_else")
191        .or_else(|| name.strip_suffix("_then"))
192        .or_else(|| name.strip_suffix("_with"))
193}
194
195fn ends_is(name: &str, end: &str) -> bool {
196    name.strip_suffix(end).is_some_and(|s| s.is_empty() || s.ends_with('_'))
197}
198
199#[cfg(test)]
200mod tests {
201    use crate::tests::check_assist;
202
203    use super::*;
204
205    #[test]
206    fn replace_or_with_or_else_simple() {
207        check_assist(
208            replace_with_lazy_method,
209            r#"
210//- minicore: option, fn
211fn foo() {
212    let foo = Some(1);
213    return foo.unwrap_$0or(2);
214}
215"#,
216            r#"
217fn foo() {
218    let foo = Some(1);
219    return foo.unwrap_or_else(|| 2);
220}
221"#,
222        )
223    }
224
225    #[test]
226    fn replace_or_with_or_else_call() {
227        check_assist(
228            replace_with_lazy_method,
229            r#"
230//- minicore: option, fn
231fn foo() {
232    let foo = Some(1);
233    return foo.unwrap_$0or(x());
234}
235"#,
236            r#"
237fn foo() {
238    let foo = Some(1);
239    return foo.unwrap_or_else(x);
240}
241"#,
242        )
243    }
244
245    #[test]
246    fn replace_or_with_or_else_block() {
247        check_assist(
248            replace_with_lazy_method,
249            r#"
250//- minicore: option, fn
251fn foo() {
252    let foo = Some(1);
253    return foo.unwrap_$0or({
254        let mut x = bar();
255        for i in 0..10 {
256            x += i;
257        }
258        x
259    });
260}
261"#,
262            r#"
263fn foo() {
264    let foo = Some(1);
265    return foo.unwrap_or_else(|| {
266        let mut x = bar();
267        for i in 0..10 {
268            x += i;
269        }
270        x
271    });
272}
273"#,
274        )
275    }
276
277    #[test]
278    fn replace_or_else_with_or_simple() {
279        check_assist(
280            replace_with_eager_method,
281            r#"
282//- minicore: option, fn
283fn foo() {
284    let foo = Some(1);
285    return foo.unwrap_$0or_else(|| 2);
286}
287"#,
288            r#"
289fn foo() {
290    let foo = Some(1);
291    return foo.unwrap_or(2);
292}
293"#,
294        )
295    }
296
297    #[test]
298    fn replace_or_else_with_or_call() {
299        check_assist(
300            replace_with_eager_method,
301            r#"
302//- minicore: option, fn
303fn foo() {
304    let foo = Some(1);
305    return foo.unwrap_$0or_else(x);
306}
307
308fn x() -> i32 { 0 }
309"#,
310            r#"
311fn foo() {
312    let foo = Some(1);
313    return foo.unwrap_or(x());
314}
315
316fn x() -> i32 { 0 }
317"#,
318        )
319    }
320
321    #[test]
322    fn replace_or_else_with_or_map() {
323        check_assist(
324            replace_with_eager_method,
325            r#"
326//- minicore: option, fn
327fn foo() {
328    let foo = Some("foo");
329    return foo.map$0_or_else(|| 42, |v| v.len());
330}
331"#,
332            r#"
333fn foo() {
334    let foo = Some("foo");
335    return foo.map_or(42, |v| v.len());
336}
337"#,
338        )
339    }
340
341    #[test]
342    fn replace_and_with_and_then() {
343        check_assist(
344            replace_with_lazy_method,
345            r#"
346//- minicore: option, fn
347fn foo() {
348    let foo = Some("foo");
349    return foo.and$0(Some("bar"));
350}
351"#,
352            r#"
353fn foo() {
354    let foo = Some("foo");
355    return foo.and_then(|it| Some("bar"));
356}
357"#,
358        )
359    }
360
361    #[test]
362    fn replace_and_then_with_and() {
363        check_assist(
364            replace_with_eager_method,
365            r#"
366//- minicore: option, fn
367fn foo() {
368    let foo = Some("foo");
369    return foo.and_then$0(|it| Some("bar"));
370}
371"#,
372            r#"
373fn foo() {
374    let foo = Some("foo");
375    return foo.and(Some("bar"));
376}
377"#,
378        )
379    }
380
381    #[test]
382    fn replace_and_then_with_and_used_param() {
383        check_assist(
384            replace_with_eager_method,
385            r#"
386//- minicore: option, fn
387fn foo() {
388    let foo = Some("foo");
389    return foo.and_then$0(|it| Some(it.strip_suffix("bar")));
390}
391"#,
392            r#"
393fn foo() {
394    let foo = Some("foo");
395    return foo.and((|it| Some(it.strip_suffix("bar")))());
396}
397"#,
398        )
399    }
400
401    #[test]
402    fn replace_then_some_with_then() {
403        check_assist(
404            replace_with_lazy_method,
405            r#"
406//- minicore: option, fn, bool_impl
407fn foo() {
408    let foo = true;
409    let x = foo.then_some$0(2);
410}
411"#,
412            r#"
413fn foo() {
414    let foo = true;
415    let x = foo.then(|| 2);
416}
417"#,
418        )
419    }
420
421    #[test]
422    fn replace_then_with_then_some() {
423        check_assist(
424            replace_with_eager_method,
425            r#"
426//- minicore: option, fn, bool_impl
427fn foo() {
428    let foo = true;
429    let x = foo.then$0(|| 2);
430}
431"#,
432            r#"
433fn foo() {
434    let foo = true;
435    let x = foo.then_some(2);
436}
437"#,
438        )
439    }
440
441    #[test]
442    fn replace_then_with_then_some_needs_parens() {
443        check_assist(
444            replace_with_eager_method,
445            r#"
446//- minicore: option, fn, bool_impl
447struct Func { f: fn() -> i32 }
448fn foo() {
449    let foo = true;
450    let func = Func { f: || 2 };
451    let x = foo.then$0(func.f);
452}
453"#,
454            r#"
455struct Func { f: fn() -> i32 }
456fn foo() {
457    let foo = true;
458    let func = Func { f: || 2 };
459    let x = foo.then_some((func.f)());
460}
461"#,
462        )
463    }
464}