blob: a34a5824f29615d46777f79f2f16927b3559dd01 [file] [log] [blame]
use hir::{db::ExpandDatabase, HasSource, HirDisplay, Semantics};
use ide_db::{base_db::FileId, source_change::SourceChange, RootDatabase};
use syntax::{
ast::{self, edit::IndentLevel, make},
AstNode,
};
use text_edit::TextEdit;
use crate::{fix, Assist, Diagnostic, DiagnosticCode, DiagnosticsContext};
// Diagnostic: no-such-field
//
// This diagnostic is triggered if created structure does not have field provided in record.
pub(crate) fn no_such_field(ctx: &DiagnosticsContext<'_>, d: &hir::NoSuchField) -> Diagnostic {
Diagnostic::new_with_syntax_node_ptr(
ctx,
DiagnosticCode::RustcHardError("E0559"),
"no such field",
d.field.clone().map(|it| it.into()),
)
.with_fixes(fixes(ctx, d))
}
fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::NoSuchField) -> Option<Vec<Assist>> {
let root = ctx.sema.db.parse_or_expand(d.field.file_id);
missing_record_expr_field_fixes(
&ctx.sema,
d.field.file_id.original_file(ctx.sema.db),
&d.field.value.to_node(&root),
)
}
fn missing_record_expr_field_fixes(
sema: &Semantics<'_, RootDatabase>,
usage_file_id: FileId,
record_expr_field: &ast::RecordExprField,
) -> Option<Vec<Assist>> {
let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?;
let def_id = sema.resolve_variant(record_lit)?;
let module;
let def_file_id;
let record_fields = match def_id {
hir::VariantDef::Struct(s) => {
module = s.module(sema.db);
let source = s.source(sema.db)?;
def_file_id = source.file_id;
let fields = source.value.field_list()?;
record_field_list(fields)?
}
hir::VariantDef::Union(u) => {
module = u.module(sema.db);
let source = u.source(sema.db)?;
def_file_id = source.file_id;
source.value.record_field_list()?
}
hir::VariantDef::Variant(e) => {
module = e.module(sema.db);
let source = e.source(sema.db)?;
def_file_id = source.file_id;
let fields = source.value.field_list()?;
record_field_list(fields)?
}
};
let def_file_id = def_file_id.original_file(sema.db);
let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?.adjusted();
if new_field_type.is_unknown() {
return None;
}
let new_field = make::record_field(
None,
make::name(record_expr_field.field_name()?.ident_token()?.text()),
make::ty(&new_field_type.display_source_code(sema.db, module.into(), true).ok()?),
);
let last_field = record_fields.fields().last()?;
let last_field_syntax = last_field.syntax();
let indent = IndentLevel::from_node(last_field_syntax);
let mut new_field = new_field.to_string();
if usage_file_id != def_file_id {
new_field = format!("pub(crate) {new_field}");
}
new_field = format!("\n{indent}{new_field}");
let needs_comma = !last_field_syntax.to_string().ends_with(',');
if needs_comma {
new_field = format!(",{new_field}");
}
let source_change = SourceChange::from_text_edit(
def_file_id,
TextEdit::insert(last_field_syntax.text_range().end(), new_field),
);
return Some(vec![fix(
"create_field",
"Create field",
source_change,
record_expr_field.syntax().text_range(),
)]);
fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
match field_def_list {
ast::FieldList::RecordFieldList(it) => Some(it),
ast::FieldList::TupleFieldList(_) => None,
}
}
}
#[cfg(test)]
mod tests {
use crate::tests::{check_diagnostics, check_fix, check_no_fix};
#[test]
fn no_such_field_diagnostics() {
check_diagnostics(
r#"
struct S { foo: i32, bar: () }
impl S {
fn new() -> S {
S {
//^ 💡 error: missing structure fields:
//| - bar
foo: 92,
baz: 62,
//^^^^^^^ 💡 error: no such field
}
}
}
"#,
);
}
#[test]
fn no_such_field_with_feature_flag_diagnostics() {
check_diagnostics(
r#"
//- /lib.rs crate:foo cfg:feature=foo
struct MyStruct {
my_val: usize,
#[cfg(feature = "foo")]
bar: bool,
}
impl MyStruct {
#[cfg(feature = "foo")]
pub(crate) fn new(my_val: usize, bar: bool) -> Self {
Self { my_val, bar }
}
#[cfg(not(feature = "foo"))]
pub(crate) fn new(my_val: usize, _bar: bool) -> Self {
Self { my_val }
}
}
"#,
);
}
#[test]
fn no_such_field_enum_with_feature_flag_diagnostics() {
check_diagnostics(
r#"
//- /lib.rs crate:foo cfg:feature=foo
enum Foo {
#[cfg(not(feature = "foo"))]
Buz,
#[cfg(feature = "foo")]
Bar,
Baz
}
fn test_fn(f: Foo) {
match f {
Foo::Bar => {},
Foo::Baz => {},
}
}
"#,
);
}
#[test]
fn no_such_field_with_feature_flag_diagnostics_on_struct_lit() {
check_diagnostics(
r#"
//- /lib.rs crate:foo cfg:feature=foo
struct S {
#[cfg(feature = "foo")]
foo: u32,
#[cfg(not(feature = "foo"))]
bar: u32,
}
impl S {
#[cfg(feature = "foo")]
fn new(foo: u32) -> Self {
Self { foo }
}
#[cfg(not(feature = "foo"))]
fn new(bar: u32) -> Self {
Self { bar }
}
fn new2(bar: u32) -> Self {
#[cfg(feature = "foo")]
{ Self { foo: bar } }
#[cfg(not(feature = "foo"))]
{ Self { bar } }
}
fn new2(val: u32) -> Self {
Self {
#[cfg(feature = "foo")]
foo: val,
#[cfg(not(feature = "foo"))]
bar: val,
}
}
}
"#,
);
}
#[test]
fn no_such_field_with_type_macro() {
check_diagnostics(
r#"
macro_rules! Type { () => { u32 }; }
struct Foo { bar: Type![] }
impl Foo {
fn new() -> Self {
Foo { bar: 0 }
}
}
"#,
);
}
#[test]
fn test_add_field_from_usage() {
check_fix(
r"
fn main() {
Foo { bar: 3, baz$0: false};
}
struct Foo {
bar: i32
}
",
r"
fn main() {
Foo { bar: 3, baz: false};
}
struct Foo {
bar: i32,
baz: bool
}
",
)
}
#[test]
fn test_add_field_in_other_file_from_usage() {
check_fix(
r#"
//- /main.rs
mod foo;
fn main() {
foo::Foo { bar: 3, $0baz: false};
}
//- /foo.rs
pub struct Foo {
bar: i32
}
"#,
r#"
pub struct Foo {
bar: i32,
pub(crate) baz: bool
}
"#,
)
}
#[test]
fn test_tuple_field_on_record_struct() {
check_no_fix(
r#"
struct Struct {}
fn main() {
Struct {
0$0: 0
}
}
"#,
)
}
}