Skip to content

Commit f7a29e4

Browse files
committed
Auto merge of rust-lang#16100 - Young-Flash:assoc_func_quickfix, r=Veykril
feat: add assoc func quickfix for `unresolved_method` diagnostic ![demo](https://github.com/rust-lang/rust-analyzer/assets/71162630/1ea1d8b8-3436-4251-a512-e0f9de01a13c) close rust-lang/rust-analyzer#13247 EDIT: I think add a demo gif would be helpful to `@lnicola` when he make a release change log :)
2 parents 7659109 + 91bd596 commit f7a29e4

File tree

5 files changed

+220
-9
lines changed

5 files changed

+220
-9
lines changed

crates/hir-ty/src/infer.rs

+1
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ pub enum InferenceDiagnostic {
217217
name: Name,
218218
/// Contains the type the field resolves to
219219
field_with_same_name: Option<Ty>,
220+
assoc_func_with_same_name: Option<AssocItemId>,
220221
},
221222
// FIXME: This should be emitted in body lowering
222223
BreakOutsideOfLoop {

crates/hir-ty/src/infer/expr.rs

+19
Original file line numberDiff line numberDiff line change
@@ -1575,11 +1575,30 @@ impl InferenceContext<'_> {
15751575
}
15761576
None => None,
15771577
};
1578+
1579+
let assoc_func_with_same_name = method_resolution::iterate_method_candidates(
1580+
&canonicalized_receiver.value,
1581+
self.db,
1582+
self.table.trait_env.clone(),
1583+
self.get_traits_in_scope().as_ref().left_or_else(|&it| it),
1584+
VisibleFromModule::Filter(self.resolver.module()),
1585+
Some(method_name),
1586+
method_resolution::LookupMode::Path,
1587+
|_ty, item, visible| {
1588+
if visible {
1589+
Some(item)
1590+
} else {
1591+
None
1592+
}
1593+
},
1594+
);
1595+
15781596
self.result.diagnostics.push(InferenceDiagnostic::UnresolvedMethodCall {
15791597
expr: tgt_expr,
15801598
receiver: receiver_ty.clone(),
15811599
name: method_name.clone(),
15821600
field_with_same_name: field_with_same_name_exists,
1601+
assoc_func_with_same_name,
15831602
});
15841603
(
15851604
receiver_ty,

crates/hir/src/diagnostics.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ pub use hir_ty::diagnostics::{CaseType, IncorrectCase};
88
use base_db::CrateId;
99
use cfg::{CfgExpr, CfgOptions};
1010
use either::Either;
11-
use hir_def::path::ModPath;
11+
use hir_def::{path::ModPath, AssocItemId};
1212
use hir_expand::{name::Name, HirFileId, InFile};
1313
use syntax::{ast, AstPtr, SyntaxError, SyntaxNodePtr, TextRange};
1414

@@ -215,6 +215,7 @@ pub struct UnresolvedMethodCall {
215215
pub receiver: Type,
216216
pub name: Name,
217217
pub field_with_same_name: Option<Type>,
218+
pub assoc_func_with_same_name: Option<AssocItemId>,
218219
}
219220

220221
#[derive(Debug)]

crates/hir/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1680,6 +1680,7 @@ impl DefWithBody {
16801680
receiver,
16811681
name,
16821682
field_with_same_name,
1683+
assoc_func_with_same_name,
16831684
} => {
16841685
let expr = expr_syntax(*expr);
16851686

@@ -1691,6 +1692,7 @@ impl DefWithBody {
16911692
field_with_same_name: field_with_same_name
16921693
.clone()
16931694
.map(|ty| Type::new(db, DefWithBodyId::from(self), ty)),
1695+
assoc_func_with_same_name: assoc_func_with_same_name.clone(),
16941696
}
16951697
.into(),
16961698
)

crates/ide-diagnostics/src/handlers/unresolved_method.rs

+196-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
use hir::{db::ExpandDatabase, HirDisplay};
1+
use hir::{db::ExpandDatabase, AssocItem, HirDisplay, InFile};
22
use ide_db::{
33
assists::{Assist, AssistId, AssistKind},
44
base_db::FileRange,
55
label::Label,
66
source_change::SourceChange,
77
};
8-
use syntax::{ast, AstNode, TextRange};
8+
use syntax::{
9+
ast::{self, make, HasArgList},
10+
AstNode, SmolStr, TextRange,
11+
};
912
use text_edit::TextEdit;
1013

1114
use crate::{adjusted_display_range_new, Diagnostic, DiagnosticCode, DiagnosticsContext};
@@ -17,15 +20,17 @@ pub(crate) fn unresolved_method(
1720
ctx: &DiagnosticsContext<'_>,
1821
d: &hir::UnresolvedMethodCall,
1922
) -> Diagnostic {
20-
let field_suffix = if d.field_with_same_name.is_some() {
23+
let suffix = if d.field_with_same_name.is_some() {
2124
", but a field with a similar name exists"
25+
} else if d.assoc_func_with_same_name.is_some() {
26+
", but an associated function with a similar name exists"
2227
} else {
2328
""
2429
};
2530
Diagnostic::new(
2631
DiagnosticCode::RustcHardError("E0599"),
2732
format!(
28-
"no method `{}` on type `{}`{field_suffix}",
33+
"no method `{}` on type `{}`{suffix}",
2934
d.name.display(ctx.sema.db),
3035
d.receiver.display(ctx.sema.db)
3136
),
@@ -46,19 +51,35 @@ pub(crate) fn unresolved_method(
4651
}
4752

4853
fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedMethodCall) -> Option<Vec<Assist>> {
49-
if let Some(ty) = &d.field_with_same_name {
54+
let field_fix = if let Some(ty) = &d.field_with_same_name {
5055
field_fix(ctx, d, ty)
5156
} else {
5257
// FIXME: add quickfix
5358
None
59+
};
60+
61+
let assoc_func_fix = assoc_func_fix(ctx, d);
62+
63+
let mut fixes = vec![];
64+
if let Some(field_fix) = field_fix {
65+
fixes.push(field_fix);
66+
}
67+
if let Some(assoc_func_fix) = assoc_func_fix {
68+
fixes.push(assoc_func_fix);
69+
}
70+
71+
if fixes.is_empty() {
72+
None
73+
} else {
74+
Some(fixes)
5475
}
5576
}
5677

5778
fn field_fix(
5879
ctx: &DiagnosticsContext<'_>,
5980
d: &hir::UnresolvedMethodCall,
6081
ty: &hir::Type,
61-
) -> Option<Vec<Assist>> {
82+
) -> Option<Assist> {
6283
if !ty.impls_fnonce(ctx.sema.db) {
6384
return None;
6485
}
@@ -78,7 +99,7 @@ fn field_fix(
7899
}
79100
_ => return None,
80101
};
81-
Some(vec![Assist {
102+
Some(Assist {
82103
id: AssistId("expected-method-found-field-fix", AssistKind::QuickFix),
83104
label: Label::new("Use parentheses to call the value of the field".to_string()),
84105
group: None,
@@ -88,13 +109,180 @@ fn field_fix(
88109
(file_id, TextEdit::insert(range.end(), ")".to_owned())),
89110
])),
90111
trigger_signature_help: false,
91-
}])
112+
})
113+
}
114+
115+
fn assoc_func_fix(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedMethodCall) -> Option<Assist> {
116+
if let Some(assoc_item_id) = d.assoc_func_with_same_name {
117+
let db = ctx.sema.db;
118+
119+
let expr_ptr = &d.expr;
120+
let root = db.parse_or_expand(expr_ptr.file_id);
121+
let expr: ast::Expr = expr_ptr.value.to_node(&root);
122+
123+
let call = ast::MethodCallExpr::cast(expr.syntax().clone())?;
124+
let range = InFile::new(expr_ptr.file_id, call.syntax().text_range())
125+
.original_node_file_range_rooted(db)
126+
.range;
127+
128+
let receiver = call.receiver()?;
129+
let receiver_type = &ctx.sema.type_of_expr(&receiver)?.original;
130+
131+
let need_to_take_receiver_as_first_arg = match hir::AssocItem::from(assoc_item_id) {
132+
AssocItem::Function(f) => {
133+
let assoc_fn_params = f.assoc_fn_params(db);
134+
if assoc_fn_params.is_empty() {
135+
false
136+
} else {
137+
assoc_fn_params
138+
.first()
139+
.map(|first_arg| {
140+
// For generic type, say `Box`, take `Box::into_raw(b: Self)` as example,
141+
// type of `b` is `Self`, which is `Box<T, A>`, containing unspecified generics.
142+
// However, type of `receiver` is specified, it could be `Box<i32, Global>` or something like that,
143+
// so `first_arg.ty() == receiver_type` evaluate to `false` here.
144+
// Here add `first_arg.ty().as_adt() == receiver_type.as_adt()` as guard,
145+
// apply `.as_adt()` over `Box<T, A>` or `Box<i32, Global>` gets `Box`, so we get `true` here.
146+
147+
// FIXME: it fails when type of `b` is `Box` with other generic param different from `receiver`
148+
first_arg.ty() == receiver_type
149+
|| first_arg.ty().as_adt() == receiver_type.as_adt()
150+
})
151+
.unwrap_or(false)
152+
}
153+
}
154+
_ => false,
155+
};
156+
157+
let mut receiver_type_adt_name = receiver_type.as_adt()?.name(db).to_smol_str().to_string();
158+
159+
let generic_parameters: Vec<SmolStr> = receiver_type.generic_parameters(db).collect();
160+
// if receiver should be pass as first arg in the assoc func,
161+
// we could omit generic parameters cause compiler can deduce it automatically
162+
if !need_to_take_receiver_as_first_arg && !generic_parameters.is_empty() {
163+
let generic_parameters = generic_parameters.join(", ").to_string();
164+
receiver_type_adt_name =
165+
format!("{}::<{}>", receiver_type_adt_name, generic_parameters);
166+
}
167+
168+
let method_name = call.name_ref()?;
169+
let assoc_func_call = format!("{}::{}()", receiver_type_adt_name, method_name);
170+
171+
let assoc_func_call = make::expr_path(make::path_from_text(&assoc_func_call));
172+
173+
let args: Vec<_> = if need_to_take_receiver_as_first_arg {
174+
std::iter::once(receiver).chain(call.arg_list()?.args()).collect()
175+
} else {
176+
call.arg_list()?.args().collect()
177+
};
178+
let args = make::arg_list(args);
179+
180+
let assoc_func_call_expr_string = make::expr_call(assoc_func_call, args).to_string();
181+
182+
let file_id = ctx.sema.original_range_opt(call.receiver()?.syntax())?.file_id;
183+
184+
Some(Assist {
185+
id: AssistId("method_call_to_assoc_func_call_fix", AssistKind::QuickFix),
186+
label: Label::new(format!(
187+
"Use associated func call instead: `{}`",
188+
assoc_func_call_expr_string
189+
)),
190+
group: None,
191+
target: range,
192+
source_change: Some(SourceChange::from_text_edit(
193+
file_id,
194+
TextEdit::replace(range, assoc_func_call_expr_string),
195+
)),
196+
trigger_signature_help: false,
197+
})
198+
} else {
199+
None
200+
}
92201
}
93202

94203
#[cfg(test)]
95204
mod tests {
96205
use crate::tests::{check_diagnostics, check_fix};
97206

207+
#[test]
208+
fn test_assoc_func_fix() {
209+
check_fix(
210+
r#"
211+
struct A {}
212+
213+
impl A {
214+
fn hello() {}
215+
}
216+
fn main() {
217+
let a = A{};
218+
a.hello$0();
219+
}
220+
"#,
221+
r#"
222+
struct A {}
223+
224+
impl A {
225+
fn hello() {}
226+
}
227+
fn main() {
228+
let a = A{};
229+
A::hello();
230+
}
231+
"#,
232+
);
233+
}
234+
235+
#[test]
236+
fn test_assoc_func_diagnostic() {
237+
check_diagnostics(
238+
r#"
239+
struct A {}
240+
impl A {
241+
fn hello() {}
242+
}
243+
fn main() {
244+
let a = A{};
245+
a.hello();
246+
// ^^^^^ 💡 error: no method `hello` on type `A`, but an associated function with a similar name exists
247+
}
248+
"#,
249+
);
250+
}
251+
252+
#[test]
253+
fn test_assoc_func_fix_with_generic() {
254+
check_fix(
255+
r#"
256+
struct A<T, U> {
257+
a: T,
258+
b: U
259+
}
260+
261+
impl<T, U> A<T, U> {
262+
fn foo() {}
263+
}
264+
fn main() {
265+
let a = A {a: 0, b: ""};
266+
a.foo()$0;
267+
}
268+
"#,
269+
r#"
270+
struct A<T, U> {
271+
a: T,
272+
b: U
273+
}
274+
275+
impl<T, U> A<T, U> {
276+
fn foo() {}
277+
}
278+
fn main() {
279+
let a = A {a: 0, b: ""};
280+
A::<i32, &str>::foo();
281+
}
282+
"#,
283+
);
284+
}
285+
98286
#[test]
99287
fn smoke_test() {
100288
check_diagnostics(

0 commit comments

Comments
 (0)