1use either::Either;
2use hir::{Adt, FileRange, HasSource, HirDisplay, InFile, Struct, Union, db::ExpandDatabase};
3use ide_db::text_edit::TextEdit;
4use ide_db::{
5 assists::{Assist, AssistId},
6 helpers::is_editable_crate,
7 label::Label,
8 source_change::{SourceChange, SourceChangeBuilder},
9};
10use syntax::{
11 AstNode, AstPtr, Direction, SyntaxKind, TextSize, algo,
12 ast::{self, FieldList, Name, Visibility, edit::IndentLevel, make},
13};
14use syntax::{
15 SyntaxNode,
16 ast::{Type, edit::AstNodeEdit},
17};
18
19use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext, adjusted_display_range};
20
21pub(crate) fn unresolved_field(
25 ctx: &DiagnosticsContext<'_>,
26 d: &hir::UnresolvedField<'_>,
27) -> Diagnostic {
28 let method_suffix = if d.method_with_same_name_exists {
29 ", but a method with a similar name exists"
30 } else {
31 ""
32 };
33 Diagnostic::new(
34 DiagnosticCode::RustcHardError("E0559"),
35 format!(
36 "no field `{}` on type `{}`{method_suffix}",
37 d.name.display(ctx.sema.db, ctx.edition),
38 d.receiver.display(ctx.sema.db, ctx.display_target)
39 ),
40 adjusted_display_range(ctx, d.expr, &|expr| {
41 Some(
42 match expr.left()? {
43 ast::Expr::MethodCallExpr(it) => it.name_ref(),
44 ast::Expr::FieldExpr(it) => it.name_ref(),
45 _ => None,
46 }?
47 .syntax()
48 .text_range(),
49 )
50 }),
51 )
52 .with_fixes(fixes(ctx, d))
53}
54
55fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField<'_>) -> Option<Vec<Assist>> {
56 let mut fixes = Vec::new();
57 if d.method_with_same_name_exists {
58 fixes.extend(method_fix(ctx, &d.expr));
59 }
60 fixes.extend(field_fix(ctx, d));
61 if fixes.is_empty() { None } else { Some(fixes) }
62}
63
64fn field_fix(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField<'_>) -> Option<Assist> {
66 let root = ctx.sema.db.parse_or_expand(d.expr.file_id);
68 let expr = d.expr.value.to_node(&root).left()?;
69
70 let error_range = ctx.sema.original_range_opt(expr.syntax())?;
71 let field_name = d.name.as_str();
72 let adt = d.receiver.strip_references().as_adt()?;
74 let target_module = adt.module(ctx.sema.db);
75
76 let suggested_type = if let Some(new_field_type) =
77 ctx.sema.type_of_expr(&expr).map(|v| v.adjusted()).filter(|it| !it.is_unknown())
78 {
79 let display =
80 new_field_type.display_source_code(ctx.sema.db, target_module.into(), false).ok();
81 make::ty(display.as_deref().unwrap_or("()"))
82 } else {
83 make::ty("()")
84 };
85
86 if !is_editable_crate(target_module.krate(ctx.sema.db), ctx.sema.db)
87 || SyntaxKind::from_keyword(field_name, ctx.edition).is_some()
88 {
89 return None;
90 }
91
92 match adt {
93 Adt::Struct(adt_struct) => {
94 add_field_to_struct_fix(ctx, adt_struct, field_name, suggested_type, error_range)
95 }
96 Adt::Union(adt_union) => {
97 add_variant_to_union(ctx, adt_union, field_name, suggested_type, error_range)
98 }
99 _ => None,
100 }
101}
102
103fn add_variant_to_union(
104 ctx: &DiagnosticsContext<'_>,
105 adt_union: Union,
106 field_name: &str,
107 suggested_type: Type,
108 error_range: FileRange,
109) -> Option<Assist> {
110 let adt_source = adt_union.source(ctx.sema.db)?;
111 let adt_syntax = adt_source.syntax();
112 let field_list = adt_source.value.record_field_list()?;
113 let range = adt_syntax.original_file_range_rooted(ctx.sema.db);
114 let field_name = make::name(field_name);
115
116 let (offset, record_field) =
117 record_field_layout(None, field_name, suggested_type, field_list, adt_syntax.value)?;
118
119 let mut src_change_builder = SourceChangeBuilder::new(range.file_id.file_id(ctx.sema.db));
120 src_change_builder.insert(offset, record_field);
121 Some(Assist {
122 id: AssistId::quick_fix("add-variant-to-union"),
123 label: Label::new("Add field to union".to_owned()),
124 group: None,
125 target: error_range.range,
126 source_change: Some(src_change_builder.finish()),
127 command: None,
128 })
129}
130
131fn add_field_to_struct_fix(
132 ctx: &DiagnosticsContext<'_>,
133 adt_struct: Struct,
134 field_name: &str,
135 suggested_type: Type,
136 error_range: FileRange,
137) -> Option<Assist> {
138 let struct_source = adt_struct.source(ctx.sema.db)?;
139 let struct_syntax = struct_source.syntax();
140 let struct_range = struct_syntax.original_file_range_rooted(ctx.sema.db);
141 let field_list = struct_source.value.field_list();
142 match field_list {
143 Some(FieldList::RecordFieldList(field_list)) => {
144 let visibility = if error_range.file_id == struct_range.file_id {
146 None
147 } else {
148 Some(make::visibility_pub_crate())
149 };
150
151 let field_name = match field_name.chars().next() {
152 Some(ch) if ch.is_numeric() => return None,
153 Some(_) => make::name(field_name),
154 None => return None,
155 };
156
157 let (offset, record_field) = record_field_layout(
158 visibility,
159 field_name,
160 suggested_type,
161 field_list,
162 struct_syntax.value,
163 )?;
164
165 let mut src_change_builder =
166 SourceChangeBuilder::new(struct_range.file_id.file_id(ctx.sema.db));
167
168 src_change_builder.insert(offset, record_field);
170 Some(Assist {
171 id: AssistId::quick_fix("add-field-to-record-struct"),
172 label: Label::new("Add field to Record Struct".to_owned()),
173 group: None,
174 target: error_range.range,
175 source_change: Some(src_change_builder.finish()),
176 command: None,
177 })
178 }
179 None => {
180 let mut src_change_builder =
182 SourceChangeBuilder::new(struct_range.file_id.file_id(ctx.sema.db));
183 let field_name = match field_name.chars().next() {
184 Some(ch) if ch.is_numeric() => return None,
186 Some(_) => make::name(field_name),
187 None => return None,
188 };
189 let visibility = if error_range.file_id == struct_range.file_id {
190 None
191 } else {
192 Some(make::visibility_pub_crate())
193 };
194 let indent = IndentLevel::from_node(struct_syntax.value);
196
197 let field =
198 make::record_field(visibility, field_name, suggested_type).indent(indent + 1);
199 let semi_colon =
201 algo::skip_trivia_token(struct_syntax.value.last_token()?, Direction::Prev)?;
202 if semi_colon.kind() != SyntaxKind::SEMICOLON {
203 return None;
204 }
205 src_change_builder.replace(
206 semi_colon.text_range(),
207 format!(" {{\n{}{field},\n{indent}}}", indent + 1),
208 );
209
210 Some(Assist {
211 id: AssistId::quick_fix("convert-unit-struct-to-record-struct"),
212 label: Label::new("Convert Unit Struct to Record Struct and add field".to_owned()),
213 group: None,
214 target: error_range.range,
215 source_change: Some(src_change_builder.finish()),
216 command: None,
217 })
218 }
219 Some(FieldList::TupleFieldList(_tuple)) => {
220 None
222 }
223 }
224}
225
226fn record_field_layout(
228 visibility: Option<Visibility>,
229 name: Name,
230 suggested_type: Type,
231 field_list: ast::RecordFieldList,
232 struct_syntax: &SyntaxNode,
233) -> Option<(TextSize, String)> {
234 let (offset, needs_comma, indent) = match field_list.fields().last() {
235 Some(record_field) => {
236 let syntax = algo::skip_trivia_token(field_list.r_curly_token()?, Direction::Prev)?;
237
238 let last_field_syntax = record_field.syntax();
239 let last_field_indent = IndentLevel::from_node(last_field_syntax);
240 (
241 last_field_syntax.text_range().end(),
242 syntax.kind() != SyntaxKind::COMMA,
243 last_field_indent,
244 )
245 }
246 None => {
248 let indent = IndentLevel::from_node(struct_syntax) + 1;
249 let offset = field_list.l_curly_token()?.text_range().end();
250 (offset, false, indent)
251 }
252 };
253 let trailing_new_line = if !field_list.syntax().text().contains_char('\n') {
254 format!("\n{}", field_list.indent_level())
255 } else {
256 String::new()
257 };
258 let comma = if needs_comma { ",\n" } else { "\n" };
259 let record_field = make::record_field(visibility, name, suggested_type);
260
261 Some((offset, format!("{comma}{indent}{record_field}{trailing_new_line}")))
262}
263
264fn method_fix(
266 ctx: &DiagnosticsContext<'_>,
267 expr_ptr: &InFile<AstPtr<Either<ast::Expr, ast::Pat>>>,
268) -> Option<Assist> {
269 let root = ctx.sema.db.parse_or_expand(expr_ptr.file_id);
270 let expr = expr_ptr.value.to_node(&root);
271 let FileRange { range, file_id } = ctx.sema.original_range_opt(expr.syntax())?;
272 Some(Assist {
273 id: AssistId::quick_fix("expected-field-found-method-call-fix"),
274 label: Label::new("Use parentheses to call the method".to_owned()),
275 group: None,
276 target: range,
277 source_change: Some(SourceChange::from_text_edit(
278 file_id.file_id(ctx.sema.db),
279 TextEdit::insert(range.end(), "()".to_owned()),
280 )),
281 command: None,
282 })
283}
284#[cfg(test)]
285mod tests {
286
287 use crate::{
288 DiagnosticsConfig,
289 tests::{
290 check_diagnostics, check_diagnostics_with_config, check_diagnostics_with_disabled,
291 check_fix, check_no_fix,
292 },
293 };
294
295 #[test]
296 fn smoke_test() {
297 check_diagnostics(
298 r#"
299fn main() {
300 ().foo;
301 // ^^^ error: no field `foo` on type `()`
302}
303"#,
304 );
305 }
306
307 #[test]
308 fn method_clash() {
309 check_diagnostics(
310 r#"
311struct Foo;
312impl Foo {
313 fn bar(&self) {}
314}
315fn foo() {
316 Foo.bar;
317 // ^^^ 💡 error: no field `bar` on type `Foo`, but a method with a similar name exists
318}
319"#,
320 );
321 }
322
323 #[test]
324 fn method_trait_() {
325 check_diagnostics(
326 r#"
327struct Foo;
328trait Bar {
329 fn bar(&self) {}
330}
331impl Bar for Foo {}
332fn foo() {
333 Foo.bar;
334 // ^^^ 💡 error: no field `bar` on type `Foo`, but a method with a similar name exists
335}
336"#,
337 );
338 }
339
340 #[test]
341 fn method_trait_2() {
342 check_diagnostics(
343 r#"
344struct Foo;
345trait Bar {
346 fn bar(&self);
347}
348impl Bar for Foo {
349 fn bar(&self) {}
350}
351fn foo() {
352 Foo.bar;
353 // ^^^ 💡 error: no field `bar` on type `Foo`, but a method with a similar name exists
354}
355"#,
356 );
357 }
358
359 #[test]
360 fn no_diagnostic_on_unknown() {
361 check_diagnostics_with_disabled(
362 r#"
363fn foo() {
364 x.foo;
365 (&x).foo;
366 (&((x,),),).foo;
367}
368"#,
369 &["E0425"],
370 );
371 }
372
373 #[test]
374 fn no_diagnostic_for_missing_name() {
375 let mut config = DiagnosticsConfig::test_sample();
376 config.disabled.insert("syntax-error".to_owned());
377 check_diagnostics_with_config(config, "fn foo() { (). }");
378 }
379
380 #[test]
381 fn unresolved_field_fix_on_unit() {
382 check_fix(
383 r#"
384 mod indent {
385 struct Foo;
386
387 fn foo() {
388 Foo.bar$0;
389 }
390 }
391 "#,
392 r#"
393 mod indent {
394 struct Foo {
395 bar: (),
396 }
397
398 fn foo() {
399 Foo.bar;
400 }
401 }
402 "#,
403 );
404 }
405 #[test]
406 fn unresolved_field_fix_on_empty() {
407 check_fix(
408 r#"
409 mod indent {
410 struct Foo{
411 }
412
413 fn foo() {
414 let foo = Foo{};
415 foo.bar$0;
416 }
417 }
418 "#,
419 r#"
420 mod indent {
421 struct Foo{
422 bar: ()
423 }
424
425 fn foo() {
426 let foo = Foo{};
427 foo.bar;
428 }
429 }
430 "#,
431 );
432
433 check_fix(
434 r#"
435 mod indent {
436 struct Foo {}
437
438 fn foo() {
439 let foo = Foo{};
440 foo.bar$0;
441 }
442 }
443 "#,
444 r#"
445 mod indent {
446 struct Foo {
447 bar: ()
448 }
449
450 fn foo() {
451 let foo = Foo{};
452 foo.bar;
453 }
454 }
455 "#,
456 );
457 }
458 #[test]
459 fn unresolved_field_fix_on_struct() {
460 check_fix(
461 r#"
462 struct Foo{
463 a: i32
464 }
465
466 fn foo() {
467 let foo = Foo{a: 0};
468 foo.bar$0;
469 }
470 "#,
471 r#"
472 struct Foo{
473 a: i32,
474 bar: ()
475 }
476
477 fn foo() {
478 let foo = Foo{a: 0};
479 foo.bar;
480 }
481 "#,
482 );
483 }
484 #[test]
485 fn unresolved_field_fix_on_union() {
486 check_fix(
487 r#"
488 union Foo{
489 a: i32
490 }
491
492 fn foo() {
493 let foo = Foo{a: 0};
494 foo.bar$0;
495 }
496 "#,
497 r#"
498 union Foo{
499 a: i32,
500 bar: ()
501 }
502
503 fn foo() {
504 let foo = Foo{a: 0};
505 foo.bar;
506 }
507 "#,
508 );
509 }
510
511 #[test]
512 fn no_fix_when_indexed() {
513 check_no_fix(
514 r#"
515 struct Kek {}
516impl Kek {
517 pub fn foo(self) {
518 self.$00
519 }
520}
521
522fn main() {}
523 "#,
524 )
525 }
526
527 #[test]
528 fn no_fix_when_without_field() {
529 check_no_fix(
530 r#"
531 struct Kek {}
532impl Kek {
533 pub fn foo(self) {
534 self.$0
535 }
536}
537
538fn main() {}
539 "#,
540 )
541 }
542
543 #[test]
544 fn regression_18683() {
545 check_diagnostics(
546 r#"
547struct S;
548impl S {
549 fn f(self) {
550 self.self
551 // ^^^^ error: no field `self` on type `S`
552 }
553}
554 "#,
555 );
556 }
557}