Fix panics on Foo{mut x} for destructure_struct_binding

Example
---
```rust
struct Foo { x: () }
struct Bar { foo: Foo }
fn f(Bar { mut $0foo }: Bar) {}
```

**Before this PR**:

Panic `Option::unwrap`

**After this PR**:

```rust
struct Foo { x: () }
struct Bar { foo: Foo }
fn f(Bar { foo: Foo { mut x } }: Bar) {}
```
This commit is contained in:
A4-Tacks 2025-09-20 14:59:59 +08:00
parent b12a129347
commit d4731ad9e2
No known key found for this signature in database
GPG key ID: DBD861323040663B

View file

@ -7,7 +7,7 @@ use ide_db::{
search::{FileReference, SearchScope}, search::{FileReference, SearchScope},
}; };
use itertools::Itertools; use itertools::Itertools;
use syntax::ast::syntax_factory::SyntaxFactory; use syntax::ast::{HasName, syntax_factory::SyntaxFactory};
use syntax::syntax_editor::SyntaxEditor; use syntax::syntax_editor::SyntaxEditor;
use syntax::{AstNode, Edition, SmolStr, SyntaxNode, ToSmolStr, ast}; use syntax::{AstNode, Edition, SmolStr, SyntaxNode, ToSmolStr, ast};
@ -71,13 +71,14 @@ fn destructure_struct_binding_impl(
struct StructEditData { struct StructEditData {
ident_pat: ast::IdentPat, ident_pat: ast::IdentPat,
name: ast::Name,
kind: hir::StructKind, kind: hir::StructKind,
struct_def_path: hir::ModPath, struct_def_path: hir::ModPath,
visible_fields: Vec<hir::Field>, visible_fields: Vec<hir::Field>,
usages: Vec<FileReference>, usages: Vec<FileReference>,
names_in_scope: FxHashSet<SmolStr>, names_in_scope: FxHashSet<SmolStr>,
has_private_members: bool, has_private_members: bool,
is_nested: bool, need_record_field_name: bool,
is_ref: bool, is_ref: bool,
edition: Edition, edition: Edition,
} }
@ -114,7 +115,11 @@ fn collect_data(ident_pat: ast::IdentPat, ctx: &AssistContext<'_>) -> Option<Str
} }
let is_ref = ty.is_reference(); let is_ref = ty.is_reference();
let is_nested = ident_pat.syntax().parent().and_then(ast::RecordPatField::cast).is_some(); let need_record_field_name = ident_pat
.syntax()
.parent()
.and_then(ast::RecordPatField::cast)
.is_some_and(|field| field.colon_token().is_none());
let usages = ctx let usages = ctx
.sema .sema
@ -133,6 +138,7 @@ fn collect_data(ident_pat: ast::IdentPat, ctx: &AssistContext<'_>) -> Option<Str
let names_in_scope = get_names_in_scope(ctx, &ident_pat, &usages).unwrap_or_default(); let names_in_scope = get_names_in_scope(ctx, &ident_pat, &usages).unwrap_or_default();
Some(StructEditData { Some(StructEditData {
name: ident_pat.name()?,
ident_pat, ident_pat,
kind, kind,
struct_def_path, struct_def_path,
@ -140,7 +146,7 @@ fn collect_data(ident_pat: ast::IdentPat, ctx: &AssistContext<'_>) -> Option<Str
has_private_members, has_private_members,
visible_fields, visible_fields,
names_in_scope, names_in_scope,
is_nested, need_record_field_name,
is_ref, is_ref,
edition: module.krate().edition(ctx.db()), edition: module.krate().edition(ctx.db()),
}) })
@ -177,6 +183,7 @@ fn destructure_pat(
field_names: &[(SmolStr, SmolStr)], field_names: &[(SmolStr, SmolStr)],
) { ) {
let ident_pat = &data.ident_pat; let ident_pat = &data.ident_pat;
let name = &data.name;
let struct_path = mod_path_to_ast(&data.struct_def_path, data.edition); let struct_path = mod_path_to_ast(&data.struct_def_path, data.edition);
let is_ref = ident_pat.ref_token().is_some(); let is_ref = ident_pat.ref_token().is_some();
@ -194,9 +201,9 @@ fn destructure_pat(
hir::StructKind::Record => { hir::StructKind::Record => {
let fields = field_names.iter().map(|(old_name, new_name)| { let fields = field_names.iter().map(|(old_name, new_name)| {
// Use shorthand syntax if possible // Use shorthand syntax if possible
if old_name == new_name && !is_mut { if old_name == new_name {
make.record_pat_field_shorthand( make.record_pat_field_shorthand(
make.ident_pat(false, false, make.name(old_name)).into(), make.ident_pat(is_ref, is_mut, make.name(old_name)).into(),
) )
} else { } else {
make.record_pat_field( make.record_pat_field(
@ -215,8 +222,8 @@ fn destructure_pat(
// If the binding is nested inside a record, we need to wrap the new // If the binding is nested inside a record, we need to wrap the new
// destructured pattern in a non-shorthand record field // destructured pattern in a non-shorthand record field
let destructured_pat = if data.is_nested { let destructured_pat = if data.need_record_field_name {
make.record_pat_field(make.name_ref(&ident_pat.to_string()), new_pat).syntax().clone() make.record_pat_field(make.name_ref(&name.to_string()), new_pat).syntax().clone()
} else { } else {
new_pat.syntax().clone() new_pat.syntax().clone()
}; };
@ -579,7 +586,7 @@ mod tests {
struct Foo { bar: i32, baz: i32 } struct Foo { bar: i32, baz: i32 }
fn main() { fn main() {
let Foo { bar: mut bar, baz: mut baz } = Foo { bar: 1, baz: 2 }; let Foo { mut bar, mut baz } = Foo { bar: 1, baz: 2 };
let bar2 = bar; let bar2 = bar;
let baz2 = &baz; let baz2 = &baz;
} }
@ -587,6 +594,86 @@ mod tests {
) )
} }
#[test]
fn mut_record_field() {
check_assist(
destructure_struct_binding,
r#"
struct Foo { x: () }
struct Bar { foo: Foo }
fn f(Bar { mut $0foo }: Bar) {}
"#,
r#"
struct Foo { x: () }
struct Bar { foo: Foo }
fn f(Bar { foo: Foo { mut x } }: Bar) {}
"#,
)
}
#[test]
fn ref_record_field() {
check_assist(
destructure_struct_binding,
r#"
struct Foo { x: () }
struct Bar { foo: Foo }
fn f(Bar { ref $0foo }: Bar) {
let _ = foo.x;
}
"#,
r#"
struct Foo { x: () }
struct Bar { foo: Foo }
fn f(Bar { foo: Foo { ref x } }: Bar) {
let _ = *x;
}
"#,
)
}
#[test]
fn ref_mut_record_field() {
check_assist(
destructure_struct_binding,
r#"
struct Foo { x: () }
struct Bar { foo: Foo }
fn f(Bar { ref mut $0foo }: Bar) {
let _ = foo.x;
}
"#,
r#"
struct Foo { x: () }
struct Bar { foo: Foo }
fn f(Bar { foo: Foo { ref mut x } }: Bar) {
let _ = *x;
}
"#,
)
}
#[test]
fn ref_mut_record_renamed_field() {
check_assist(
destructure_struct_binding,
r#"
struct Foo { x: () }
struct Bar { foo: Foo }
fn f(Bar { foo: ref mut $0foo1 }: Bar) {
let _ = foo1.x;
}
"#,
r#"
struct Foo { x: () }
struct Bar { foo: Foo }
fn f(Bar { foo: Foo { ref mut x } }: Bar) {
let _ = *x;
}
"#,
)
}
#[test] #[test]
fn mut_ref() { fn mut_ref() {
check_assist( check_assist(