10539: Add "generate delegate methods" assist r=Veykril a=yoshuawuyts

_Co-authored with `@rylev_.`

This patch adds a new assist: "generate delegate method" which creates a method that calls to a method defined on an inner field. Delegation is common when authoring newtypes, and having IDE support for this is the best way we can make this easier to author in Rust, bar adding language-level support for it. Thanks!

Closes #5944.

## Example

__before__
```rust
struct Age(u8);
impl Age {
    fn age(&self) -> u8 {
        self.0
    }
}

struct Person {
    ag$0e: Age,
}
```

__after__
```rust
struct Age(u8);
impl Age {
    fn age(&self) -> u8 {
        self.0
    }
}

struct Person {
    age: Age,
}

impl Person {
    $0fn age(&self) -> u8 {
        self.age.age()
    }
}
```

Co-authored-by: Ryan Levick <me@ryanlevick.com>
Co-authored-by: Yoshua Wuyts <yoshuawuyts@gmail.com>
This commit is contained in:
bors[bot] 2021-10-14 18:16:17 +00:00 committed by GitHub
commit e52d47a3b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 401 additions and 1 deletions

View file

@ -3007,3 +3007,9 @@ impl HasCrate for Function {
self.module(db).krate()
}
}
impl HasCrate for Type {
fn krate(&self, _db: &dyn HirDatabase) -> Crate {
self.krate.into()
}
}

View file

@ -0,0 +1,314 @@
use hir::{self, HasCrate, HasSource};
use syntax::ast::{self, make, AstNode, HasGenericParams, HasName, HasVisibility};
use crate::{
utils::{convert_param_list_to_arg_list, find_struct_impl, render_snippet, Cursor},
AssistContext, AssistId, AssistKind, Assists, GroupLabel,
};
use syntax::ast::edit::AstNodeEdit;
// Assist: generate_delegate_methods
//
// Generate delegate methods.
//
// ```
// struct Age(u8);
// impl Age {
// fn age(&self) -> u8 {
// self.0
// }
// }
//
// struct Person {
// ag$0e: Age,
// }
// ```
// ->
// ```
// struct Age(u8);
// impl Age {
// fn age(&self) -> u8 {
// self.0
// }
// }
//
// struct Person {
// age: Age,
// }
//
// impl Person {
// $0fn age(&self) -> u8 {
// self.age.age()
// }
// }
// ```
pub(crate) fn generate_delegate_methods(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let strukt = ctx.find_node_at_offset::<ast::Struct>()?;
let strukt_name = strukt.name()?;
let (field_name, field_ty) = match ctx.find_node_at_offset::<ast::RecordField>() {
Some(field) => {
let field_name = field.name()?;
let field_ty = field.ty()?;
(format!("{}", field_name), field_ty)
}
None => {
let field = ctx.find_node_at_offset::<ast::TupleField>()?;
let field_list = ctx.find_node_at_offset::<ast::TupleFieldList>()?;
let field_list_index = field_list.fields().position(|it| it == field)?;
let field_ty = field.ty()?;
(format!("{}", field_list_index), field_ty)
}
};
let sema_field_ty = ctx.sema.resolve_type(&field_ty)?;
let krate = sema_field_ty.krate(ctx.db());
let mut methods = vec![];
sema_field_ty.iterate_assoc_items(ctx.db(), krate, |item| {
if let hir::AssocItem::Function(f) = item {
if f.self_param(ctx.db()).is_some() {
methods.push(f)
}
}
Option::<()>::None
});
let target = field_ty.syntax().text_range();
for method in methods {
let adt = ast::Adt::Struct(strukt.clone());
let name = method.name(ctx.db()).to_string();
let impl_def = find_struct_impl(ctx, &adt, &name).flatten();
acc.add_group(
&GroupLabel("Generate delegate methods…".to_owned()),
AssistId("generate_delegate_methods", AssistKind::Generate),
format!("Generate delegate for `{}.{}()`", field_name, method.name(ctx.db())),
target,
|builder| {
// Create the function
let method_source = match method.source(ctx.db()) {
Some(source) => source.value,
None => return,
};
let method_name = method.name(ctx.db());
let vis = method_source.visibility();
let name = make::name(&method.name(ctx.db()).to_string());
let params =
method_source.param_list().unwrap_or_else(|| make::param_list(None, []));
let type_params = method_source.generic_param_list();
let arg_list = match method_source.param_list() {
Some(list) => convert_param_list_to_arg_list(list),
None => make::arg_list([]),
};
let tail_expr = make::expr_method_call(
make::ext::field_from_idents(["self", &field_name]).unwrap(), // This unwrap is ok because we have at least 1 arg in the list
make::name_ref(&method_name.to_string()),
arg_list,
);
let body = make::block_expr([], Some(tail_expr));
let ret_type = method_source.ret_type();
let is_async = method_source.async_token().is_some();
let f = make::fn_(vis, name, type_params, params, body, ret_type, is_async)
.indent(ast::edit::IndentLevel(1))
.clone_for_update();
let cursor = Cursor::Before(f.syntax());
// Create or update an impl block, attach the function to it,
// then insert into our code.
match impl_def {
Some(impl_def) => {
// Remember where in our source our `impl` block lives.
let impl_def = impl_def.clone_for_update();
let old_range = impl_def.syntax().text_range();
// Attach the function to the impl block
let assoc_items = impl_def.get_or_create_assoc_item_list();
assoc_items.add_item(f.clone().into());
// Update the impl block.
match ctx.config.snippet_cap {
Some(cap) => {
let snippet = render_snippet(cap, impl_def.syntax(), cursor);
builder.replace_snippet(cap, old_range, snippet);
}
None => {
builder.replace(old_range, impl_def.syntax().to_string());
}
}
}
None => {
// Attach the function to the impl block
let name = &strukt_name.to_string();
let params = strukt.generic_param_list();
let ty_params = params.clone();
let impl_def = make::impl_(make::ext::ident_path(name), params, ty_params)
.clone_for_update();
let assoc_items = impl_def.get_or_create_assoc_item_list();
assoc_items.add_item(f.clone().into());
// Insert the impl block.
match ctx.config.snippet_cap {
Some(cap) => {
let offset = strukt.syntax().text_range().end();
let snippet = render_snippet(cap, impl_def.syntax(), cursor);
let snippet = format!("\n\n{}", snippet);
builder.insert_snippet(cap, offset, snippet);
}
None => {
let offset = strukt.syntax().text_range().end();
let snippet = format!("\n\n{}", impl_def.syntax().to_string());
builder.insert(offset, snippet);
}
}
}
}
},
)?;
}
Some(())
}
#[cfg(test)]
mod tests {
use crate::tests::check_assist;
use super::*;
#[test]
fn test_generate_delegate_create_impl_block() {
check_assist(
generate_delegate_methods,
r#"
struct Age(u8);
impl Age {
fn age(&self) -> u8 {
self.0
}
}
struct Person {
ag$0e: Age,
}"#,
r#"
struct Age(u8);
impl Age {
fn age(&self) -> u8 {
self.0
}
}
struct Person {
age: Age,
}
impl Person {
$0fn age(&self) -> u8 {
self.age.age()
}
}"#,
);
}
#[test]
fn test_generate_delegate_update_impl_block() {
check_assist(
generate_delegate_methods,
r#"
struct Age(u8);
impl Age {
fn age(&self) -> u8 {
self.0
}
}
struct Person {
ag$0e: Age,
}
impl Person {}"#,
r#"
struct Age(u8);
impl Age {
fn age(&self) -> u8 {
self.0
}
}
struct Person {
age: Age,
}
impl Person {
$0fn age(&self) -> u8 {
self.age.age()
}
}"#,
);
}
#[test]
fn test_generate_delegate_tuple_struct() {
check_assist(
generate_delegate_methods,
r#"
struct Age(u8);
impl Age {
fn age(&self) -> u8 {
self.0
}
}
struct Person(A$0ge);"#,
r#"
struct Age(u8);
impl Age {
fn age(&self) -> u8 {
self.0
}
}
struct Person(Age);
impl Person {
$0fn age(&self) -> u8 {
self.0.age()
}
}"#,
);
}
#[test]
fn test_generate_delegate_enable_all_attributes() {
check_assist(
generate_delegate_methods,
r#"
struct Age<T>(T);
impl<T> Age<T> {
pub(crate) async fn age<J, 'a>(&'a mut self, ty: T, arg: J) -> T {
self.0
}
}
struct Person<T> {
ag$0e: Age<T>,
}"#,
r#"
struct Age<T>(T);
impl<T> Age<T> {
pub(crate) async fn age<J, 'a>(&'a mut self, ty: T, arg: J) -> T {
self.0
}
}
struct Person<T> {
age: Age<T>,
}
impl<T> Person<T> {
$0pub(crate) async fn age<J, 'a>(&'a mut self, ty: T, arg: J) -> T {
self.age.age(ty, arg)
}
}"#,
);
}
}

View file

@ -144,6 +144,7 @@ mod handlers {
mod generate_is_empty_from_len;
mod generate_new;
mod generate_setter;
mod generate_delegate_methods;
mod add_return_type;
mod inline_call;
mod inline_local_variable;
@ -210,6 +211,7 @@ mod handlers {
generate_constant::generate_constant,
generate_default_from_enum_variant::generate_default_from_enum_variant,
generate_default_from_new::generate_default_from_new,
generate_delegate_methods::generate_delegate_methods,
generate_deref::generate_deref,
generate_derive::generate_derive,
generate_enum_is_method::generate_enum_is_method,

View file

@ -728,6 +728,43 @@ impl Default for Example {
)
}
#[test]
fn doctest_generate_delegate_methods() {
check_doc_test(
"generate_delegate_methods",
r#####"
struct Age(u8);
impl Age {
fn age(&self) -> u8 {
self.0
}
}
struct Person {
ag$0e: Age,
}
"#####,
r#####"
struct Age(u8);
impl Age {
fn age(&self) -> u8 {
self.0
}
}
struct Person {
age: Age,
}
impl Person {
$0fn age(&self) -> u8 {
self.age.age()
}
}
"#####,
)
}
#[test]
fn doctest_generate_deref() {
check_doc_test(

View file

@ -525,3 +525,19 @@ pub(crate) fn trimmed_text_range(source_file: &SourceFile, initial_range: TextRa
}
trimmed_range
}
/// Convert a list of function params to a list of arguments that can be passed
/// into a function call.
pub(crate) fn convert_param_list_to_arg_list(list: ast::ParamList) -> ast::ArgList {
let mut args = vec![];
for param in list.params() {
if let Some(ast::Pat::IdentPat(pat)) = param.pat() {
if let Some(name) = pat.name() {
let name = name.to_string();
let expr = make::expr_path(make::ext::ident_path(&name));
args.push(expr);
}
}
}
make::arg_list(args)
}

View file

@ -44,6 +44,15 @@ pub mod ext {
Some(path)
}
pub fn field_from_idents<'a>(
parts: impl std::iter::IntoIterator<Item = &'a str>,
) -> Option<ast::Expr> {
let mut iter = parts.into_iter();
let base = expr_path(ext::ident_path(iter.next()?));
let expr = iter.fold(base, |base, s| expr_field(base, s));
Some(expr)
}
pub fn expr_unreachable() -> ast::Expr {
expr_from_text("unreachable!()")
}
@ -124,6 +133,22 @@ pub fn assoc_item_list() -> ast::AssocItemList {
ast_from_text("impl C for D {}")
}
pub fn impl_(
ty: ast::Path,
params: Option<ast::GenericParamList>,
ty_params: Option<ast::GenericParamList>,
) -> ast::Impl {
let params = match params {
Some(params) => params.to_string(),
None => String::new(),
};
let ty_params = match ty_params {
Some(params) => params.to_string(),
None => String::new(),
};
ast_from_text(&format!("impl{} {}{} {{}}", params, ty, ty_params))
}
pub fn impl_trait(trait_: ast::Path, ty: ast::Path) -> ast::Impl {
ast_from_text(&format!("impl {} for {} {{}}", trait_, ty))
}
@ -645,7 +670,7 @@ pub fn fn_(
is_async: bool,
) -> ast::Fn {
let type_params = match type_params {
Some(type_params) => format!("<{}>", type_params),
Some(type_params) => format!("{}", type_params),
None => "".into(),
};
let ret_type = match ret_type {