Unnamed repository; edit this file 'description' to name the repository.
fix: no_such_field panic with macros
The no_such_field assist offers to add a field to a struct definition
when the user write a struct literal `Foo { no_such_field_yet: 1 }`.
However, if the struct is defined in a macro, we would look for the
struct definition using the offset computed from the expansion.
This produced changes at the wrong offset in the file. It also caused
"invalid offset" panics if the file with the definition site of the
macro was bigger than the file with the call site of the macro.
Instead, map the macro position back to the relevant source file
position.
AI disclosure: Written with help by GPT-5.6.
| -rw-r--r-- | crates/ide-diagnostics/src/handlers/no_such_field.rs | 89 |
1 files changed, 85 insertions, 4 deletions
diff --git a/crates/ide-diagnostics/src/handlers/no_such_field.rs b/crates/ide-diagnostics/src/handlers/no_such_field.rs index 6fb8702c53..e3364c1a37 100644 --- a/crates/ide-diagnostics/src/handlers/no_such_field.rs +++ b/crates/ide-diagnostics/src/handlers/no_such_field.rs @@ -1,11 +1,11 @@ use either::Either; -use hir::{HasSource, HirDisplay, Semantics, VariantId}; +use hir::{HasSource, HirDisplay, InMacroFile, Semantics, VariantId}; use ide_db::text_edit::TextEdit; use ide_db::{ EditionedFileId, RootDatabase, helpers::is_editable_crate, source_change::SourceChange, }; use syntax::{ - AstNode, + AstNode, TextSize, ast::{self, edit::IndentLevel, make}, }; @@ -87,7 +87,6 @@ fn missing_record_expr_field_fixes( record_field_list(fields)? } }; - let def_file_id = def_file_id.original_file(sema.db); if !is_editable_crate(module.krate(sema.db), sema.db) { return None; @@ -103,13 +102,19 @@ fn missing_record_expr_field_fixes( make::ty(&new_field_type.display_source_code(sema.db, module.into(), true).ok()?), ); - let (indent, offset, postfix, needs_comma) = + let (mut indent, offset, postfix, needs_comma) = if let Some(last_field) = record_fields.fields().last() { let indent = IndentLevel::from_node(last_field.syntax()); let offset = last_field.syntax().text_range().end(); let needs_comma = !last_field.to_string().ends_with(','); (indent, offset, String::new(), needs_comma) } else { + // We don't have enough whitespace information in a macro-defined empty struct + // to compute the correct indent. + if def_file_id.is_macro() { + return None; + } + let indent = IndentLevel::from_node(record_fields.syntax()); let offset = record_fields.l_curly_token()?.text_range().end(); let postfix = if record_fields.syntax().text().contains_char('\n') { @@ -119,6 +124,20 @@ fn missing_record_expr_field_fixes( }; (indent + 1, offset, postfix, false) }; + let (def_file_id, offset) = if let Some(macro_file) = def_file_id.macro_file() { + // Map the preceding token so the source insertion uses the end of that token. + let (range, _) = + InMacroFile::new(macro_file, offset - TextSize::new(1)).original_file_range(sema.db); + let anchor = sema + .parse(range.file_id) + .syntax() + .token_at_offset(range.range.start()) + .right_biased()?; + indent = IndentLevel::from_token(&anchor); + (range.file_id, range.range.end()) + } else { + (def_file_id.file_id()?, offset) + }; let mut new_field = new_field.to_string(); // FIXME: check submodule instead of FileId @@ -456,6 +475,68 @@ fn main() { } #[test] + fn test_add_field_macro_defined_struct() { + check_fix( + r#" +macro_rules! identity { ($($t:tt)*) => { $($t)* }; } +identity! { + struct S { + a: i32 + } +} +fn f() { let _ = S { a: 1, b$0: false }; } +"#, + r#" +macro_rules! identity { ($($t:tt)*) => { $($t)* }; } +identity! { + struct S { + a: i32, + b: bool + } +} +fn f() { let _ = S { a: 1, b: false }; } +"#, + ); + } + + #[test] + fn test_add_field_macro_defined_struct_large_offset() { + // Regression test: we should handle macro definitions whose offset is larger than + // the max position in main.rs. + check_fix( + r#" +//- /main.rs +#[macro_use] +mod m; +make_items!(); +fn f() { let _ = S { a: 1, bbb$0: 2 }; } +//- /m.rs +macro_rules! make_items { + () => { + pub struct Padding { + pub p0: i32, pub p1: i32, pub p2: i32, + pub p3: i32, pub p4: i32, pub p5: i32, + } + pub struct S { pub a: i32 } + }; +} +"#, + r#" +macro_rules! make_items { + () => { + pub struct Padding { + pub p0: i32, pub p1: i32, pub p2: i32, + pub p3: i32, pub p4: i32, pub p5: i32, + } + pub struct S { pub a: i32, + pub(crate) bbb: i32 } + }; +} +"#, + ) + } + + #[test] fn no_such_field_no_fix_for_struct_in_library_crate() { check_no_fix( r#" |