Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract function assist #7535

Merged
merged 16 commits into from
Feb 5, 2021
Merged

Conversation

cpud36
Copy link
Contributor

@cpud36 cpud36 commented Feb 3, 2021

This PR adds extract function/method assist. closes #5409.

Supported features

Assist should support extracting from expressions(1, 2 + 2, loop { }) and from a series of statements, e.g.:

foo();
$0bar();
baz();$0
quix();

Assist also supports extracting parameters, like:

fn foo() -> i32 {
  let n = 1;
  $0n + 1$0
}

// -
fn foo() -> i32 {
  let n = 1;
  fun_name(n)
}

fn fun_name(n: i32) -> i32 {
  n + 1
}

Extracting methods also generally works.

Assist allows referencing outer variables, both mutably and immutably, and handles handles access to variables local to extracted function:

fn foo() {
  let mut n = 1;
  let mut m = 2;
  let mut moved_v = Vec::new();
  let mut ref_mut_v = Vec::new();
  $0
  n += 1;
  let k = 1;
  moved_v.push(n);
  let r = &mut m;
  ref_mut_v.push(*r);
  let h = 3;
  $0
  n = ref_mut_v.len() + k;
  n -= h + m;
}

// -
fn foo() {
  let mut n = 1;
  let mut m = 2;
  let mut moved_v = Vec::new();
  let mut ref_mut_v = Vec::new();
 
  let (k, h) =  fun_name(&mut n, moved_v, &mut m, &mut ref_mut_v);

  n = ref_mut_v.len() + k;
  n -= h + m;
}

fn fun_name(n: &mut i32, mut moved_v: Vec<i32>, m: &mut i32, ref_mut_v: &mut Vec<i32>) -> (i32, i32) {
  *n += 1;
  let k = 1;
  moved_v.push(*n);
  let r = m;
  ref_mut_v.push(*r);
  let h = 3;
  (k, h)
}

So we handle both input and output paramters

Showcase

extract_cursor_in_range_3
fill_match_arms_discard_wildcard
ide_db_helpers_handle_kind
ide_db_imports_location_local_query

Working with non-Copy types

Consider the following example:

fn foo() {
  let v = Vec::new();
  $0
  let n = v.len();
  $0
  let is_empty = v.is_empty();
}

v must be a parameter to extracted function.
The question is, what type should it have.
It could be v: Vec<i32>, or v: &Vec<i32>.
The former is incorrect for Vec<i32>, but the later is silly for i32.

To resolve this we need to know if the type implements Copy trait.

I didn't find any api available from assists to query this.
hir_ty::method_resolution::implements seems relevant, but is isn't publicly re-exported from hir.

Star(*) token and pointer dereference

If I understand correctly, in order to create expression like *p, one should use ast::make::expr_prefix(T![*], ...), which
in turn calls token(T![*]).

token does not have star in tokens::SOURCE_FILE, so this panics.
I had to add * to SOURCE_FILE to make it work.

Correct me if this is not intended way to do this.

Lowering access value -> mut ref -> shared ref

Consider the following example:

fn foo() {
  let v = Vec::new();
  $0 let n = v.len(); $0
}

v is not used after extracted function body, so both v: &Vec<i32> and v: Vec<i32> would work.
Currently the later would be chosen.

We can however check the body of extracted function and conclude that v: &Vec<i32> is sufficient.
Using v: &Vec<i32>(that is a minimal required access level) might be a better default.
I am unsure.

Cleanup

The assist seems to be reasonably handling most of common cases.
If there are no concerns with code it produces(i.e. with test cases), I will start cleaning up

[edit]
added showcase

there are a few currently limitations:
* no modifications of function body
* does not handle mutability and references
* no method support
* may produce incorrect results
currently mut refernce will *not* be downgraded to shared
if it is sufficient(see relevant test for example)
before child getter was used
when variable is defined inside extracted body
export this variable to original scope via return value(s)
It currently allows only directly setting variable.
No `&mut` references or methods.
Recognise &mut as variable modification.
This allows extracting functions with
`&mut var` with `var` being in outer scope
@cpud36 cpud36 changed the title [WIP] #5409 extract function assist Extract function assist Feb 3, 2021
@Veykril
Copy link
Member

Veykril commented Feb 4, 2021

Regarding whether a type implements copy you should be able to make use of https://github.com/rust-analyzer/rust-analyzer/blob/74a223faa33156be1b8b1a6880f9b63463027946/crates/hir/src/code_model.rs#L1662-L1662

@matklad
Copy link
Member

matklad commented Feb 4, 2021

I didn't find any api available from assists to query this.
hir_ty::method_resolution::implements seems relevant, but is isn't publicly re-exported from hir.

https://github.com/matklad/rust-analyzer/blob/1008aaae5821ce38975495c76d93b004888f2ed5/crates/hir/src/code_model.rs#L1662-L1670

Using v: &Vec(that is a minimal required access level) might be a better default. I am unsure.

I am not sure. I'd use whichever is simple. Long term, we need a spearate refactor to change function's parameter from T to &T

Copy link
Member

@matklad matklad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, this is one excellent PR @cpud36! I am absolutely blown away by the attention to details here, 👍. One day, once I (or someone else) writes how_to_add_assist.md guide, this would the the showcase. Kudos!

To my mind, this is already in a state well above average for assists, so feel free to merge when you think this is ready.

One day, once the major infrastructure of ra is polished, we should go through all the existing assists and make sure they also meet a high standard of quality, set by this PR. Thanks!

@@ -487,7 +487,7 @@ pub mod tokens {
use crate::{ast, AstNode, Parse, SourceFile, SyntaxKind::*, SyntaxToken};

pub(super) static SOURCE_FILE: Lazy<Parse<SourceFile>> =
Lazy::new(|| SourceFile::parse("const C: <()>::Item = (1 != 1, 2 == 2, !true)\n;\n\n"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

SyntaxKind::{self, BLOCK_EXPR, BREAK_EXPR, COMMENT, PATH_EXPR, RETURN_EXPR},
SyntaxNode, SyntaxToken, TextRange, TextSize, TokenAtOffset, T,
};
use test_utils::mark;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

})
.collect();

let params: Vec<_> = param_pats
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can move this (and maybe some other things) into the lambda inside ctx.add. That would be more efficient, as we won't call this code just to compute if the lightbulb should be visible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Most of the things actually.

Comment on lines 699 to 705
fn vars_defined_in_body(body: &FunctionBody, ctx: &AssistContext) -> Vec<Local> {
body.descendants()
.filter_map(ast::IdentPat::cast)
.filter_map(|let_stmt| ctx.sema.to_def(&let_stmt))
.unique()
.collect()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JFIY, it is an open question how to write this kind of logic properly. This very much depends on semantics, and to handle semantics precisely, you need to look at lowered expressions (hir::Body, ExprId), as they take macro expansion into account.

But its unclear how to loslessly go between lowering and the source syntax, and its the syntax you are ultimatelly interested in.

To be clear, this is a perfectly fine implementation for this PR, I just wist it were obvious how to make this pedantically correct.

.filter(|element| element.text_range().contains_inclusive(offset));
let element1 = iter.next().expect("offset does not fall into body");
let element2 = iter.next();
stdx::always!(iter.next().is_none(), "> 2 tokens at offset");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

let path = match token.ancestors().find_map(ast::Expr::cast) {
Some(n) => n,
None => {
stdx::never!(false, "cannot find path parent of variable usage: {:?}", token);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we should add support for stdx::never!("unreachable") as an alias for false

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 305 to 306
ParamKind::Value => "",
ParamKind::MutValue => "",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 merge match arms

@matklad
Copy link
Member

matklad commented Feb 4, 2021

bors d+

@bors
Copy link
Contributor

bors bot commented Feb 4, 2021

✌️ cpud36 can now approve this pull request. To approve and merge a pull request, simply reply with bors r+. More detailed instructions are available here.

@cpud36 cpud36 force-pushed the 5409_extract_function_assist branch from e4ebd71 to 7eaa3e5 Compare February 5, 2021 02:40
@cpud36
Copy link
Contributor Author

cpud36 commented Feb 5, 2021

bors r+

@bors
Copy link
Contributor

bors bot commented Feb 5, 2021

@bors bors bot merged commit ac59584 into rust-lang:master Feb 5, 2021
@cpud36 cpud36 deleted the 5409_extract_function_assist branch February 5, 2021 03:11
@matklad
Copy link
Member

matklad commented Feb 28, 2021

@cpud36 you might like this talk from jet brains conf: https://youtu.be/GRmOXuoe648?t=7872

@winkee01
Copy link

winkee01 commented May 1, 2022

@cpud36 In Neovim, usually I just type :lua vim.lsp.buf.code_action() and the code action popup menu should appear, but when I do this in Rust file, it didn't happen. Can you tell me how can I use this feature in neovim? any specific or extra configuration do I need?

PS: I have already configured rust_analyzer language server in Neovim, and it was correctly detected and attached.

lspconfig.rust_analyzer.setup({
   on_attach = on_attach,
   ...
})

Much appreciated.

@RReverser
Copy link

Similar to previous commenter, but - how do I access this from VSCode? I had no idea Rust Analyzer has this already implemented, and I don't see it in either "Refactor..." menu or the Command Palette.

@Veykril
Copy link
Member

Veykril commented May 10, 2022

You select the code you want to extract, then click the lightbulb that appears or hit ctrl + . and select extract to function (or something similar, not sure what the label states)

@RReverser
Copy link

Yeah I found the problem - looks like it doesn't work inside macro bodies (the bulb doesn't appear), and most of my code in this project is wrapped into contextual macros like

dbg_elapsed!("Context 1", {
  something();
  let value = dbg_elapsed!("Context 2", { ... });
});

I suppose I could convert those into closures, but it was a bit surprising considering that Rust Analyzer supports macros pretty well in other places.

@RReverser
Copy link

(to clarify, the 2nd argument in those macros is just :expr)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[feature-request] Extract method/function
5 participants