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