Skip to content

Commit 65eaabc

Browse files
bors[bot]Veykril
andauthored
Merge #11887
11887: fix: Add missing fields diagnostic fix for patterns r=Veykril a=Veykril bors r+ Co-authored-by: Lukas Wirth <[email protected]>
2 parents 63d2df1 + dd3f766 commit 65eaabc

File tree

4 files changed

+184
-62
lines changed

4 files changed

+184
-62
lines changed

crates/ide_diagnostics/src/handlers/missing_fields.rs

Lines changed: 135 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use stdx::format_to;
99
use syntax::{
1010
algo,
1111
ast::{self, make},
12-
AstNode, SyntaxNodePtr,
12+
AstNode, SyntaxNode, SyntaxNodePtr,
1313
};
1414
use text_edit::TextEdit;
1515

@@ -55,70 +55,95 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Option<Vec<Ass
5555
}
5656

5757
let root = ctx.sema.db.parse_or_expand(d.file)?;
58-
let field_list_parent = match &d.field_list_parent {
59-
Either::Left(record_expr) => record_expr.to_node(&root),
60-
// FIXE: patterns should be fixable as well.
61-
Either::Right(_) => return None,
62-
};
63-
let old_field_list = field_list_parent.record_expr_field_list()?;
6458

65-
let new_field_list = old_field_list.clone_for_update();
66-
let mut locals = FxHashMap::default();
67-
ctx.sema.scope(field_list_parent.syntax())?.process_all_names(&mut |name, def| {
68-
if let hir::ScopeDef::Local(local) = def {
69-
locals.insert(name, local);
70-
}
71-
});
72-
let missing_fields = ctx.sema.record_literal_missing_fields(&field_list_parent);
73-
74-
let generate_fill_expr = |ty: &Type| match ctx.config.expr_fill_default {
75-
crate::ExprFillDefaultMode::Todo => Some(make::ext::expr_todo()),
76-
crate::ExprFillDefaultMode::Default => {
77-
let default_constr = get_default_constructor(ctx, d, ty);
78-
match default_constr {
79-
Some(default_constr) => Some(default_constr),
80-
_ => Some(make::ext::expr_todo()),
59+
let build_text_edit = |parent_syntax, new_syntax: &SyntaxNode, old_syntax| {
60+
let edit = {
61+
let mut builder = TextEdit::builder();
62+
if d.file.is_macro() {
63+
// we can't map the diff up into the macro input unfortunately, as the macro loses all
64+
// whitespace information so the diff wouldn't be applicable no matter what
65+
// This has the downside that the cursor will be moved in macros by doing it without a diff
66+
// but that is a trade off we can make.
67+
// FIXME: this also currently discards a lot of whitespace in the input... we really need a formatter here
68+
let range = ctx.sema.original_range_opt(old_syntax)?;
69+
builder.replace(range.range, new_syntax.to_string());
70+
} else {
71+
algo::diff(old_syntax, new_syntax).into_text_edit(&mut builder);
8172
}
82-
}
73+
builder.finish()
74+
};
75+
Some(vec![fix(
76+
"fill_missing_fields",
77+
"Fill struct fields",
78+
SourceChange::from_text_edit(d.file.original_file(ctx.sema.db), edit),
79+
ctx.sema.original_range(parent_syntax).range,
80+
)])
8381
};
8482

85-
for (f, ty) in missing_fields.iter() {
86-
let field_expr = if let Some(local_candidate) = locals.get(&f.name(ctx.sema.db)) {
87-
cov_mark::hit!(field_shorthand);
88-
let candidate_ty = local_candidate.ty(ctx.sema.db);
89-
if ty.could_unify_with(ctx.sema.db, &candidate_ty) {
90-
None
91-
} else {
92-
generate_fill_expr(ty)
83+
match &d.field_list_parent {
84+
Either::Left(record_expr) => {
85+
let field_list_parent = record_expr.to_node(&root);
86+
let missing_fields = ctx.sema.record_literal_missing_fields(&field_list_parent);
87+
88+
let mut locals = FxHashMap::default();
89+
ctx.sema.scope(field_list_parent.syntax())?.process_all_names(&mut |name, def| {
90+
if let hir::ScopeDef::Local(local) = def {
91+
locals.insert(name, local);
92+
}
93+
});
94+
95+
let generate_fill_expr = |ty: &Type| match ctx.config.expr_fill_default {
96+
crate::ExprFillDefaultMode::Todo => make::ext::expr_todo(),
97+
crate::ExprFillDefaultMode::Default => {
98+
get_default_constructor(ctx, d, ty).unwrap_or_else(|| make::ext::expr_todo())
99+
}
100+
};
101+
102+
let old_field_list = field_list_parent.record_expr_field_list()?;
103+
let new_field_list = old_field_list.clone_for_update();
104+
for (f, ty) in missing_fields.iter() {
105+
let field_expr = if let Some(local_candidate) = locals.get(&f.name(ctx.sema.db)) {
106+
cov_mark::hit!(field_shorthand);
107+
let candidate_ty = local_candidate.ty(ctx.sema.db);
108+
if ty.could_unify_with(ctx.sema.db, &candidate_ty) {
109+
None
110+
} else {
111+
Some(generate_fill_expr(ty))
112+
}
113+
} else {
114+
Some(generate_fill_expr(ty))
115+
};
116+
let field = make::record_expr_field(
117+
make::name_ref(&f.name(ctx.sema.db).to_smol_str()),
118+
field_expr,
119+
);
120+
new_field_list.add_field(field.clone_for_update());
93121
}
94-
} else {
95-
generate_fill_expr(ty)
96-
};
97-
let field =
98-
make::record_expr_field(make::name_ref(&f.name(ctx.sema.db).to_smol_str()), field_expr)
99-
.clone_for_update();
100-
new_field_list.add_field(field);
101-
}
102-
103-
let mut builder = TextEdit::builder();
104-
if d.file.is_macro() {
105-
// we can't map the diff up into the macro input unfortunately, as the macro loses all
106-
// whitespace information so the diff wouldn't be applicable no matter what
107-
// This has the downside that the cursor will be moved in macros by doing it without a diff
108-
// but that is a trade off we can make.
109-
// FIXE: this also currently discards a lot of whitespace in the input... we really need a formatter here
110-
let range = ctx.sema.original_range_opt(old_field_list.syntax())?;
111-
builder.replace(range.range, new_field_list.to_string());
112-
} else {
113-
algo::diff(old_field_list.syntax(), new_field_list.syntax()).into_text_edit(&mut builder);
122+
build_text_edit(
123+
field_list_parent.syntax(),
124+
new_field_list.syntax(),
125+
old_field_list.syntax(),
126+
)
127+
}
128+
Either::Right(record_pat) => {
129+
let field_list_parent = record_pat.to_node(&root);
130+
let missing_fields = ctx.sema.record_pattern_missing_fields(&field_list_parent);
131+
132+
let old_field_list = field_list_parent.record_pat_field_list()?;
133+
let new_field_list = old_field_list.clone_for_update();
134+
for (f, _) in missing_fields.iter() {
135+
let field = make::record_pat_field_shorthand(make::name_ref(
136+
&f.name(ctx.sema.db).to_smol_str(),
137+
));
138+
new_field_list.add_field(field.clone_for_update());
139+
}
140+
build_text_edit(
141+
field_list_parent.syntax(),
142+
new_field_list.syntax(),
143+
old_field_list.syntax(),
144+
)
145+
}
114146
}
115-
let edit = builder.finish();
116-
Some(vec![fix(
117-
"fill_missing_fields",
118-
"Fill struct fields",
119-
SourceChange::from_text_edit(d.file.original_file(ctx.sema.db), edit),
120-
ctx.sema.original_range(field_list_parent.syntax()).range,
121-
)])
122147
}
123148

124149
fn make_ty(ty: &hir::Type, db: &dyn HirDatabase, module: hir::Module) -> ast::Type {
@@ -190,7 +215,7 @@ mod tests {
190215
struct S { foo: i32, bar: () }
191216
fn baz(s: S) {
192217
let S { foo: _ } = s;
193-
//^ error: missing structure fields:
218+
//^ 💡 error: missing structure fields:
194219
//| - bar
195220
}
196221
"#,
@@ -581,6 +606,56 @@ fn f() {
581606
);
582607
}
583608

609+
#[test]
610+
fn test_fill_struct_pat_fields() {
611+
check_fix(
612+
r#"
613+
struct S { a: &'static str, b: i32 }
614+
615+
fn f() {
616+
let S {
617+
$0
618+
};
619+
}
620+
"#,
621+
r#"
622+
struct S { a: &'static str, b: i32 }
623+
624+
fn f() {
625+
let S {
626+
a,
627+
b,
628+
};
629+
}
630+
"#,
631+
);
632+
}
633+
634+
#[test]
635+
fn test_fill_struct_pat_fields_partial() {
636+
check_fix(
637+
r#"
638+
struct S { a: &'static str, b: i32 }
639+
640+
fn f() {
641+
let S {
642+
a,$0
643+
};
644+
}
645+
"#,
646+
r#"
647+
struct S { a: &'static str, b: i32 }
648+
649+
fn f() {
650+
let S {
651+
a,
652+
b,
653+
};
654+
}
655+
"#,
656+
);
657+
}
658+
584659
#[test]
585660
fn import_extern_crate_clash_with_inner_item() {
586661
// This is more of a resolver test, but doesn't really work with the hir_def testsuite.

crates/ide_diagnostics/src/handlers/missing_match_arms.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,14 +396,14 @@ fn main() {
396396
//^ error: missing match arm
397397
match a {
398398
Either::A { } => (),
399-
//^^^^^^^^^ error: missing structure fields:
399+
//^^^^^^^^^ 💡 error: missing structure fields:
400400
// | - foo
401401
Either::B => (),
402402
}
403403
match a {
404404
//^ error: missing match arm
405405
Either::A { } => (),
406-
} //^^^^^^^^^ error: missing structure fields:
406+
} //^^^^^^^^^ 💡 error: missing structure fields:
407407
// | - foo
408408
409409
match a {

crates/syntax/src/ast/edit_in_place.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,49 @@ impl ast::RecordExprField {
563563
}
564564
}
565565

566+
impl ast::RecordPatFieldList {
567+
pub fn add_field(&self, field: ast::RecordPatField) {
568+
let is_multiline = self.syntax().text().contains_char('\n');
569+
let whitespace = if is_multiline {
570+
let indent = IndentLevel::from_node(self.syntax()) + 1;
571+
make::tokens::whitespace(&format!("\n{}", indent))
572+
} else {
573+
make::tokens::single_space()
574+
};
575+
576+
if is_multiline {
577+
normalize_ws_between_braces(self.syntax());
578+
}
579+
580+
let position = match self.fields().last() {
581+
Some(last_field) => {
582+
let comma = match last_field
583+
.syntax()
584+
.siblings_with_tokens(Direction::Next)
585+
.filter_map(|it| it.into_token())
586+
.find(|it| it.kind() == T![,])
587+
{
588+
Some(it) => it,
589+
None => {
590+
let comma = ast::make::token(T![,]);
591+
ted::insert(Position::after(last_field.syntax()), &comma);
592+
comma
593+
}
594+
};
595+
Position::after(comma)
596+
}
597+
None => match self.l_curly_token() {
598+
Some(it) => Position::after(it),
599+
None => Position::last_child_of(self.syntax()),
600+
},
601+
};
602+
603+
ted::insert_all(position, vec![whitespace.into(), field.syntax().clone().into()]);
604+
if is_multiline {
605+
ted::insert(Position::after(field.syntax()), ast::make::token(T![,]));
606+
}
607+
}
608+
}
566609
impl ast::StmtList {
567610
pub fn push_front(&self, statement: ast::Stmt) {
568611
ted::insert(Position::after(self.l_curly_token().unwrap()), statement.syntax());

crates/syntax/src/ast/make.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,10 @@ pub fn record_pat_field(name_ref: ast::NameRef, pat: ast::Pat) -> ast::RecordPat
555555
ast_from_text(&format!("fn f(S {{ {}: {} }}: ()))", name_ref, pat))
556556
}
557557

558+
pub fn record_pat_field_shorthand(name_ref: ast::NameRef) -> ast::RecordPatField {
559+
ast_from_text(&format!("fn f(S {{ {} }}: ()))", name_ref))
560+
}
561+
558562
/// Returns a `BindPat` if the path has just one segment, a `PathPat` otherwise.
559563
pub fn path_pat(path: ast::Path) -> ast::Pat {
560564
return from_text(&path.to_string());

0 commit comments

Comments
 (0)