Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit a58f574

Browse files
committed
Auto merge of rust-lang#16762 - wyatt-herkamp:master, r=Veykril
Added QuickFix for unresolved field. Adds an AssistKind::QuickFix for unresolved field. Currently, Snippets are not available for Diagnostics. So the type defaults to the Unit type if the type is not able to be inferred. https://github.com/rust-lang/rust-analyzer/assets/11785959/bd4c67bd-f8cb-4254-9a87-641ae80a7a28 Closes rust-lang/rust-analyzer#4563
2 parents a5035f4 + 4f0bc1a commit a58f574

File tree

1 file changed

+293
-8
lines changed

1 file changed

+293
-8
lines changed

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

Lines changed: 293 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
1-
use hir::{db::ExpandDatabase, HirDisplay, InFile};
1+
use std::iter;
2+
3+
use hir::{db::ExpandDatabase, Adt, HasSource, HirDisplay, InFile, Struct, Union};
24
use ide_db::{
35
assists::{Assist, AssistId, AssistKind},
46
base_db::FileRange,
7+
helpers::is_editable_crate,
58
label::Label,
6-
source_change::SourceChange,
9+
source_change::{SourceChange, SourceChangeBuilder},
10+
};
11+
use syntax::{
12+
algo,
13+
ast::{self, edit::IndentLevel, make, FieldList, Name, Visibility},
14+
AstNode, AstPtr, Direction, SyntaxKind, TextSize,
15+
};
16+
use syntax::{
17+
ast::{edit::AstNodeEdit, Type},
18+
SyntaxNode,
719
};
8-
use syntax::{ast, AstNode, AstPtr};
920
use text_edit::TextEdit;
1021

1122
use crate::{adjusted_display_range, Diagnostic, DiagnosticCode, DiagnosticsContext};
@@ -46,15 +57,191 @@ pub(crate) fn unresolved_field(
4657
}
4758

4859
fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField) -> Option<Vec<Assist>> {
49-
if d.method_with_same_name_exists {
50-
method_fix(ctx, &d.expr)
51-
} else {
52-
// FIXME: add quickfix
60+
let mut fixes = if d.method_with_same_name_exists { method_fix(ctx, &d.expr) } else { None };
61+
if let Some(fix) = add_field_fix(ctx, d) {
62+
fixes.get_or_insert_with(Vec::new).extend(fix);
63+
}
64+
fixes
65+
}
66+
67+
fn add_field_fix(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField) -> Option<Vec<Assist>> {
68+
// Get the FileRange of the invalid field access
69+
let root = ctx.sema.db.parse_or_expand(d.expr.file_id);
70+
let expr = d.expr.value.to_node(&root);
71+
72+
let error_range = ctx.sema.original_range_opt(expr.syntax())?;
73+
// Convert the receiver to an ADT
74+
let adt = d.receiver.as_adt()?;
75+
let target_module = adt.module(ctx.sema.db);
76+
77+
let suggested_type =
78+
if let Some(new_field_type) = ctx.sema.type_of_expr(&expr).map(|v| v.adjusted()) {
79+
let display =
80+
new_field_type.display_source_code(ctx.sema.db, target_module.into(), true).ok();
81+
make::ty(display.as_deref().unwrap_or("()"))
82+
} else {
83+
make::ty("()")
84+
};
5385

54-
None
86+
if !is_editable_crate(target_module.krate(), ctx.sema.db) {
87+
return None;
88+
}
89+
90+
// FIXME: Add Snippet Support
91+
let field_name = d.name.as_str()?;
92+
93+
match adt {
94+
Adt::Struct(adt_struct) => {
95+
add_field_to_struct_fix(ctx, adt_struct, field_name, suggested_type, error_range)
96+
}
97+
Adt::Union(adt_union) => {
98+
add_variant_to_union(ctx, adt_union, field_name, suggested_type, error_range)
99+
}
100+
_ => None,
55101
}
56102
}
103+
fn 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<Vec<Assist>> {
110+
let adt_source = adt_union.source(ctx.sema.db)?;
111+
let adt_syntax = adt_source.syntax();
112+
let Some(field_list) = adt_source.value.record_field_list() else {
113+
return None;
114+
};
115+
let range = adt_syntax.original_file_range(ctx.sema.db);
116+
let field_name = make::name(field_name);
57117

118+
let (offset, record_field) =
119+
record_field_layout(None, field_name, suggested_type, field_list, adt_syntax.value)?;
120+
121+
let mut src_change_builder = SourceChangeBuilder::new(range.file_id);
122+
src_change_builder.insert(offset, record_field);
123+
Some(vec![Assist {
124+
id: AssistId("add-variant-to-union", AssistKind::QuickFix),
125+
label: Label::new("Add field to union".to_owned()),
126+
group: None,
127+
target: error_range.range,
128+
source_change: Some(src_change_builder.finish()),
129+
trigger_signature_help: false,
130+
}])
131+
}
132+
fn add_field_to_struct_fix(
133+
ctx: &DiagnosticsContext<'_>,
134+
adt_struct: Struct,
135+
field_name: &str,
136+
suggested_type: Type,
137+
error_range: FileRange,
138+
) -> Option<Vec<Assist>> {
139+
let struct_source = adt_struct.source(ctx.sema.db)?;
140+
let struct_syntax = struct_source.syntax();
141+
let struct_range = struct_syntax.original_file_range(ctx.sema.db);
142+
let field_list = struct_source.value.field_list();
143+
match field_list {
144+
Some(FieldList::RecordFieldList(field_list)) => {
145+
// Get range of final field in the struct
146+
let visibility = if error_range.file_id == struct_range.file_id {
147+
None
148+
} else {
149+
Some(make::visibility_pub_crate())
150+
};
151+
let field_name = make::name(field_name);
152+
153+
let (offset, record_field) = record_field_layout(
154+
visibility,
155+
field_name,
156+
suggested_type,
157+
field_list,
158+
struct_syntax.value,
159+
)?;
160+
161+
let mut src_change_builder = SourceChangeBuilder::new(struct_range.file_id);
162+
163+
// FIXME: Allow for choosing a visibility modifier see https://github.com/rust-lang/rust-analyzer/issues/11563
164+
src_change_builder.insert(offset, record_field);
165+
Some(vec![Assist {
166+
id: AssistId("add-field-to-record-struct", AssistKind::QuickFix),
167+
label: Label::new("Add field to Record Struct".to_owned()),
168+
group: None,
169+
target: error_range.range,
170+
source_change: Some(src_change_builder.finish()),
171+
trigger_signature_help: false,
172+
}])
173+
}
174+
None => {
175+
// Add a field list to the Unit Struct
176+
let mut src_change_builder = SourceChangeBuilder::new(struct_range.file_id);
177+
let field_name = make::name(field_name);
178+
let visibility = if error_range.file_id == struct_range.file_id {
179+
None
180+
} else {
181+
Some(make::visibility_pub_crate())
182+
};
183+
// FIXME: Allow for choosing a visibility modifier see https://github.com/rust-lang/rust-analyzer/issues/11563
184+
let indent = IndentLevel::from_node(struct_syntax.value) + 1;
185+
186+
let field = make::record_field(visibility, field_name, suggested_type).indent(indent);
187+
let record_field_list = make::record_field_list(iter::once(field));
188+
// A Unit Struct with no `;` is invalid syntax. We should not suggest this fix.
189+
let semi_colon =
190+
algo::skip_trivia_token(struct_syntax.value.last_token()?, Direction::Prev)?;
191+
if semi_colon.kind() != SyntaxKind::SEMICOLON {
192+
return None;
193+
}
194+
src_change_builder.replace(semi_colon.text_range(), record_field_list.to_string());
195+
196+
Some(vec![Assist {
197+
id: AssistId("convert-unit-struct-to-record-struct", AssistKind::QuickFix),
198+
label: Label::new("Convert Unit Struct to Record Struct and add field".to_owned()),
199+
group: None,
200+
target: error_range.range,
201+
source_change: Some(src_change_builder.finish()),
202+
trigger_signature_help: false,
203+
}])
204+
}
205+
Some(FieldList::TupleFieldList(_tuple)) => {
206+
// FIXME: Add support for Tuple Structs. Tuple Structs are not sent to this diagnostic
207+
None
208+
}
209+
}
210+
}
211+
/// Used to determine the layout of the record field in the struct.
212+
fn record_field_layout(
213+
visibility: Option<Visibility>,
214+
name: Name,
215+
suggested_type: Type,
216+
field_list: ast::RecordFieldList,
217+
struct_syntax: &SyntaxNode,
218+
) -> Option<(TextSize, String)> {
219+
let (offset, needs_comma, trailing_new_line, indent) = match field_list.fields().last() {
220+
Some(record_field) => {
221+
let syntax = algo::skip_trivia_token(field_list.r_curly_token()?, Direction::Prev)?;
222+
223+
let last_field_syntax = record_field.syntax();
224+
let last_field_indent = IndentLevel::from_node(last_field_syntax);
225+
(
226+
last_field_syntax.text_range().end(),
227+
syntax.kind() != SyntaxKind::COMMA,
228+
false,
229+
last_field_indent,
230+
)
231+
}
232+
// Empty Struct. Add a field right before the closing brace
233+
None => {
234+
let indent = IndentLevel::from_node(struct_syntax) + 1;
235+
let offset = field_list.r_curly_token()?.text_range().start();
236+
(offset, false, true, indent)
237+
}
238+
};
239+
let comma = if needs_comma { ",\n" } else { "" };
240+
let trailing_new_line = if trailing_new_line { "\n" } else { "" };
241+
let record_field = make::record_field(visibility, name, suggested_type);
242+
243+
Some((offset, format!("{comma}{indent}{record_field}{trailing_new_line}")))
244+
}
58245
// FIXME: We should fill out the call here, move the cursor and trigger signature help
59246
fn method_fix(
60247
ctx: &DiagnosticsContext<'_>,
@@ -77,9 +264,11 @@ fn method_fix(
77264
}
78265
#[cfg(test)]
79266
mod tests {
267+
80268
use crate::{
81269
tests::{
82270
check_diagnostics, check_diagnostics_with_config, check_diagnostics_with_disabled,
271+
check_fix,
83272
},
84273
DiagnosticsConfig,
85274
};
@@ -168,4 +357,100 @@ fn foo() {
168357
config.disabled.insert("syntax-error".to_owned());
169358
check_diagnostics_with_config(config, "fn foo() { (). }");
170359
}
360+
361+
#[test]
362+
fn unresolved_field_fix_on_unit() {
363+
check_fix(
364+
r#"
365+
struct Foo;
366+
367+
fn foo() {
368+
Foo.bar$0;
369+
}
370+
"#,
371+
r#"
372+
struct Foo{ bar: () }
373+
374+
fn foo() {
375+
Foo.bar;
376+
}
377+
"#,
378+
);
379+
}
380+
#[test]
381+
fn unresolved_field_fix_on_empty() {
382+
check_fix(
383+
r#"
384+
struct Foo{
385+
}
386+
387+
fn foo() {
388+
let foo = Foo{};
389+
foo.bar$0;
390+
}
391+
"#,
392+
r#"
393+
struct Foo{
394+
bar: ()
395+
}
396+
397+
fn foo() {
398+
let foo = Foo{};
399+
foo.bar;
400+
}
401+
"#,
402+
);
403+
}
404+
#[test]
405+
fn unresolved_field_fix_on_struct() {
406+
check_fix(
407+
r#"
408+
struct Foo{
409+
a: i32
410+
}
411+
412+
fn foo() {
413+
let foo = Foo{a: 0};
414+
foo.bar$0;
415+
}
416+
"#,
417+
r#"
418+
struct Foo{
419+
a: i32,
420+
bar: ()
421+
}
422+
423+
fn foo() {
424+
let foo = Foo{a: 0};
425+
foo.bar;
426+
}
427+
"#,
428+
);
429+
}
430+
#[test]
431+
fn unresolved_field_fix_on_union() {
432+
check_fix(
433+
r#"
434+
union Foo{
435+
a: i32
436+
}
437+
438+
fn foo() {
439+
let foo = Foo{a: 0};
440+
foo.bar$0;
441+
}
442+
"#,
443+
r#"
444+
union Foo{
445+
a: i32,
446+
bar: ()
447+
}
448+
449+
fn foo() {
450+
let foo = Foo{a: 0};
451+
foo.bar;
452+
}
453+
"#,
454+
);
455+
}
171456
}

0 commit comments

Comments
 (0)