rust-analyzer icon indicating copy to clipboard operation
rust-analyzer copied to clipboard

Add ide-assist: extract_struct_from_function_signature

Open mendelsshop opened this issue 5 months ago • 13 comments

When you select the signature of a function, you should have the ability to extract all or part of the function's parameters to a struct.

fn foo(bar: u32, baz: u32) { ... }

->

struct FooStruct {
     bar: u32,
     baz: u32,
}

fn foo(FooStruct) { ... }

Still very much a work in progress.

Closes: rust-lang/rust-analyzer#20331

mendelsshop avatar Jul 30 '25 14:07 mendelsshop

I have a few design decisions that I do not know what I should do about:

  • Functions support anonymous lifetimes, but structs do not so I should generate a new lifetime for every anonymous lifetime I see?

  • Should a self parameter be extracted to the struct? What if the self parameter is fully selected?

  • When I replace the signature in the original function, I could either 1) destructure the struct in the signature or 2) go around the whole function, updating all the uses.

fn foo(mut i: i32, j: i32) {
    i = j
}

-> 1)

struct FooStruct{ i: i32, j: i32 }

fn foo(FooStruct { mut i, j }: FooStruct) {
    i = j
}

or 2)

struct FooStruct{ i: i32, j: i32 }

fn foo(mut foo_struct FooStruct) {
    foo_struct.i = foo_struct.j
}
  • I would like there to be a way to just select a certain part of the signature and only extract that part to a new struct. If one character is selected, I want to assume that the user wants the whole function signature extracted to a struct, would that be a good way to differentiate whether to extract the whole signature or only a part of it?

  • I am also not planning on supporting destructred patterns like:

fn foo((a, b): (i32, i32)) {}

mendelsshop avatar Jul 31 '25 14:07 mendelsshop

Also, I think CI is failing because the slow tests need to be updated. I could not figure out how to update them.

mendelsshop avatar Jul 31 '25 14:07 mendelsshop

  • Functions support anonymous lifetimes, but structs do not so I should generate a new lifetime for every anonymous lifetime I see?

Lifetime support in r-a is very incomplete. Ideally I think we'd generate one lifetime for each invariant lifetime, and another one for all the covariant. We can't do it now so I think the best will be to always generate a single lifetime.

  • When I replace the signature in the original function, I could either 1) destructure the struct in the signature or 2) go around the whole function, updating all the uses.

Assists should focus on one change. Therefore, I think we should destructure the parameter, and have another assist to convert to dot access.

Although... Destructuring in parameters isn't that common in idiomatic Rust, so I'm not sure. @rust-lang/rust-analyzer thoughts?

  • I would like there to be a way to just select a certain part of the signature and only extract that part to a new struct. If one character is selected, I want to assume that the user wants the whole function signature extracted to a struct, would that be a good way to differentiate whether to extract the whole signature or only a part of it?

I think the assist should only be enabled when there is a selection, not on empty selection, that's too much clutter. This way you also don't have to fix the slow tests.

  • I am also not planning on supporting destructred patterns like:

Feel free. This can always be added later if desired.

  • Should a self parameter be extracted to the struct? What if the self parameter is fully selected?

Given what I said about selection, I think it should be treated like any other parameter, i.e. put it only if it's selected.

ChayimFriedman2 avatar Jul 31 '25 15:07 ChayimFriedman2

What should I do about impl trait, my assumption is that just don't provide the assist then.

Also, when the struct has generics, the way I am doing it now only the function signature fully specifies all the generics while uses, rely on inference from the caller.

fn foo<'a>($0bar: &'_ &'a i32$0, baz: i32) {
    foo(bar, baz)
}

->

struct FooStruct<'a, 'b>{ bar: &'b &'a i32 }
// signature (all generics specified)
fn foo<'a>(FooStruct { bar, .. }: FooStruct<'a, '_>, baz: i32) {
    // use (bare struct name without any generics specified)
    foo(FooStruct { bar: bar }, baz)
}

mendelsshop avatar Aug 01 '25 23:08 mendelsshop

I think I won't support doing it for self parameters, as that would be too much of a change. You have to possibly move the function out of the impl block and update all the references to be normal function calls. And this can be done in two steps, first by renaming self and the using this code assist.

Also, should I be copying relevant where clauses? My reason not to do that is that I think it is common to put where clauses on functions and leave the types alone. But when we copy the generics parameters from the function we also copy any bound that are on the generics directly, like <A: Copy>.

mendelsshop avatar Aug 03 '25 17:08 mendelsshop

It's mostly working, there are a few things I could not figure out how to do:

If there is a comment in between two parameters, I cannot move into the struct definition between the corresponding fields.

Also for a case like this:

struct Foo<'a> { baz: &'a i32 }
fn bar(foo: Foo) {}

How do I detect that the Foo type in bar is missing lifetime arguments so I can add explicit lifetimes for it?

And I assume I should remove the "feat: extract_struct_from_function_signature" header from all my commit messages?

mendelsshop avatar Aug 04 '25 21:08 mendelsshop

:warning: Warning :warning:

  • The following commits have merge commits (commits with multiple parents) in your changes. We have a no merge policy so these commits will need to be removed for this pull request to be merged.

    • 86597eb7740b2ce4f0dddf39a4c83852cbf9af4d

    You can start a rebase with the following commands:

    $ # rebase
    $ git pull --rebase https://github.com/rust-lang/rust-analyzer.git master
    $ git push --force-with-lease
    

rustbot avatar Aug 04 '25 21:08 rustbot

:warning: Warning :warning:

  • There are username mentions (such as @user) in the commit messages of the following commits. Please remove the mentions to avoid spamming these users.
    • 5947e3f925168b30df7347556a5f154e0140552c

rustbot avatar Sep 26 '25 17:09 rustbot

Sorry about the git mess I am trying to fix it.

mendelsshop avatar Sep 26 '25 17:09 mendelsshop

I almost everything away from ted to SyntaxEditor, besides for the import logic which indirectly relies on ted.

I think this ready for review.

mendelsshop avatar Oct 01 '25 21:10 mendelsshop

Fwiw we already have a https://github.com/rust-analyzer/rust-analyzer/blob/c5181dbbe33af6f21b9d83e02fdb6fda298a3b65/crates/ide-assists/src/handlers/generate_fn_type_alias.rs assist to extract the signature to a type alias, so there might be some sharing potential / changes to be made there depending on what we do here.

Veykril avatar Oct 26 '25 08:10 Veykril

I did not see that until now, it looks like a much simpler assist (it doesn't have to deal with anonymous/hidden lifetimes, imports, ....), and basically just copies the type signature.

Some stuff that maybe should be adapted is some of the verification this new assist does: it won't be suggested if there is impl Trait in one of the types or if one of the parameters is not a simple pattern.

Also, I realized that the generate_fn_types_alias assist copies any comments in the type signature into the type alias, I don't know if that is expected behavior.

mendelsshop avatar Oct 26 '25 21:10 mendelsshop

Edit: I had the wrong upstream remote.

I for some reason cannot get to keeping things up to date to work (it ends in lots of conflicts), this seems to be partially why CI is failing.

I also tried cherry-picking my commits on top of master, and there are no conflicts, but it ends up with something like 280270 commits ahead of, 8069 commits behind, and ends up adding lots of other contributors to the PR.

mendelsshop avatar Oct 26 '25 23:10 mendelsshop