ide_diagnostics/handlers/
unresolved_method.rs1use hir::{FileRange, HirDisplay, InFile, db::ExpandDatabase};
2use ide_db::text_edit::TextEdit;
3use ide_db::{
4 assists::{Assist, AssistId},
5 label::Label,
6 source_change::SourceChange,
7};
8use syntax::{
9 AstNode, SmolStr, TextRange, ToSmolStr,
10 ast::{self, HasArgList, make},
11 format_smolstr,
12};
13
14use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext, adjusted_display_range};
15
16pub(crate) fn unresolved_method(
20 ctx: &DiagnosticsContext<'_, '_>,
21 d: &hir::UnresolvedMethodCall<'_>,
22) -> Diagnostic {
23 let suffix = if d.field_with_same_name.is_some() {
24 ", 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"
27 } else {
28 ""
29 };
30 Diagnostic::new(
31 DiagnosticCode::RustcHardError("E0599"),
32 format!(
33 "no method `{}` on type `{}`{suffix}",
34 d.name.display(ctx.sema.db, ctx.edition),
35 d.receiver.display(ctx.sema.db, ctx.display_target)
36 ),
37 adjusted_display_range(ctx, d.expr, &|expr| {
38 Some(
39 match expr.left()? {
40 ast::Expr::MethodCallExpr(it) => it.name_ref(),
41 ast::Expr::FieldExpr(it) => it.name_ref(),
42 _ => None,
43 }?
44 .syntax()
45 .text_range(),
46 )
47 }),
48 )
49 .with_fixes(fixes(ctx, d))
50}
51
52fn fixes(
53 ctx: &DiagnosticsContext<'_, '_>,
54 d: &hir::UnresolvedMethodCall<'_>,
55) -> Option<Vec<Assist>> {
56 let field_fix = if let Some(ty) = &d.field_with_same_name {
57 field_fix(ctx, d, ty)
58 } else {
59 None
61 };
62
63 let assoc_func_fix = assoc_func_fix(ctx, d);
64
65 let mut fixes = vec![];
66 if let Some(field_fix) = field_fix {
67 fixes.push(field_fix);
68 }
69 if let Some(assoc_func_fix) = assoc_func_fix {
70 fixes.push(assoc_func_fix);
71 }
72
73 if fixes.is_empty() { None } else { Some(fixes) }
74}
75
76fn field_fix(
77 ctx: &DiagnosticsContext<'_, '_>,
78 d: &hir::UnresolvedMethodCall<'_>,
79 ty: &hir::Type<'_>,
80) -> Option<Assist> {
81 if !ty.impls_fnonce(ctx.sema.db) {
82 return None;
83 }
84 let expr_ptr = &d.expr;
85 let root = ctx.sema.db.parse_or_expand(expr_ptr.file_id);
86 let expr = expr_ptr.value.to_node(&root);
87 let (file_id, range) = match expr.left()? {
88 ast::Expr::MethodCallExpr(mcall) => {
89 let FileRange { range, file_id } =
90 ctx.sema.original_range_opt(mcall.receiver()?.syntax())?;
91 let FileRange { range: range2, file_id: file_id2 } =
92 ctx.sema.original_range_opt(mcall.name_ref()?.syntax())?;
93 if file_id != file_id2 {
94 return None;
95 }
96 (file_id, TextRange::new(range.start(), range2.end()))
97 }
98 _ => return None,
99 };
100 Some(Assist {
101 id: AssistId::quick_fix("expected-method-found-field-fix"),
102 label: Label::new("Use parentheses to call the value of the field".to_owned()),
103 group: None,
104 target: range,
105 source_change: Some(SourceChange::from_iter([
106 (file_id.file_id(ctx.sema.db), TextEdit::insert(range.start(), "(".to_owned())),
107 (file_id.file_id(ctx.sema.db), TextEdit::insert(range.end(), ")".to_owned())),
108 ])),
109 command: None,
110 })
111}
112
113fn assoc_func_fix(
114 ctx: &DiagnosticsContext<'_, '_>,
115 d: &hir::UnresolvedMethodCall<'_>,
116) -> Option<Assist> {
117 if let Some(f) = d.assoc_func_with_same_name {
118 let db = ctx.sema.db;
119
120 let expr_ptr = &d.expr;
121 let root = db.parse_or_expand(expr_ptr.file_id);
122 let expr: ast::Expr = expr_ptr.value.to_node(&root).left()?;
123
124 let call = ast::MethodCallExpr::cast(expr.syntax().clone())?;
125 let range = InFile::new(expr_ptr.file_id, call.syntax().text_range())
126 .original_node_file_range_rooted_opt(db)?;
127
128 let receiver = call.receiver()?;
129 let receiver_type = &ctx.sema.type_of_expr(&receiver)?.original;
130
131 let assoc_fn_params = f.assoc_fn_params(db);
132 let need_to_take_receiver_as_first_arg = assoc_fn_params.first().is_some_and(|first_arg| {
133 first_arg.ty() == receiver_type || first_arg.ty().as_adt() == receiver_type.as_adt()
142 });
143
144 let mut receiver_type_adt_name =
145 receiver_type.as_adt()?.name(db).display_no_db(ctx.edition).to_smolstr();
146
147 let generic_parameters: Vec<SmolStr> =
148 receiver_type.generic_parameters(db, ctx.display_target).collect();
149 if !need_to_take_receiver_as_first_arg && !generic_parameters.is_empty() {
152 let generic_parameters = generic_parameters.join(", ");
153 receiver_type_adt_name =
154 format_smolstr!("{receiver_type_adt_name}::<{generic_parameters}>");
155 }
156
157 let method_name = call.name_ref()?;
158 let assoc_func_path = format!("{receiver_type_adt_name}::{method_name}");
159
160 let assoc_func_path = make::expr_path(make::path_from_text(&assoc_func_path));
161
162 let args: Vec<_> = if need_to_take_receiver_as_first_arg {
163 std::iter::once(receiver).chain(call.arg_list()?.args()).collect()
164 } else {
165 call.arg_list()?.args().collect()
166 };
167 let args = make::arg_list(args);
168
169 let assoc_func_call_expr_string = make::expr_call(assoc_func_path, args).to_string();
170
171 Some(Assist {
172 id: AssistId::quick_fix("method_call_to_assoc_func_call_fix"),
173 label: Label::new(format!(
174 "Use associated func call instead: `{assoc_func_call_expr_string}`"
175 )),
176 group: None,
177 target: range.range,
178 source_change: Some(SourceChange::from_text_edit(
179 range.file_id.file_id(ctx.sema.db),
180 TextEdit::replace(range.range, assoc_func_call_expr_string),
181 )),
182 command: None,
183 })
184 } else {
185 None
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use crate::tests::{check_diagnostics, check_fix};
192
193 #[test]
194 fn test_assoc_func_fix() {
195 check_fix(
196 r#"
197struct A {}
198
199impl A {
200 fn hello() {}
201}
202fn main() {
203 let a = A{};
204 a.hello$0();
205}
206"#,
207 r#"
208struct A {}
209
210impl A {
211 fn hello() {}
212}
213fn main() {
214 let a = A{};
215 A::hello();
216}
217"#,
218 );
219 }
220
221 #[test]
222 fn test_assoc_func_diagnostic() {
223 check_diagnostics(
224 r#"
225struct A {}
226impl A {
227 fn hello() {}
228}
229fn main() {
230 let a = A{};
231 a.hello();
232 // ^^^^^ 💡 error: no method `hello` on type `A`, but an associated function with a similar name exists
233}
234"#,
235 );
236 }
237
238 #[test]
239 fn test_assoc_func_fix_with_generic() {
240 check_fix(
241 r#"
242struct A<T, U> {
243 a: T,
244 b: U
245}
246
247impl<T, U> A<T, U> {
248 fn foo() {}
249}
250fn main() {
251 let a = A {a: 0, b: ""};
252 a.foo()$0;
253}
254"#,
255 r#"
256struct A<T, U> {
257 a: T,
258 b: U
259}
260
261impl<T, U> A<T, U> {
262 fn foo() {}
263}
264fn main() {
265 let a = A {a: 0, b: ""};
266 A::<i32, &str>::foo();
267}
268"#,
269 );
270 }
271
272 #[test]
273 fn smoke_test() {
274 check_diagnostics(
275 r#"
276fn main() {
277 ().foo();
278 // ^^^ error: no method `foo` on type `()`
279}
280"#,
281 );
282 }
283
284 #[test]
285 fn smoke_test_in_macro_def_site() {
286 check_diagnostics(
287 r#"
288macro_rules! m {
289 ($rcv:expr) => {
290 $rcv.foo()
291 }
292}
293fn main() {
294 m!(());
295 // ^^ error: no method `foo` on type `()`
296}
297"#,
298 );
299 }
300
301 #[test]
302 fn smoke_test_in_macro_call_site() {
303 check_diagnostics(
304 r#"
305macro_rules! m {
306 ($ident:ident) => {
307 ().$ident()
308 }
309}
310fn main() {
311 m!(foo);
312 // ^^^ error: no method `foo` on type `()`
313}
314"#,
315 );
316 }
317
318 #[test]
319 fn field() {
320 check_diagnostics(
321 r#"
322struct Foo { bar: i32 }
323fn foo() {
324 Foo { bar: 0 }.bar();
325 // ^^^ error: no method `bar` on type `Foo`, but a field with a similar name exists
326}
327"#,
328 );
329 }
330
331 #[test]
332 fn callable_field() {
333 check_fix(
334 r#"
335//- minicore: fn
336struct Foo { bar: fn() }
337fn foo() {
338 Foo { bar: foo }.b$0ar();
339}
340"#,
341 r#"
342struct Foo { bar: fn() }
343fn foo() {
344 (Foo { bar: foo }.bar)();
345}
346"#,
347 );
348 }
349
350 #[test]
351 fn iter_collect() {
352 check_diagnostics(
353 r#"
354//- minicore: unsize, coerce_unsized, iterator, iterators, sized
355struct Map<K, V>(K, V);
356impl<K, V> FromIterator<(K, V)> for Map<K, V> {
357 fn from_iter<T: IntoIterator<Item = (K, V)>>(_iter: T) -> Self {
358 loop {}
359 }
360}
361
362fn foo() -> Map<i32, &'static [&'static str]> {
363 [
364 (123, &["abc", "def"] as _),
365 (456, &["ghi"] as _),
366 ].into_iter().collect()
367}
368 "#,
369 );
370 }
371}