ra_ap_ide_assists/handlers/
replace_method_eager_lazy.rs1use 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
10pub(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 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
98pub(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 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}