Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit 31c12ec

Browse files
committed
Auto merge of rust-lang#14266 - Veykril:generalize-eager-lazy, r=Veykril
feature: Make replace_or_with_or_else assists more generally applicable
2 parents 1bfe96e + 0ce0608 commit 31c12ec

File tree

10 files changed

+415
-522
lines changed

10 files changed

+415
-522
lines changed

crates/hir/src/lib.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1677,6 +1677,10 @@ impl Function {
16771677
.collect()
16781678
}
16791679

1680+
pub fn num_params(self, db: &dyn HirDatabase) -> usize {
1681+
db.function_data(self.id).params.len()
1682+
}
1683+
16801684
pub fn method_params(self, db: &dyn HirDatabase) -> Option<Vec<Param>> {
16811685
if self.self_param(db).is_none() {
16821686
return None;
@@ -3857,11 +3861,13 @@ impl Type {
38573861
}
38583862
}
38593863

3864+
// FIXME: Document this
38603865
#[derive(Debug)]
38613866
pub struct Callable {
38623867
ty: Type,
38633868
sig: CallableSig,
38643869
callee: Callee,
3870+
/// Whether this is a method that was called with method call syntax.
38653871
pub(crate) is_bound_method: bool,
38663872
}
38673873

@@ -3895,14 +3901,14 @@ impl Callable {
38953901
Other => CallableKind::Other,
38963902
}
38973903
}
3898-
pub fn receiver_param(&self, db: &dyn HirDatabase) -> Option<ast::SelfParam> {
3904+
pub fn receiver_param(&self, db: &dyn HirDatabase) -> Option<(ast::SelfParam, Type)> {
38993905
let func = match self.callee {
39003906
Callee::Def(CallableDefId::FunctionId(it)) if self.is_bound_method => it,
39013907
_ => return None,
39023908
};
39033909
let src = func.lookup(db.upcast()).source(db.upcast());
39043910
let param_list = src.value.param_list()?;
3905-
param_list.self_param()
3911+
Some((param_list.self_param()?, self.ty.derived(self.sig.params()[0].clone())))
39063912
}
39073913
pub fn n_params(&self) -> usize {
39083914
self.sig.params().len() - if self.is_bound_method { 1 } else { 0 }
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
use ide_db::assists::{AssistId, AssistKind};
2+
use syntax::{
3+
ast::{self, make, Expr, HasArgList},
4+
AstNode,
5+
};
6+
7+
use 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+
// ```
27+
pub(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() == Some(&*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("replace_with_lazy_method", AssistKind::RefactorRewrite),
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+
74+
fn into_closure(param: &Expr) -> Expr {
75+
(|| {
76+
if let ast::Expr::CallExpr(call) = param {
77+
if call.arg_list()?.args().count() == 0 {
78+
Some(call.expr()?)
79+
} else {
80+
None
81+
}
82+
} else {
83+
None
84+
}
85+
})()
86+
.unwrap_or_else(|| make::expr_closure(None, param.clone()))
87+
}
88+
89+
// Assist: replace_with_eager_method
90+
//
91+
// Replace `unwrap_or_else` with `unwrap_or` and `ok_or_else` with `ok_or`.
92+
//
93+
// ```
94+
// # //- minicore:option, fn
95+
// fn foo() {
96+
// let a = Some(1);
97+
// a.unwra$0p_or_else(|| 2);
98+
// }
99+
// ```
100+
// ->
101+
// ```
102+
// fn foo() {
103+
// let a = Some(1);
104+
// a.unwrap_or(2);
105+
// }
106+
// ```
107+
pub(crate) fn replace_with_eager_method(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
108+
let call: ast::MethodCallExpr = ctx.find_node_at_offset()?;
109+
let scope = ctx.sema.scope(call.syntax())?;
110+
111+
let last_arg = call.arg_list()?.args().next()?;
112+
let method_name = call.name_ref()?;
113+
114+
let callable = ctx.sema.resolve_method_call_as_callable(&call)?;
115+
let (_, receiver_ty) = callable.receiver_param(ctx.sema.db)?;
116+
let n_params = callable.n_params() + 1;
117+
let params = callable.params(ctx.sema.db);
118+
119+
// FIXME: Check that the arg is of the form `() -> T`
120+
if !params.first()?.1.impls_fnonce(ctx.sema.db) {
121+
return None;
122+
}
123+
124+
let method_name_text = method_name.text();
125+
let method_name_eager = method_name_text
126+
.strip_suffix("_else")
127+
.or_else(|| method_name_text.strip_suffix("_with"))?;
128+
129+
receiver_ty.iterate_method_candidates_with_traits(
130+
ctx.sema.db,
131+
&scope,
132+
&scope.visible_traits().0,
133+
None,
134+
None,
135+
|func| {
136+
let valid = func.name(ctx.sema.db).as_str() == Some(&*method_name_eager)
137+
&& func.num_params(ctx.sema.db) == n_params;
138+
valid.then_some(func)
139+
},
140+
)?;
141+
142+
acc.add(
143+
AssistId("replace_with_eager_method", AssistKind::RefactorRewrite),
144+
format!("Replace {method_name} with {method_name_eager}"),
145+
call.syntax().text_range(),
146+
|builder| {
147+
builder.replace(method_name.syntax().text_range(), method_name_eager);
148+
let called = into_call(&last_arg);
149+
builder.replace_ast(last_arg, called);
150+
},
151+
)
152+
}
153+
154+
fn into_call(param: &Expr) -> Expr {
155+
(|| {
156+
if let ast::Expr::ClosureExpr(closure) = param {
157+
if closure.param_list()?.params().count() == 0 {
158+
Some(closure.body()?)
159+
} else {
160+
None
161+
}
162+
} else {
163+
None
164+
}
165+
})()
166+
.unwrap_or_else(|| make::expr_call(param.clone(), make::arg_list(Vec::new())))
167+
}
168+
169+
#[cfg(test)]
170+
mod tests {
171+
use crate::tests::check_assist;
172+
173+
use super::*;
174+
175+
#[test]
176+
fn replace_or_with_or_else_simple() {
177+
check_assist(
178+
replace_with_lazy_method,
179+
r#"
180+
//- minicore: option, fn
181+
fn foo() {
182+
let foo = Some(1);
183+
return foo.unwrap_$0or(2);
184+
}
185+
"#,
186+
r#"
187+
fn foo() {
188+
let foo = Some(1);
189+
return foo.unwrap_or_else(|| 2);
190+
}
191+
"#,
192+
)
193+
}
194+
195+
#[test]
196+
fn replace_or_with_or_else_call() {
197+
check_assist(
198+
replace_with_lazy_method,
199+
r#"
200+
//- minicore: option, fn
201+
fn foo() {
202+
let foo = Some(1);
203+
return foo.unwrap_$0or(x());
204+
}
205+
"#,
206+
r#"
207+
fn foo() {
208+
let foo = Some(1);
209+
return foo.unwrap_or_else(x);
210+
}
211+
"#,
212+
)
213+
}
214+
215+
#[test]
216+
fn replace_or_with_or_else_block() {
217+
check_assist(
218+
replace_with_lazy_method,
219+
r#"
220+
//- minicore: option, fn
221+
fn foo() {
222+
let foo = Some(1);
223+
return foo.unwrap_$0or({
224+
let mut x = bar();
225+
for i in 0..10 {
226+
x += i;
227+
}
228+
x
229+
});
230+
}
231+
"#,
232+
r#"
233+
fn foo() {
234+
let foo = Some(1);
235+
return foo.unwrap_or_else(|| {
236+
let mut x = bar();
237+
for i in 0..10 {
238+
x += i;
239+
}
240+
x
241+
});
242+
}
243+
"#,
244+
)
245+
}
246+
247+
#[test]
248+
fn replace_or_else_with_or_simple() {
249+
check_assist(
250+
replace_with_eager_method,
251+
r#"
252+
//- minicore: option, fn
253+
fn foo() {
254+
let foo = Some(1);
255+
return foo.unwrap_$0or_else(|| 2);
256+
}
257+
"#,
258+
r#"
259+
fn foo() {
260+
let foo = Some(1);
261+
return foo.unwrap_or(2);
262+
}
263+
"#,
264+
)
265+
}
266+
267+
#[test]
268+
fn replace_or_else_with_or_call() {
269+
check_assist(
270+
replace_with_eager_method,
271+
r#"
272+
//- minicore: option, fn
273+
fn foo() {
274+
let foo = Some(1);
275+
return foo.unwrap_$0or_else(x);
276+
}
277+
278+
fn x() -> i32 { 0 }
279+
"#,
280+
r#"
281+
fn foo() {
282+
let foo = Some(1);
283+
return foo.unwrap_or(x());
284+
}
285+
286+
fn x() -> i32 { 0 }
287+
"#,
288+
)
289+
}
290+
291+
#[test]
292+
fn replace_or_else_with_or_map() {
293+
check_assist(
294+
replace_with_eager_method,
295+
r#"
296+
//- minicore: option, fn
297+
fn foo() {
298+
let foo = Some("foo");
299+
return foo.map$0_or_else(|| 42, |v| v.len());
300+
}
301+
"#,
302+
r#"
303+
fn foo() {
304+
let foo = Some("foo");
305+
return foo.map_or(42, |v| v.len());
306+
}
307+
"#,
308+
)
309+
}
310+
}

0 commit comments

Comments
 (0)