Skip to content

Commit f5ac313

Browse files
committed
Add quickfix to add a struct field
1 parent b56ad14 commit f5ac313

File tree

5 files changed

+134
-5
lines changed

5 files changed

+134
-5
lines changed

crates/ra_hir/src/semantics.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ use std::{cell::RefCell, fmt, iter::successors};
66

77
use hir_def::{
88
resolver::{self, HasResolver, Resolver},
9-
AsMacroCall, TraitId,
9+
AsMacroCall, TraitId, VariantId,
1010
};
11-
use hir_expand::{hygiene::Hygiene, ExpansionInfo};
11+
use hir_expand::{diagnostics::AstDiagnostic, hygiene::Hygiene, ExpansionInfo};
1212
use hir_ty::associated_type_shorthand_candidates;
1313
use itertools::Itertools;
1414
use ra_db::{FileId, FileRange};
@@ -104,6 +104,13 @@ impl<'db, DB: HirDatabase> Semantics<'db, DB> {
104104
tree
105105
}
106106

107+
pub fn ast<T: AstDiagnostic + Diagnostic>(&self, d: &T) -> <T as AstDiagnostic>::AST {
108+
let file_id = d.source().file_id;
109+
let root = self.db.parse_or_expand(file_id).unwrap();
110+
self.cache(root, file_id);
111+
d.ast(self.db)
112+
}
113+
107114
pub fn expand(&self, macro_call: &ast::MacroCall) -> Option<SyntaxNode> {
108115
let macro_call = self.find_file(macro_call.syntax().clone()).with_value(macro_call);
109116
let sa = self.analyze2(macro_call.map(|it| it.syntax()), None);
@@ -247,6 +254,10 @@ impl<'db, DB: HirDatabase> Semantics<'db, DB> {
247254
self.analyze(path.syntax()).resolve_path(self.db, path)
248255
}
249256

257+
pub fn resolve_variant(&self, record_lit: ast::RecordLit) -> Option<VariantId> {
258+
self.analyze(record_lit.syntax()).resolve_variant(self.db, record_lit)
259+
}
260+
250261
pub fn lower_path(&self, path: &ast::Path) -> Option<Path> {
251262
let src = self.find_file(path.syntax().clone());
252263
Path::from_src(path.clone(), &Hygiene::new(self.db.upcast(), src.file_id.into()))

crates/ra_hir/src/source_analyzer.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,16 @@ impl SourceAnalyzer {
313313
})?;
314314
Some(macro_call_id.as_file())
315315
}
316+
317+
pub(crate) fn resolve_variant(
318+
&self,
319+
db: &dyn HirDatabase,
320+
record_lit: ast::RecordLit,
321+
) -> Option<VariantId> {
322+
let infer = self.infer.as_ref()?;
323+
let expr_id = self.expr_id(db, &record_lit.into())?;
324+
infer.variant_resolution_for_expr(expr_id)
325+
}
316326
}
317327

318328
fn scope_for(

crates/ra_hir_ty/src/diagnostics.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use hir_expand::{db::AstDatabase, name::Name, HirFileId, InFile};
66
use ra_syntax::{ast, AstNode, AstPtr, SyntaxNodePtr};
77
use stdx::format_to;
88

9-
pub use hir_def::{diagnostics::UnresolvedModule, expr::MatchArm};
9+
pub use hir_def::{diagnostics::UnresolvedModule, expr::MatchArm, path::Path};
1010
pub use hir_expand::diagnostics::{AstDiagnostic, Diagnostic, DiagnosticSink};
1111

1212
#[derive(Debug)]
@@ -29,6 +29,16 @@ impl Diagnostic for NoSuchField {
2929
}
3030
}
3131

32+
impl AstDiagnostic for NoSuchField {
33+
type AST = ast::RecordField;
34+
35+
fn ast(&self, db: &impl AstDatabase) -> Self::AST {
36+
let root = db.parse_or_expand(self.source().file_id).unwrap();
37+
let node = self.source().value.to_node(&root);
38+
ast::RecordField::cast(node).unwrap()
39+
}
40+
}
41+
3242
#[derive(Debug)]
3343
pub struct MissingFields {
3444
pub file: HirFileId,

crates/ra_ide/src/diagnostics.rs

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ use std::cell::RefCell;
88

99
use hir::{
1010
diagnostics::{AstDiagnostic, Diagnostic as _, DiagnosticSink},
11-
Semantics,
11+
HasSource, HirDisplay, Semantics, VariantDef,
1212
};
1313
use itertools::Itertools;
1414
use ra_db::{RelativePath, SourceDatabase, SourceDatabaseExt};
1515
use ra_ide_db::RootDatabase;
1616
use ra_prof::profile;
1717
use ra_syntax::{
1818
algo,
19-
ast::{self, make, AstNode},
19+
ast::{self, edit::IndentLevel, make, AstNode},
2020
SyntaxNode, TextRange, T,
2121
};
2222
use ra_text_edit::{TextEdit, TextEditBuilder};
@@ -123,14 +123,85 @@ pub(crate) fn diagnostics(db: &RootDatabase, file_id: FileId) -> Vec<Diagnostic>
123123
severity: Severity::Error,
124124
fix: Some(fix),
125125
})
126+
})
127+
.on::<hir::diagnostics::NoSuchField, _>(|d| {
128+
res.borrow_mut().push(Diagnostic {
129+
range: sema.diagnostics_range(d).range,
130+
message: d.message(),
131+
severity: Severity::Error,
132+
fix: missing_struct_field_fix(&sema, file_id, d),
133+
})
126134
});
135+
127136
if let Some(m) = sema.to_module_def(file_id) {
128137
m.diagnostics(db, &mut sink);
129138
};
130139
drop(sink);
131140
res.into_inner()
132141
}
133142

143+
fn missing_struct_field_fix(
144+
sema: &Semantics<RootDatabase>,
145+
file_id: FileId,
146+
d: &hir::diagnostics::NoSuchField,
147+
) -> Option<Fix> {
148+
let record_expr = sema.ast(d);
149+
150+
let record_lit = ast::RecordLit::cast(record_expr.syntax().parent()?.parent()?)?;
151+
let def_id = sema.resolve_variant(record_lit)?;
152+
let module;
153+
let record_fields = match VariantDef::from(def_id) {
154+
VariantDef::Struct(s) => {
155+
module = s.module(sema.db);
156+
let source = s.source(sema.db);
157+
let fields = source.value.field_def_list()?;
158+
record_field_def_list(fields)?
159+
}
160+
VariantDef::Union(u) => {
161+
module = u.module(sema.db);
162+
let source = u.source(sema.db);
163+
source.value.record_field_def_list()?
164+
}
165+
VariantDef::EnumVariant(e) => {
166+
module = e.module(sema.db);
167+
let source = e.source(sema.db);
168+
let fields = source.value.field_def_list()?;
169+
record_field_def_list(fields)?
170+
}
171+
};
172+
173+
let new_field_type = sema.type_of_expr(&record_expr.expr()?)?;
174+
let new_field = make::record_field_def(
175+
record_expr.field_name()?,
176+
make::type_ref(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
177+
);
178+
179+
let last_field = record_fields.fields().last()?;
180+
let last_field_syntax = last_field.syntax();
181+
let indent = IndentLevel::from_node(last_field_syntax);
182+
183+
let mut new_field = format!("\n{}{}", indent, new_field);
184+
185+
let needs_comma = !last_field_syntax.to_string().ends_with(",");
186+
if needs_comma {
187+
new_field = format!(",{}", new_field);
188+
}
189+
190+
let source_change = SourceFileEdit {
191+
file_id,
192+
edit: TextEdit::insert(last_field_syntax.text_range().end(), new_field),
193+
};
194+
let fix = Fix::new("Create field", source_change.into());
195+
return Some(fix);
196+
197+
fn record_field_def_list(field_def_list: ast::FieldDefList) -> Option<ast::RecordFieldDefList> {
198+
match field_def_list {
199+
ast::FieldDefList::RecordFieldDefList(it) => Some(it),
200+
ast::FieldDefList::TupleFieldDefList(_) => None,
201+
}
202+
}
203+
}
204+
134205
fn check_unnecessary_braces_in_use_statement(
135206
acc: &mut Vec<Diagnostic>,
136207
file_id: FileId,
@@ -795,4 +866,27 @@ fn main() {
795866
check_struct_shorthand_initialization,
796867
);
797868
}
869+
870+
#[test]
871+
fn test_add_field_from_usage() {
872+
check_apply_diagnostic_fix(
873+
r"
874+
fn main() {
875+
Foo { bar: 3, baz: false};
876+
}
877+
struct Foo {
878+
bar: i32
879+
}
880+
",
881+
r"
882+
fn main() {
883+
Foo { bar: 3, baz: false};
884+
}
885+
struct Foo {
886+
bar: i32,
887+
baz: bool
888+
}
889+
",
890+
)
891+
}
798892
}

crates/ra_syntax/src/ast/make.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ pub fn record_field(name: ast::NameRef, expr: Option<ast::Expr>) -> ast::RecordF
7575
}
7676
}
7777

78+
pub fn record_field_def(name: ast::NameRef, ty: ast::TypeRef) -> ast::RecordFieldDef {
79+
ast_from_text(&format!("struct S {{ {}: {}, }}", name, ty))
80+
}
81+
7882
pub fn block_expr(
7983
stmts: impl IntoIterator<Item = ast::Stmt>,
8084
tail_expr: Option<ast::Expr>,

0 commit comments

Comments
 (0)