Improving “Extract Function” in Rust Analyzer 

Over the last several weeks, I’ve made a series of small improvements to Rust Analyzer, mostly focusing on the “Extract Function” assist.

Why Here? Why Now?

I’ve been following the development of the Rust Programming Language for the last several years. I’ve reviewed many an RFC, participated in internals discussions here or there, and read piles of blog posts. Beyond that, however, I haven’t meaningfully contributed to the project. I’m glad to finally be changing that.

According to the official Rust Analyzer website:

rust-analyzer is an implementation of Language Server Protocol for the Rust programming language. It provides features like completion and goto definition for many code editors, including VS Code, Emacs and Vim.

I think that might be underselling things a little. My favorite part of Rust Analyzer are the automated refactoring tools like “Extract Variable” or “Extract Function”.

I was specifically interested in the “Extract Function” assist for a couple of reasons. First: I’ve had the good fortune to work with colleagues like Jay Bazuzi and Arlo Belshee who fully convinced me of the value of automated refactoring tools. I may be completely unable to type without vim keybindings, but these days I use them from within IDEs. Thank goodness for plugins!

Second: a while back, I was watching someone stream some rust development, and when they went to extract a function, none of the generics showed up! Generics are one of Rust’s defining features, so providing support for them in refactoring tools felt like a big opportunity for improvement. I’ve had some extra time on my hands recently, so I decided to dig in!

A Screenshot of VSCode after applying the Extract Function assist to generic code, showing errors introduced by the lack of generic support, with a super-imposed crying emoji

Where to Start?

I cloned the repo, fired up my IDE, hit command P and typed “extract function”. Bingo! extract_function.rs reporting for duty! Three cheers for good names and the principle of least surprise!

Next: I wrote a unit test, and watched it fail. Gotta start somewhere!

From there, I worked backwards:

  • Can I change the output at all? Yup!
  • Can I make my test pass without properly implementing the logic to support it? Sure can!
  • Does that make ~all the other tests fail? You bet!

Okay, so I’ve got my problem covered by tests, and I know to change the system I’m working with. Progress! Now I just need to figure out how to… you know… actually fix the bug.

Luckily, I was not alone. There is a great community of folks working on Rust Analyzer, and they all hang out on the rust-lang zulip. I asked a few questions, and got some great help from Laurențiu, Florian Diebold, and Lukas Wirth. (Thanks again, y’all!)

Thanks to their help, I was able to make a bunch more progress.

What am I doing here?

I also realized I may have bitten off more than I could chew, at least for a first contribution. For one, there are three different kinds of “generics” in Rust (for now): “Type”, “Lifetime”, and the newest member of the club, “Const”. For two, they can be defined in multiple places (on the function definition itself, or on the parent impl block). For three, we only want to include generics in our newly extracted function if the code being extracted actually uses them.

My shipping instincts started to kick in: time to try cutting scope.

  • Can we get away with ignoring Lifetimes and Consts, to focus on Type Params for now? Probably.
  • Can we get away with copying all the Type Params, and not worry about filtering the unused ones out? Probably not.
  • Can we ship something else useful in the mean time, while figuring out what to do here? Definitely!

Baby Steps

Since I’d gotten reasonably comfortable with the Extract Function assist, I decided to take a look through the issue to tracker to see if there were any smaller problems folks had reported. Turns out, there were!

I landed my first contribution by ensuring that “Extract Function” would no longer produce duplicate function names. For my second, I prevented Extract Function from trying to create non-existent trait methods. Third, I found and fixed an issue with closures and parameters. I even stepped outside my comfort zone a little and improved the names suggested by “Extract Variable”.

Landing the Plane

A screenshot of the GitHub UI showing the pull request as "Merged"

After several rounds of review feedback and lots of incremental progress, I’m happy to report that my pull request to support generic type parameters in the Extract Function assist merged this morning.

First, it searches the parent function (and that function’s parent impl or Trait definition) for any generic parameters and where clauses.

Next, it searches the newly extracted function body and parameter list for any references to type parameters.

Finally, it filters the generics and where clauses from the parent to include only the ones actually referenced by the new function, and includes them in the new function definition. Neat!

A Screenshot of VSCode after applying the Extract Function assist to generic code, this time showing the generic type parameters and where clauses properly included in the newly extracted function, with a super-imposed heart-eyes emoji

It should be available to all rust analyzer users starting on Monday, July 18th. I hope you’ll try it out!

If it works for you, I’d be thrilled to hear about it. If it doesn’t, shoot me a tweet!

I’m hopeful my changes haven’t introduced any issues, but I’m always interested to hear about new use cases I may have failed to consider!

If you’re looking to get involved in a Rust-related open source project, I can strongly recommend contributing to Rust Analyzer. It’s a well organized, well tested codebase, with a helpful, supportive community, and solid developer documentation! They’ve even got a lovely curated list of "good first issues".

Many thanks to the Rust Analyzer team for making contributing such a fun and easy process, and a special shout out to @Veykril for holding my hand through the code review process.

Discuss this post on r/rust