]>
Commit | Line | Data |
---|---|---|
c0240ec0 | 1 | use hir::{db::ExpandDatabase, AssocItem, HirDisplay, InFile}; |
353b0b11 FG |
2 | use ide_db::{ |
3 | assists::{Assist, AssistId, AssistKind}, | |
4 | base_db::FileRange, | |
5 | label::Label, | |
6 | source_change::SourceChange, | |
7 | }; | |
c0240ec0 FG |
8 | use syntax::{ |
9 | ast::{self, make, HasArgList}, | |
10 | AstNode, SmolStr, TextRange, | |
11 | }; | |
353b0b11 FG |
12 | use text_edit::TextEdit; |
13 | ||
c620b35d | 14 | use crate::{adjusted_display_range, Diagnostic, DiagnosticCode, DiagnosticsContext}; |
353b0b11 FG |
15 | |
16 | // Diagnostic: unresolved-method | |
17 | // | |
18 | // This diagnostic is triggered if a method does not exist on a given type. | |
19 | pub(crate) fn unresolved_method( | |
20 | ctx: &DiagnosticsContext<'_>, | |
21 | d: &hir::UnresolvedMethodCall, | |
22 | ) -> Diagnostic { | |
c0240ec0 | 23 | let suffix = if d.field_with_same_name.is_some() { |
353b0b11 | 24 | ", but a field with a similar name exists" |
c0240ec0 FG |
25 | } else if d.assoc_func_with_same_name.is_some() { |
26 | ", but an associated function with a similar name exists" | |
353b0b11 FG |
27 | } else { |
28 | "" | |
29 | }; | |
4b012472 | 30 | Diagnostic::new( |
add651ee | 31 | DiagnosticCode::RustcHardError("E0599"), |
353b0b11 | 32 | format!( |
c0240ec0 | 33 | "no method `{}` on type `{}`{suffix}", |
fe692bf9 | 34 | d.name.display(ctx.sema.db), |
353b0b11 FG |
35 | d.receiver.display(ctx.sema.db) |
36 | ), | |
c620b35d | 37 | adjusted_display_range(ctx, d.expr, &|expr| { |
4b012472 FG |
38 | Some( |
39 | match expr { | |
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 | }), | |
353b0b11 FG |
48 | ) |
49 | .with_fixes(fixes(ctx, d)) | |
50 | .experimental() | |
51 | } | |
52 | ||
53 | fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedMethodCall) -> Option<Vec<Assist>> { | |
c0240ec0 | 54 | let field_fix = if let Some(ty) = &d.field_with_same_name { |
353b0b11 FG |
55 | field_fix(ctx, d, ty) |
56 | } else { | |
57 | // FIXME: add quickfix | |
58 | None | |
c0240ec0 FG |
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) | |
353b0b11 FG |
75 | } |
76 | } | |
77 | ||
78 | fn field_fix( | |
79 | ctx: &DiagnosticsContext<'_>, | |
80 | d: &hir::UnresolvedMethodCall, | |
81 | ty: &hir::Type, | |
c0240ec0 | 82 | ) -> Option<Assist> { |
353b0b11 FG |
83 | if !ty.impls_fnonce(ctx.sema.db) { |
84 | return None; | |
85 | } | |
86 | let expr_ptr = &d.expr; | |
fe692bf9 | 87 | let root = ctx.sema.db.parse_or_expand(expr_ptr.file_id); |
353b0b11 FG |
88 | let expr = expr_ptr.value.to_node(&root); |
89 | let (file_id, range) = match expr { | |
90 | ast::Expr::MethodCallExpr(mcall) => { | |
91 | let FileRange { range, file_id } = | |
92 | ctx.sema.original_range_opt(mcall.receiver()?.syntax())?; | |
93 | let FileRange { range: range2, file_id: file_id2 } = | |
94 | ctx.sema.original_range_opt(mcall.name_ref()?.syntax())?; | |
95 | if file_id != file_id2 { | |
96 | return None; | |
97 | } | |
98 | (file_id, TextRange::new(range.start(), range2.end())) | |
99 | } | |
100 | _ => return None, | |
101 | }; | |
c0240ec0 | 102 | Some(Assist { |
353b0b11 | 103 | id: AssistId("expected-method-found-field-fix", AssistKind::QuickFix), |
c620b35d | 104 | label: Label::new("Use parentheses to call the value of the field".to_owned()), |
353b0b11 FG |
105 | group: None, |
106 | target: range, | |
107 | source_change: Some(SourceChange::from_iter([ | |
108 | (file_id, TextEdit::insert(range.start(), "(".to_owned())), | |
109 | (file_id, TextEdit::insert(range.end(), ")".to_owned())), | |
110 | ])), | |
111 | trigger_signature_help: false, | |
c0240ec0 FG |
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(", "); | |
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 | } | |
353b0b11 FG |
201 | } |
202 | ||
203 | #[cfg(test)] | |
204 | mod tests { | |
205 | use crate::tests::{check_diagnostics, check_fix}; | |
206 | ||
c0240ec0 FG |
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 | ||
353b0b11 FG |
286 | #[test] |
287 | fn smoke_test() { | |
288 | check_diagnostics( | |
289 | r#" | |
290 | fn main() { | |
291 | ().foo(); | |
4b012472 FG |
292 | // ^^^ error: no method `foo` on type `()` |
293 | } | |
294 | "#, | |
295 | ); | |
296 | } | |
297 | ||
298 | #[test] | |
299 | fn smoke_test_in_macro_def_site() { | |
300 | check_diagnostics( | |
301 | r#" | |
302 | macro_rules! m { | |
303 | ($rcv:expr) => { | |
304 | $rcv.foo() | |
305 | } | |
306 | } | |
307 | fn main() { | |
308 | m!(()); | |
309 | // ^^^^^^ error: no method `foo` on type `()` | |
310 | } | |
311 | "#, | |
312 | ); | |
313 | } | |
314 | ||
315 | #[test] | |
316 | fn smoke_test_in_macro_call_site() { | |
317 | check_diagnostics( | |
318 | r#" | |
319 | macro_rules! m { | |
320 | ($ident:ident) => { | |
321 | ().$ident() | |
322 | } | |
323 | } | |
324 | fn main() { | |
325 | m!(foo); | |
326 | // ^^^ error: no method `foo` on type `()` | |
353b0b11 FG |
327 | } |
328 | "#, | |
329 | ); | |
330 | } | |
331 | ||
332 | #[test] | |
333 | fn field() { | |
334 | check_diagnostics( | |
335 | r#" | |
336 | struct Foo { bar: i32 } | |
337 | fn foo() { | |
c620b35d FG |
338 | Foo { bar: 0 }.bar(); |
339 | // ^^^ error: no method `bar` on type `Foo`, but a field with a similar name exists | |
353b0b11 FG |
340 | } |
341 | "#, | |
342 | ); | |
343 | } | |
344 | ||
345 | #[test] | |
346 | fn callable_field() { | |
347 | check_fix( | |
348 | r#" | |
349 | //- minicore: fn | |
350 | struct Foo { bar: fn() } | |
351 | fn foo() { | |
352 | Foo { bar: foo }.b$0ar(); | |
353 | } | |
354 | "#, | |
355 | r#" | |
356 | struct Foo { bar: fn() } | |
357 | fn foo() { | |
358 | (Foo { bar: foo }.bar)(); | |
359 | } | |
360 | "#, | |
361 | ); | |
362 | } | |
363 | } |