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

Commit 255ba69

Browse files
committed
Added tests, added Union Support, and code cleanup
1 parent 6027eae commit 255ba69

File tree

1 file changed

+252
-44
lines changed

1 file changed

+252
-44
lines changed

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

Lines changed: 252 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use hir::{db::ExpandDatabase, Adt, HasSource, 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,8 +9,13 @@ use ide_db::{
79
source_change::{SourceChange, SourceChangeBuilder},
810
};
911
use syntax::{
10-
ast::{self, edit::IndentLevel, make},
11-
AstNode, AstPtr, SyntaxKind,
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,
1219
};
1320
use text_edit::TextEdit;
1421

@@ -52,23 +59,19 @@ pub(crate) fn unresolved_field(
5259
fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField) -> Option<Vec<Assist>> {
5360
let mut fixes = if d.method_with_same_name_exists { method_fix(ctx, &d.expr) } else { None };
5461
if let Some(fix) = add_field_fix(ctx, d) {
55-
fixes.get_or_insert_with(Vec::new).push(fix);
62+
fixes.get_or_insert_with(Vec::new).extend(fix);
5663
}
5764
fixes
5865
}
5966

60-
fn add_field_fix(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField) -> Option<Assist> {
67+
fn add_field_fix(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField) -> Option<Vec<Assist>> {
6168
// Get the FileRange of the invalid field access
6269
let root = ctx.sema.db.parse_or_expand(d.expr.file_id);
6370
let expr = d.expr.value.to_node(&root);
6471

6572
let error_range = ctx.sema.original_range_opt(expr.syntax())?;
6673
// Convert the receiver to an ADT
6774
let adt = d.receiver.as_adt()?;
68-
let Adt::Struct(adt) = adt else {
69-
return None;
70-
};
71-
7275
let target_module = adt.module(ctx.sema.db);
7376

7477
let suggested_type =
@@ -83,54 +86,161 @@ fn add_field_fix(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField) -> Opti
8386
if !is_editable_crate(target_module.krate(), ctx.sema.db) {
8487
return None;
8588
}
86-
let adt_source = adt.source(ctx.sema.db)?;
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_varient_to_union(ctx, adt_union, field_name, suggested_type, error_range)
99+
}
100+
_ => None,
101+
}
102+
}
103+
fn add_varient_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)?;
87111
let adt_syntax = adt_source.syntax();
112+
let Some(field_list) = adt_source.value.record_field_list() else {
113+
return None;
114+
};
88115
let range = adt_syntax.original_file_range(ctx.sema.db);
116+
let field_name = make::name(field_name);
89117

90-
// Get range of final field in the struct
91-
let (offset, needs_comma, indent) = match adt.fields(ctx.sema.db).last() {
92-
Some(field) => {
93-
let last_field = field.source(ctx.sema.db)?.value;
94-
let hir::FieldSource::Named(record_field) = last_field else {
95-
return None;
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-varient-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())
96150
};
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+
97223
let last_field_syntax = record_field.syntax();
98-
let last_field_imdent = IndentLevel::from_node(last_field_syntax);
224+
let last_field_indent = IndentLevel::from_node(last_field_syntax);
99225
(
100226
last_field_syntax.text_range().end(),
101-
!last_field_syntax.to_string().ends_with(','),
102-
last_field_imdent,
227+
syntax.kind() != SyntaxKind::COMMA,
228+
false,
229+
last_field_indent,
103230
)
104231
}
232+
// Empty Struct. Add a field right before the closing brace
105233
None => {
106-
// Empty Struct. Add a field right before the closing brace
107-
let indent = IndentLevel::from_node(adt_syntax.value) + 1;
108-
let record_field_list =
109-
adt_syntax.value.children().find(|v| v.kind() == SyntaxKind::RECORD_FIELD_LIST)?;
110-
let offset = record_field_list.first_token().map(|f| f.text_range().end())?;
111-
(offset, false, indent)
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)
112237
}
113238
};
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);
114242

115-
let field_name = make::name(d.name.as_str()?);
116-
117-
// If the Type is in the same file. We don't need to add a visibility modifier. Otherwise make it pub(crate)
118-
let visibility = if error_range.file_id == range.file_id { "" } else { "pub(crate)" };
119-
let mut src_change_builder = SourceChangeBuilder::new(range.file_id);
120-
let comma = if needs_comma { "," } else { "" };
121-
src_change_builder
122-
.insert(offset, format!("{comma}\n{indent}{visibility}{field_name}: {suggested_type}\n"));
123-
124-
// FIXME: Add a Snippet for the new field type
125-
let source_change = src_change_builder.finish();
126-
Some(Assist {
127-
id: AssistId("add-field-to-type", AssistKind::QuickFix),
128-
label: Label::new("Add field to type".to_owned()),
129-
group: None,
130-
target: error_range.range,
131-
source_change: Some(source_change),
132-
trigger_signature_help: false,
133-
})
243+
Some((offset, format!("{comma}{indent}{record_field}{trailing_new_line}")))
134244
}
135245
// FIXME: We should fill out the call here, move the cursor and trigger signature help
136246
fn method_fix(
@@ -154,9 +264,11 @@ fn method_fix(
154264
}
155265
#[cfg(test)]
156266
mod tests {
267+
157268
use crate::{
158269
tests::{
159270
check_diagnostics, check_diagnostics_with_config, check_diagnostics_with_disabled,
271+
check_fix,
160272
},
161273
DiagnosticsConfig,
162274
};
@@ -245,4 +357,100 @@ fn foo() {
245357
config.disabled.insert("syntax-error".to_owned());
246358
check_diagnostics_with_config(config, "fn foo() { (). }");
247359
}
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+
}
248456
}

0 commit comments

Comments
 (0)