7617: Add getter/setter assists r=Veykril a=yoshuawuyts

This patch makes progress towards the design outlined in https://github.com/rust-analyzer/rust-analyzer/issues/5943, and includes a small refactor which closes https://github.com/rust-analyzer/rust-analyzer/issues/7607. All together this patch does 4 things:

- Adds a `generate_getter` assist.
- Adds a `generate_getter_mut` assist.
- Adds a `generate_setter` assist.
- Moves the `generate_impl_text` function from `generate_new` into `utils` (which closes #7607).

## Design Notes

I've chosen to follow the [Rust API guidelines on getters](https://rust-lang.github.io/api-guidelines/naming.html#getter-names-follow-rust-convention-c-getter) as closely as possible. This deliberately leaves "builder pattern"-style setters out of scope.

Also, similar to https://github.com/rust-analyzer/rust-analyzer/pull/7570 this assist generates doc comments. I think this should work well in most cases, and for the few where it doesn't it's probably easily edited. This makes it slightly less correct than the #7570 implementation, but I think this is still useful enough to include for many of the same reasons.

The reason why this PR contains 3 assists, rather than 1, is because each of them is so similar to the others that it felt more noisy to do them separately than all at once. The amount of code added does not necessarily reflect that, but hope that still makes sense.

## Examples

**Input**
```rust
struct Person {
    name: String,     // <- cursor on "name"
}
```

**generate getter**
```rust
struct Person {
    name: String,
}

impl Person {
    /// Get a reference to the person's name.
    fn name(&self) -> &String {
        &self.name
    }
}
```

**generate mut getter**
```rust
struct Person {
    name: String,
}

impl Person {
    /// Get a mutable reference to the person's name.
    fn name_mut(&mut self) -> &mut String {
        &mut self.name
    }
}
```

**generate setter**
```rust
struct Person {
    name: String,
}

impl Person {
    /// Set the person's name.
    fn set_name(&mut self, name: String) {
        self.name = name;
    }
}
```


Co-authored-by: Yoshua Wuyts <yoshuawuyts+github@gmail.com>
This commit is contained in:
bors[bot] 2021-02-10 10:32:36 +00:00 committed by GitHub
commit 5e39d7a680
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 603 additions and 68 deletions

View File

@ -4,7 +4,7 @@ use syntax::ast::{self, AstNode, NameOwner};
use test_utils::mark;
use crate::{
utils::{find_impl_block, find_struct_impl},
utils::{find_impl_block, find_struct_impl, generate_impl_text},
AssistContext, AssistId, AssistKind, Assists,
};
@ -82,7 +82,7 @@ pub(crate) fn generate_enum_match_method(acc: &mut Assists, ctx: &AssistContext)
let start_offset = impl_def
.and_then(|impl_def| find_impl_block(impl_def, &mut buf))
.unwrap_or_else(|| {
buf = generate_impl_text(&parent_enum, &buf);
buf = generate_impl_text(&ast::Adt::Enum(parent_enum.clone()), &buf);
parent_enum.syntax().text_range().end()
});
@ -91,16 +91,6 @@ pub(crate) fn generate_enum_match_method(acc: &mut Assists, ctx: &AssistContext)
)
}
// Generates the surrounding `impl Type { <code> }` including type and lifetime
// parameters
fn generate_impl_text(strukt: &ast::Enum, code: &str) -> String {
let mut buf = String::with_capacity(code.len());
buf.push_str("\n\nimpl ");
buf.push_str(strukt.name().unwrap().text());
format_to!(buf, " {{\n{}\n}}", code);
buf
}
#[cfg(test)]
mod tests {
use test_utils::mark;

View File

@ -0,0 +1,156 @@
use stdx::{format_to, to_lower_snake_case};
use syntax::ast::VisibilityOwner;
use syntax::ast::{self, AstNode, NameOwner};
use crate::{
utils::{find_impl_block, find_struct_impl, generate_impl_text},
AssistContext, AssistId, AssistKind, Assists,
};
// Assist: generate_getter
//
// Generate a getter method.
//
// ```
// struct Person {
// nam$0e: String,
// }
// ```
// ->
// ```
// struct Person {
// name: String,
// }
//
// impl Person {
// /// Get a reference to the person's name.
// fn name(&self) -> &String {
// &self.name
// }
// }
// ```
pub(crate) fn generate_getter(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let strukt = ctx.find_node_at_offset::<ast::Struct>()?;
let field = ctx.find_node_at_offset::<ast::RecordField>()?;
let strukt_name = strukt.name()?;
let field_name = field.name()?;
let field_ty = field.ty()?;
// Return early if we've found an existing fn
let fn_name = to_lower_snake_case(&field_name.to_string());
let impl_def = find_struct_impl(&ctx, &ast::Adt::Struct(strukt.clone()), fn_name.as_str())?;
let target = field.syntax().text_range();
acc.add(
AssistId("generate_getter", AssistKind::Generate),
"Generate a getter method",
target,
|builder| {
let mut buf = String::with_capacity(512);
let fn_name_spaced = fn_name.replace('_', " ");
let strukt_name_spaced =
to_lower_snake_case(&strukt_name.to_string()).replace('_', " ");
if impl_def.is_some() {
buf.push('\n');
}
let vis = strukt.visibility().map_or(String::new(), |v| format!("{} ", v));
format_to!(
buf,
" /// Get a reference to the {}'s {}.
{}fn {}(&self) -> &{} {{
&self.{}
}}",
strukt_name_spaced,
fn_name_spaced,
vis,
fn_name,
field_ty,
fn_name,
);
let start_offset = impl_def
.and_then(|impl_def| find_impl_block(impl_def, &mut buf))
.unwrap_or_else(|| {
buf = generate_impl_text(&ast::Adt::Struct(strukt.clone()), &buf);
strukt.syntax().text_range().end()
});
builder.insert(start_offset, buf);
},
)
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
fn check_not_applicable(ra_fixture: &str) {
check_assist_not_applicable(generate_getter, ra_fixture)
}
#[test]
fn test_generate_getter_from_field() {
check_assist(
generate_getter,
r#"
struct Context<T: Clone> {
dat$0a: T,
}"#,
r#"
struct Context<T: Clone> {
data: T,
}
impl<T: Clone> Context<T> {
/// Get a reference to the context's data.
fn data(&self) -> &T {
&self.data
}
}"#,
);
}
#[test]
fn test_generate_getter_already_implemented() {
check_not_applicable(
r#"
struct Context<T: Clone> {
dat$0a: T,
}
impl<T: Clone> Context<T> {
fn data(&self) -> &T {
&self.data
}
}"#,
);
}
#[test]
fn test_generate_getter_from_field_with_visibility_marker() {
check_assist(
generate_getter,
r#"
pub(crate) struct Context<T: Clone> {
dat$0a: T,
}"#,
r#"
pub(crate) struct Context<T: Clone> {
data: T,
}
impl<T: Clone> Context<T> {
/// Get a reference to the context's data.
pub(crate) fn data(&self) -> &T {
&self.data
}
}"#,
);
}
}

View File

@ -0,0 +1,159 @@
use stdx::{format_to, to_lower_snake_case};
use syntax::ast::VisibilityOwner;
use syntax::ast::{self, AstNode, NameOwner};
use crate::{
utils::{find_impl_block, find_struct_impl, generate_impl_text},
AssistContext, AssistId, AssistKind, Assists,
};
// Assist: generate_getter_mut
//
// Generate a mut getter method.
//
// ```
// struct Person {
// nam$0e: String,
// }
// ```
// ->
// ```
// struct Person {
// name: String,
// }
//
// impl Person {
// /// Get a mutable reference to the person's name.
// fn name_mut(&mut self) -> &mut String {
// &mut self.name
// }
// }
// ```
pub(crate) fn generate_getter_mut(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let strukt = ctx.find_node_at_offset::<ast::Struct>()?;
let field = ctx.find_node_at_offset::<ast::RecordField>()?;
let strukt_name = strukt.name()?;
let field_name = field.name()?;
let field_ty = field.ty()?;
// Return early if we've found an existing fn
let fn_name = to_lower_snake_case(&field_name.to_string());
let impl_def = find_struct_impl(
&ctx,
&ast::Adt::Struct(strukt.clone()),
format!("{}_mut", fn_name).as_str(),
)?;
let target = field.syntax().text_range();
acc.add(
AssistId("generate_getter_mut", AssistKind::Generate),
"Generate a mut getter method",
target,
|builder| {
let mut buf = String::with_capacity(512);
let fn_name_spaced = fn_name.replace('_', " ");
let strukt_name_spaced =
to_lower_snake_case(&strukt_name.to_string()).replace('_', " ");
if impl_def.is_some() {
buf.push('\n');
}
let vis = strukt.visibility().map_or(String::new(), |v| format!("{} ", v));
format_to!(
buf,
" /// Get a mutable reference to the {}'s {}.
{}fn {}_mut(&mut self) -> &mut {} {{
&mut self.{}
}}",
strukt_name_spaced,
fn_name_spaced,
vis,
fn_name,
field_ty,
fn_name,
);
let start_offset = impl_def
.and_then(|impl_def| find_impl_block(impl_def, &mut buf))
.unwrap_or_else(|| {
buf = generate_impl_text(&ast::Adt::Struct(strukt.clone()), &buf);
strukt.syntax().text_range().end()
});
builder.insert(start_offset, buf);
},
)
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
fn check_not_applicable(ra_fixture: &str) {
check_assist_not_applicable(generate_getter_mut, ra_fixture)
}
#[test]
fn test_generate_getter_mut_from_field() {
check_assist(
generate_getter_mut,
r#"
struct Context<T: Clone> {
dat$0a: T,
}"#,
r#"
struct Context<T: Clone> {
data: T,
}
impl<T: Clone> Context<T> {
/// Get a mutable reference to the context's data.
fn data_mut(&mut self) -> &mut T {
&mut self.data
}
}"#,
);
}
#[test]
fn test_generate_getter_mut_already_implemented() {
check_not_applicable(
r#"
struct Context<T: Clone> {
dat$0a: T,
}
impl<T: Clone> Context<T> {
fn data_mut(&mut self) -> &mut T {
&mut self.data
}
}"#,
);
}
#[test]
fn test_generate_getter_mut_from_field_with_visibility_marker() {
check_assist(
generate_getter_mut,
r#"
pub(crate) struct Context<T: Clone> {
dat$0a: T,
}"#,
r#"
pub(crate) struct Context<T: Clone> {
data: T,
}
impl<T: Clone> Context<T> {
/// Get a mutable reference to the context's data.
pub(crate) fn data_mut(&mut self) -> &mut T {
&mut self.data
}
}"#,
);
}
}

View File

@ -1,12 +1,10 @@
use ast::Adt;
use itertools::Itertools;
use stdx::format_to;
use syntax::{
ast::{self, AstNode, GenericParamsOwner, NameOwner, StructKind, VisibilityOwner},
SmolStr,
};
use syntax::ast::{self, AstNode, NameOwner, StructKind, VisibilityOwner};
use crate::{
utils::{find_impl_block, find_struct_impl},
utils::{find_impl_block, find_struct_impl, generate_impl_text},
AssistContext, AssistId, AssistKind, Assists,
};
@ -28,7 +26,6 @@ use crate::{
// impl<T: Clone> Ctx<T> {
// fn $0new(data: T) -> Self { Self { data } }
// }
//
// ```
pub(crate) fn generate_new(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let strukt = ctx.find_node_at_offset::<ast::Struct>()?;
@ -40,7 +37,7 @@ pub(crate) fn generate_new(acc: &mut Assists, ctx: &AssistContext) -> Option<()>
};
// Return early if we've found an existing new fn
let impl_def = find_struct_impl(&ctx, &ast::Adt::Struct(strukt.clone()), "new")?;
let impl_def = find_struct_impl(&ctx, &Adt::Struct(strukt.clone()), "new")?;
let target = strukt.syntax().text_range();
acc.add(AssistId("generate_new", AssistKind::Generate), "Generate `new`", target, |builder| {
@ -63,7 +60,7 @@ pub(crate) fn generate_new(acc: &mut Assists, ctx: &AssistContext) -> Option<()>
let start_offset = impl_def
.and_then(|impl_def| find_impl_block(impl_def, &mut buf))
.unwrap_or_else(|| {
buf = generate_impl_text(&strukt, &buf);
buf = generate_impl_text(&Adt::Struct(strukt.clone()), &buf);
strukt.syntax().text_range().end()
});
@ -77,32 +74,6 @@ pub(crate) fn generate_new(acc: &mut Assists, ctx: &AssistContext) -> Option<()>
})
}
// Generates the surrounding `impl Type { <code> }` including type and lifetime
// parameters
fn generate_impl_text(strukt: &ast::Struct, code: &str) -> String {
let type_params = strukt.generic_param_list();
let mut buf = String::with_capacity(code.len());
buf.push_str("\n\nimpl");
if let Some(type_params) = &type_params {
format_to!(buf, "{}", type_params.syntax());
}
buf.push(' ');
buf.push_str(strukt.name().unwrap().text());
if let Some(type_params) = type_params {
let lifetime_params = type_params
.lifetime_params()
.filter_map(|it| it.lifetime())
.map(|it| SmolStr::from(it.text()));
let type_params =
type_params.type_params().filter_map(|it| it.name()).map(|it| SmolStr::from(it.text()));
format_to!(buf, "<{}>", lifetime_params.chain(type_params).format(", "))
}
format_to!(buf, " {{\n{}\n}}\n", code);
buf
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
@ -120,8 +91,7 @@ mod tests {
impl Foo {
fn $0new() -> Self { Self { } }
}
",
}",
);
check_assist(
generate_new,
@ -130,8 +100,7 @@ impl Foo {
impl<T: Clone> Foo<T> {
fn $0new() -> Self { Self { } }
}
",
}",
);
check_assist(
generate_new,
@ -140,8 +109,7 @@ impl<T: Clone> Foo<T> {
impl<'a, T: Foo<'a>> Foo<'a, T> {
fn $0new() -> Self { Self { } }
}
",
}",
);
check_assist(
generate_new,
@ -150,8 +118,7 @@ impl<'a, T: Foo<'a>> Foo<'a, T> {
impl Foo {
fn $0new(baz: String) -> Self { Self { baz } }
}
",
}",
);
check_assist(
generate_new,
@ -160,8 +127,7 @@ impl Foo {
impl Foo {
fn $0new(baz: String, qux: Vec<i32>) -> Self { Self { baz, qux } }
}
",
}",
);
// Check that visibility modifiers don't get brought in for fields
@ -172,8 +138,7 @@ impl Foo {
impl Foo {
fn $0new(baz: String, qux: Vec<i32>) -> Self { Self { baz, qux } }
}
",
}",
);
// Check that it reuses existing impls
@ -240,8 +205,7 @@ impl Foo {
impl Foo {
pub fn $0new() -> Self { Self { } }
}
",
}",
);
check_assist(
generate_new,
@ -250,8 +214,7 @@ impl Foo {
impl Foo {
pub(crate) fn $0new() -> Self { Self { } }
}
",
}",
);
}
@ -322,8 +285,7 @@ impl<T> Source<T> {
pub fn map<F: FnOnce(T) -> U, U>(self, f: F) -> Source<U> {
Source { file_id: self.file_id, ast: f(self.ast) }
}
}
"##,
}"##,
r##"
pub struct AstId<N: AstNode> {
file_id: HirFileId,
@ -347,8 +309,7 @@ impl<T> Source<T> {
pub fn map<F: FnOnce(T) -> U, U>(self, f: F) -> Source<U> {
Source { file_id: self.file_id, ast: f(self.ast) }
}
}
"##,
}"##,
);
}
}

View File

@ -0,0 +1,162 @@
use stdx::{format_to, to_lower_snake_case};
use syntax::ast::VisibilityOwner;
use syntax::ast::{self, AstNode, NameOwner};
use crate::{
utils::{find_impl_block, find_struct_impl, generate_impl_text},
AssistContext, AssistId, AssistKind, Assists,
};
// Assist: generate_setter
//
// Generate a setter method.
//
// ```
// struct Person {
// nam$0e: String,
// }
// ```
// ->
// ```
// struct Person {
// name: String,
// }
//
// impl Person {
// /// Set the person's name.
// fn set_name(&mut self, name: String) {
// self.name = name;
// }
// }
// ```
pub(crate) fn generate_setter(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let strukt = ctx.find_node_at_offset::<ast::Struct>()?;
let field = ctx.find_node_at_offset::<ast::RecordField>()?;
let strukt_name = strukt.name()?;
let field_name = field.name()?;
let field_ty = field.ty()?;
// Return early if we've found an existing fn
let fn_name = to_lower_snake_case(&field_name.to_string());
let impl_def = find_struct_impl(
&ctx,
&ast::Adt::Struct(strukt.clone()),
format!("set_{}", fn_name).as_str(),
)?;
let target = field.syntax().text_range();
acc.add(
AssistId("generate_setter", AssistKind::Generate),
"Generate a setter method",
target,
|builder| {
let mut buf = String::with_capacity(512);
let fn_name_spaced = fn_name.replace('_', " ");
let strukt_name_spaced =
to_lower_snake_case(&strukt_name.to_string()).replace('_', " ");
if impl_def.is_some() {
buf.push('\n');
}
let vis = strukt.visibility().map_or(String::new(), |v| format!("{} ", v));
format_to!(
buf,
" /// Set the {}'s {}.
{}fn set_{}(&mut self, {}: {}) {{
self.{} = {};
}}",
strukt_name_spaced,
fn_name_spaced,
vis,
fn_name,
fn_name,
field_ty,
fn_name,
fn_name,
);
let start_offset = impl_def
.and_then(|impl_def| find_impl_block(impl_def, &mut buf))
.unwrap_or_else(|| {
buf = generate_impl_text(&ast::Adt::Struct(strukt.clone()), &buf);
strukt.syntax().text_range().end()
});
builder.insert(start_offset, buf);
},
)
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
fn check_not_applicable(ra_fixture: &str) {
check_assist_not_applicable(generate_setter, ra_fixture)
}
#[test]
fn test_generate_setter_from_field() {
check_assist(
generate_setter,
r#"
struct Person<T: Clone> {
dat$0a: T,
}"#,
r#"
struct Person<T: Clone> {
data: T,
}
impl<T: Clone> Person<T> {
/// Set the person's data.
fn set_data(&mut self, data: T) {
self.data = data;
}
}"#,
);
}
#[test]
fn test_generate_setter_already_implemented() {
check_not_applicable(
r#"
struct Person<T: Clone> {
dat$0a: T,
}
impl<T: Clone> Person<T> {
fn set_data(&mut self, data: T) {
self.data = data;
}
}"#,
);
}
#[test]
fn test_generate_setter_from_field_with_visibility_marker() {
check_assist(
generate_setter,
r#"
pub(crate) struct Person<T: Clone> {
dat$0a: T,
}"#,
r#"
pub(crate) struct Person<T: Clone> {
data: T,
}
impl<T: Clone> Person<T> {
/// Set the person's data.
pub(crate) fn set_data(&mut self, data: T) {
self.data = data;
}
}"#,
);
}
}

View File

@ -130,8 +130,11 @@ mod handlers {
mod generate_enum_match_method;
mod generate_from_impl_for_enum;
mod generate_function;
mod generate_getter;
mod generate_getter_mut;
mod generate_impl;
mod generate_new;
mod generate_setter;
mod infer_function_return_type;
mod inline_function;
mod inline_local_variable;
@ -189,8 +192,11 @@ mod handlers {
generate_enum_match_method::generate_enum_match_method,
generate_from_impl_for_enum::generate_from_impl_for_enum,
generate_function::generate_function,
generate_getter::generate_getter,
generate_getter_mut::generate_getter_mut,
generate_impl::generate_impl,
generate_new::generate_new,
generate_setter::generate_setter,
infer_function_return_type::infer_function_return_type,
inline_function::inline_function,
inline_local_variable::inline_local_variable,

View File

@ -173,6 +173,9 @@ fn assist_order_field_struct() {
let mut assists = assists.iter();
assert_eq!(assists.next().expect("expected assist").label, "Change visibility to pub(crate)");
assert_eq!(assists.next().expect("expected assist").label, "Generate a getter method");
assert_eq!(assists.next().expect("expected assist").label, "Generate a mut getter method");
assert_eq!(assists.next().expect("expected assist").label, "Generate a setter method");
assert_eq!(assists.next().expect("expected assist").label, "Add `#[derive]`");
}

View File

@ -533,6 +533,54 @@ fn bar(arg: &str, baz: Baz) ${0:-> ()} {
)
}
#[test]
fn doctest_generate_getter() {
check_doc_test(
"generate_getter",
r#####"
struct Person {
nam$0e: String,
}
"#####,
r#####"
struct Person {
name: String,
}
impl Person {
/// Get a reference to the person's name.
fn name(&self) -> &String {
&self.name
}
}
"#####,
)
}
#[test]
fn doctest_generate_getter_mut() {
check_doc_test(
"generate_getter_mut",
r#####"
struct Person {
nam$0e: String,
}
"#####,
r#####"
struct Person {
name: String,
}
impl Person {
/// Get a mutable reference to the person's name.
fn name_mut(&mut self) -> &mut String {
&mut self.name
}
}
"#####,
)
}
#[test]
fn doctest_generate_impl() {
check_doc_test(
@ -571,7 +619,30 @@ struct Ctx<T: Clone> {
impl<T: Clone> Ctx<T> {
fn $0new(data: T) -> Self { Self { data } }
}
"#####,
)
}
#[test]
fn doctest_generate_setter() {
check_doc_test(
"generate_setter",
r#####"
struct Person {
nam$0e: String,
}
"#####,
r#####"
struct Person {
name: String,
}
impl Person {
/// Set the person's name.
fn set_name(&mut self, name: String) {
self.name = name;
}
}
"#####,
)
}

View File

@ -5,12 +5,13 @@ use std::ops;
use hir::{Adt, HasSource};
use ide_db::{helpers::SnippetCap, RootDatabase};
use itertools::Itertools;
use stdx::format_to;
use syntax::{
ast::edit::AstNodeEdit,
ast::AttrsOwner,
ast::NameOwner,
ast::{self, edit, make, ArgListOwner},
AstNode, Direction,
ast::{self, edit, make, ArgListOwner, GenericParamsOwner},
AstNode, Direction, SmolStr,
SyntaxKind::*,
SyntaxNode, TextSize, T,
};
@ -354,3 +355,29 @@ pub(crate) fn find_impl_block(impl_def: ast::Impl, buf: &mut String) -> Option<T
.end();
Some(start)
}
// Generates the surrounding `impl Type { <code> }` including type and lifetime
// parameters
pub(crate) fn generate_impl_text(adt: &ast::Adt, code: &str) -> String {
let type_params = adt.generic_param_list();
let mut buf = String::with_capacity(code.len());
buf.push_str("\n\nimpl");
if let Some(type_params) = &type_params {
format_to!(buf, "{}", type_params.syntax());
}
buf.push(' ');
buf.push_str(adt.name().unwrap().text());
if let Some(type_params) = type_params {
let lifetime_params = type_params
.lifetime_params()
.filter_map(|it| it.lifetime())
.map(|it| SmolStr::from(it.text()));
let type_params =
type_params.type_params().filter_map(|it| it.name()).map(|it| SmolStr::from(it.text()));
format_to!(buf, "<{}>", lifetime_params.chain(type_params).format(", "))
}
format_to!(buf, " {{\n{}\n}}", code);
buf
}