Repository: rust-analyzer/rust-analyzer
Pull request #8317 (2021-04-03 21:14:26)

Convert tuple struct to named struct assist
Closes https://github.com/rust-analyzer/rust-analyzer/issues/8192


Patch:
diff --git a/crates/ide_assists/src/handlers/convert_tuple_struct_to_named_struct.rs b/crates/ide_assists/src/handlers/convert_tuple_struct_to_named_struct.rs
new file mode 100644
index 0000000000..b5b5ada5e7
--- /dev/null
+++ b/crates/ide_assists/src/handlers/convert_tuple_struct_to_named_struct.rs
@@ -0,0 +1,516 @@
+use ide_db::defs::{Definition, NameRefClass};
+use syntax::{
+    ast::{self, AstNode, GenericParamsOwner, VisibilityOwner},
+    match_ast, SyntaxNode,
+};
+
+use crate::{assist_context::AssistBuilder, AssistContext, AssistId, AssistKind, Assists};
+
+// Assist: convert_tuple_struct_to_named_struct
+//
+// Converts tuple struct to struct with named fields.
+//
+// ```
+// struct Point$0(f32, f32);
+//
+// impl Point {
+//     pub fn new(x: f32, y: f32) -> Self {
+//         Point(x, y)
+//     }
+//
+//     pub fn x(&self) -> f32 {
+//         self.0
+//     }
+//
+//     pub fn y(&self) -> f32 {
+//         self.1
+//     }
+// }
+// ```
+// ->
+// ```
+// struct Point { field1: f32, field2: f32 }
+//
+// impl Point {
+//     pub fn new(x: f32, y: f32) -> Self {
+//         Point { field1: x, field2: y }
+//     }
+//
+//     pub fn x(&self) -> f32 {
+//         self.field1
+//     }
+//
+//     pub fn y(&self) -> f32 {
+//         self.field2
+//     }
+// }
+// ```
+pub(crate) fn convert_tuple_struct_to_named_struct(
+    acc: &mut Assists,
+    ctx: &AssistContext,
+) -> Option<()> {
+    let strukt = ctx.find_node_at_offset::<ast::Struct>()?;
+    let tuple_fields = match strukt.field_list()? {
+        ast::FieldList::TupleFieldList(it) => it,
+        ast::FieldList::RecordFieldList(_) => return None,
+    };
+    let strukt_def = ctx.sema.to_def(&strukt)?;
+
+    let target = strukt.syntax().text_range();
+    acc.add(
+        AssistId("convert_tuple_struct_to_named_struct", AssistKind::RefactorRewrite),
+        "Convert to named struct",
+        target,
+        |edit| {
+            let names = generate_names(tuple_fields.fields());
+            edit_field_references(ctx, edit, tuple_fields.fields(), &names);
+            edit_struct_references(ctx, edit, strukt_def, &names);
+            edit_struct_def(ctx, edit, &strukt, tuple_fields, names);
+        },
+    )
+}
+
+fn edit_struct_def(
+    ctx: &AssistContext,
+    edit: &mut AssistBuilder,
+    strukt: &ast::Struct,
+    tuple_fields: ast::TupleFieldList,
+    names: Vec<ast::Name>,
+) {
+    let record_fields = tuple_fields
+        .fields()
+        .zip(names)
+        .filter_map(|(f, name)| Some(ast::make::record_field(f.visibility(), name, f.ty()?)));
+    let record_fields = ast::make::record_field_list(record_fields);
+    let tuple_fields_text_range = tuple_fields.syntax().text_range();
+
+    edit.edit_file(ctx.frange.file_id);
+
+    if let Some(w) = strukt.where_clause() {
+        edit.delete(w.syntax().text_range());
+        edit.insert(tuple_fields_text_range.start(), ast::make::tokens::single_newline().text());
+        edit.insert(tuple_fields_text_range.start(), w.syntax().text());
+        edit.insert(tuple_fields_text_range.start(), ",");
+        edit.insert(tuple_fields_text_range.start(), ast::make::tokens::single_newline().text());
+    } else {
+        edit.insert(tuple_fields_text_range.start(), ast::make::tokens::single_space().text());
+    }
+
+    edit.replace(tuple_fields_text_range, record_fields.to_string());
+    strukt.semicolon_token().map(|t| edit.delete(t.text_range()));
+}
+
+fn edit_struct_references(
+    ctx: &AssistContext,
+    edit: &mut AssistBuilder,
+    strukt: hir::Struct,
+    names: &[ast::Name],
+) {
+    let strukt_def = Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Struct(strukt)));
+    let usages = strukt_def.usages(&ctx.sema).include_self_kw_refs(true).all();
+
+    let edit_node = |edit: &mut AssistBuilder, node: SyntaxNode| -> Option<()> {
+        match_ast! {
+            match node {
+                ast::TupleStructPat(tuple_struct_pat) => {
+                    edit.replace(
+                        tuple_struct_pat.syntax().text_range(),
+                        ast::make::record_pat_with_fields(
+                            tuple_struct_pat.path()?,
+                            ast::make::record_pat_field_list(tuple_struct_pat.fields().zip(names).map(
+                                |(pat, name)| {
+                                    ast::make::record_pat_field(
+                                        ast::make::name_ref(&name.to_string()),
+                                        pat,
+                                    )
+                                },
+                            )),
+                        )
+                        .to_string(),
+                    );
+                },
+                // for tuple struct creations like Foo(42)
+                ast::CallExpr(call_expr) => {
+                    let path = call_expr.syntax().descendants().find_map(ast::PathExpr::cast).and_then(|expr| expr.path())?;
+
+                    // this also includes method calls like Foo::new(42), we should skip them
+                    if let Some(name_ref) = path.segment().and_then(|s| s.name_ref()) {
+                        match NameRefClass::classify(&ctx.sema, &name_ref) {
+                            Some(NameRefClass::Definition(Definition::SelfType(_))) => {},
+                            Some(NameRefClass::Definition(def)) if def == strukt_def => {},
+                            _ => return None,
+                        };
+                    }
+
+                    let arg_list = call_expr.syntax().descendants().find_map(ast::ArgList::cast)?;
+
+                    edit.replace(
+                        call_expr.syntax().text_range(),
+                        ast::make::record_expr(
+                            path,
+                            ast::make::record_expr_field_list(arg_list.args().zip(names).map(
+                                |(expr, name)| {
+                                    ast::make::record_expr_field(
+                                        ast::make::name_ref(&name.to_string()),
+                                        Some(expr),
+                                    )
+                                },
+                            )),
+                        )
+                        .to_string(),
+                    );
+                },
+                _ => return None,
+            }
+        }
+        Some(())
+    };
+
+    for (file_id, refs) in usages {
+        edit.edit_file(file_id);
+        for r in refs {
+            for node in r.name.syntax().ancestors() {
+                if edit_node(edit, node).is_some() {
+                    break;
+                }
+            }
+        }
+    }
+}
+
+fn edit_field_references(
+    ctx: &AssistContext,
+    edit: &mut AssistBuilder,
+    fields: impl Iterator<Item = ast::TupleField>,
+    names: &[ast::Name],
+) {
+    for (field, name) in fields.zip(names) {
+        let field = match ctx.sema.to_def(&field) {
+            Some(it) => it,
+            None => continue,
+        };
+        let def = Definition::Field(field);
+        let usages = def.usages(&ctx.sema).all();
+        for (file_id, refs) in usages {
+            edit.edit_file(file_id);
+            for r in refs {
+                if let Some(name_ref) = r.name.as_name_ref() {
+                    edit.replace(name_ref.syntax().text_range(), name.text());
+                }
+            }
+        }
+    }
+}
+
+fn generate_names(fields: impl Iterator<Item = ast::TupleField>) -> Vec<ast::Name> {
+    fields.enumerate().map(|(i, _)| ast::make::name(&format!("field{}", i + 1))).collect()
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::tests::{check_assist, check_assist_not_applicable};
+
+    use super::*;
+
+    #[test]
+    fn not_applicable_other_than_tuple_struct() {
+        check_assist_not_applicable(
+            convert_tuple_struct_to_named_struct,
+            r#"struct Foo$0 { bar: u32 };"#,
+        );
+        check_assist_not_applicable(convert_tuple_struct_to_named_struct, r#"struct Foo$0;"#);
+    }
+
+    #[test]
+    fn convert_simple_struct() {
+        check_assist(
+            convert_tuple_struct_to_named_struct,
+            r#"
+struct Inner;
+struct A$0(Inner);
+
+impl A {
+    fn new(inner: Inner) -> A {
+        A(inner)
+    }
+
+    fn new_with_default() -> A {
+        A::new(Inner)
+    }
+
+    fn into_inner(self) -> Inner {
+        self.0
+    }
+}"#,
+            r#"
+struct Inner;
+struct A { field1: Inner }
+
+impl A {
+    fn new(inner: Inner) -> A {
+        A { field1: inner }
+    }
+
+    fn new_with_default() -> A {
+        A::new(Inner)
+    }
+
+    fn into_inner(self) -> Inner {
+        self.field1
+    }
+}"#,
+        );
+    }
+
+    #[test]
+    fn convert_struct_referenced_via_self_kw() {
+        check_assist(
+            convert_tuple_struct_to_named_struct,
+            r#"
+struct Inner;
+struct A$0(Inner);
+
+impl A {
+    fn new(inner: Inner) -> Self {
+        Self(inner)
+    }
+
+    fn new_with_default() -> Self {
+        Self::new(Inner)
+    }
+
+    fn into_inner(self) -> Inner {
+        self.0
+    }
+}"#,
+            r#"
+struct Inner;
+struct A { field1: Inner }
+
+impl A {
+    fn new(inner: Inner) -> Self {
+        Self { field1: inner }
+    }
+
+    fn new_with_default() -> Self {
+        Self::new(Inner)
+    }
+
+    fn into_inner(self) -> Inner {
+        self.field1
+    }
+}"#,
+        );
+    }
+
+    #[test]
+    fn convert_destructured_struct() {
+        check_assist(
+            convert_tuple_struct_to_named_struct,
+            r#"
+struct Inner;
+struct A$0(Inner);
+
+impl A {
+    fn into_inner(self) -> Inner {
+        let A(first) = self;
+        first
+    }
+
+    fn into_inner_via_self(self) -> Inner {
+        let Self(first) = self;
+        first
+    }
+}"#,
+            r#"
+struct Inner;
+struct A { field1: Inner }
+
+impl A {
+    fn into_inner(self) -> Inner {
+        let A { field1: first } = self;
+        first
+    }
+
+    fn into_inner_via_self(self) -> Inner {
+        let Self { field1: first } = self;
+        first
+    }
+}"#,
+        );
+    }
+
+    #[test]
+    fn convert_struct_with_visibility() {
+        check_assist(
+            convert_tuple_struct_to_named_struct,
+            r#"
+struct A$0(pub u32, pub(crate) u64);
+
+impl A {
+    fn new() -> A {
+        A(42, 42)
+    }
+
+    fn into_first(self) -> u32 {
+        self.0
+    }
+
+    fn into_second(self) -> u64 {
+        self.1
+    }
+}"#,
+            r#"
+struct A { pub field1: u32, pub(crate) field2: u64 }
+
+impl A {
+    fn new() -> A {
+        A { field1: 42, field2: 42 }
+    }
+
+    fn into_first(self) -> u32 {
+        self.field1
+    }
+
+    fn into_second(self) -> u64 {
+        self.field2
+    }
+}"#,
+        );
+    }
+
+    #[test]
+    fn convert_struct_with_wrapped_references() {
+        check_assist(
+            convert_tuple_struct_to_named_struct,
+            r#"
+struct Inner$0(u32);
+struct Outer(Inner);
+
+impl Outer {
+    fn new() -> Self {
+        Self(Inner(42))
+    }
+
+    fn into_inner(self) -> u32 {
+        (self.0).0
+    }
+
+    fn into_inner_destructed(self) -> u32 {
+        let Outer(Inner(x)) = self;
+        x
+    }
+}"#,
+            r#"
+struct Inner { field1: u32 }
+struct Outer(Inner);
+
+impl Outer {
+    fn new() -> Self {
+        Self(Inner { field1: 42 })
+    }
+
+    fn into_inner(self) -> u32 {
+        (self.0).field1
+    }
+
+    fn into_inner_destructed(self) -> u32 {
+        let Outer(Inner { field1: x }) = self;
+        x
+    }
+}"#,
+        );
+
+        check_assist(
+            convert_tuple_struct_to_named_struct,
+            r#"
+struct Inner(u32);
+struct Outer$0(Inner);
+
+impl Outer {
+    fn new() -> Self {
+        Self(Inner(42))
+    }
+
+    fn into_inner(self) -> u32 {
+        (self.0).0
+    }
+
+    fn into_inner_destructed(self) -> u32 {
+        let Outer(Inner(x)) = self;
+        x
+    }
+}"#,
+            r#"
+struct Inner(u32);
+struct Outer { field1: Inner }
+
+impl Outer {
+    fn new() -> Self {
+        Self { field1: Inner(42) }
+    }
+
+    fn into_inner(self) -> u32 {
+        (self.field1).0
+    }
+
+    fn into_inner_destructed(self) -> u32 {
+        let Outer { field1: Inner(x) } = self;
+        x
+    }
+}"#,
+        );
+    }
+
+    #[test]
+    fn convert_struct_with_multi_file_references() {
+        check_assist(
+            convert_tuple_struct_to_named_struct,
+            r#"
+//- /main.rs
+struct Inner;
+struct A$0(Inner);
+
+mod foo;
+
+//- /foo.rs
+use crate::{A, Inner};
+fn f() {
+    let a = A(Inner);
+}
+"#,
+            r#"
+//- /main.rs
+struct Inner;
+struct A { field1: Inner }
+
+mod foo;
+
+//- /foo.rs
+use crate::{A, Inner};
+fn f() {
+    let a = A { field1: Inner };
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn convert_struct_with_where_clause() {
+        check_assist(
+            convert_tuple_struct_to_named_struct,
+            r#"
+struct Wrap$0<T>(T)
+where
+    T: Display;
+"#,
+            r#"
+struct Wrap<T>
+where
+    T: Display,
+{ field1: T }
+
+"#,
+        );
+    }
+}
diff --git a/crates/ide_assists/src/lib.rs b/crates/ide_assists/src/lib.rs
index 3e2c82dace..1c55b9fbf9 100644
--- a/crates/ide_assists/src/lib.rs
+++ b/crates/ide_assists/src/lib.rs
@@ -118,6 +118,7 @@ mod handlers {
     mod convert_comment_block;
     mod convert_iter_for_each_to_for;
     mod convert_into_to_from;
+    mod convert_tuple_struct_to_named_struct;
     mod early_return;
     mod expand_glob_import;
     mod extract_function;
@@ -187,6 +188,7 @@ mod handlers {
             convert_comment_block::convert_comment_block,
             convert_iter_for_each_to_for::convert_iter_for_each_to_for,
             convert_into_to_from::convert_into_to_from,
+            convert_tuple_struct_to_named_struct::convert_tuple_struct_to_named_struct,
             early_return::convert_to_guarded_return,
             expand_glob_import::expand_glob_import,
             extract_struct_from_enum_variant::extract_struct_from_enum_variant,
diff --git a/crates/ide_db/src/search.rs b/crates/ide_db/src/search.rs
index b55e3851eb..8f899ea56b 100644
--- a/crates/ide_db/src/search.rs
+++ b/crates/ide_db/src/search.rs
@@ -7,7 +7,9 @@
 use std::{convert::TryInto, mem};
 
 use base_db::{FileId, FileRange, SourceDatabase, SourceDatabaseExt};
-use hir::{DefWithBody, HasAttrs, HasSource, InFile, ModuleSource, Semantics, Visibility};
+use hir::{
+    DefWithBody, HasAttrs, HasSource, InFile, ModuleDef, ModuleSource, Semantics, Visibility,
+};
 use once_cell::unsync::Lazy;
 use rustc_hash::FxHashMap;
 use syntax::{ast, match_ast, AstNode, TextRange, TextSize};
@@ -295,7 +297,7 @@ impl Definition {
     }
 
     pub fn usages<'a>(&'a self, sema: &'a Semantics<RootDatabase>) -> FindUsages<'a> {
-        FindUsages { def: self, sema, scope: None }
+        FindUsages { def: self, sema, scope: None, include_self_kw_refs: false }
     }
 }
 
@@ -303,9 +305,15 @@ pub struct FindUsages<'a> {
     def: &'a Definition,
     sema: &'a Semantics<'a, RootDatabase>,
     scope: Option<SearchScope>,
+    include_self_kw_refs: bool,
 }
 
 impl<'a> FindUsages<'a> {
+    pub fn include_self_kw_refs(mut self, include: bool) -> FindUsages<'a> {
+        self.include_self_kw_refs = include;
+        self
+    }
+
     pub fn in_scope(self, scope: SearchScope) -> FindUsages<'a> {
         self.set_scope(Some(scope))
     }
@@ -352,6 +360,8 @@ impl<'a> FindUsages<'a> {
         };
 
         let pat = name.as_str();
+        let search_for_self = self.include_self_kw_refs;
+
         for (file_id, search_range) in search_scope {
             let text = sema.db.file_text(file_id);
             let search_range =
@@ -359,31 +369,47 @@ impl<'a> FindUsages<'a> {
 
             let tree = Lazy::new(|| sema.parse(file_id).syntax().clone());
 
-            for (idx, _) in text.match_indices(pat) {
+            let mut handle_match = |idx: usize| -> bool {
                 let offset: TextSize = idx.try_into().unwrap();
                 if !search_range.contains_inclusive(offset) {
-                    continue;
+                    return false;
                 }
 
                 if let Some(name) = sema.find_node_at_offset_with_descend(&tree, offset) {
                     match name {
                         ast::NameLike::NameRef(name_ref) => {
                             if self.found_name_ref(&name_ref, sink) {
-                                return;
+                                return true;
                             }
                         }
                         ast::NameLike::Name(name) => {
                             if self.found_name(&name, sink) {
-                                return;
+                                return true;
                             }
                         }
                         ast::NameLike::Lifetime(lifetime) => {
                             if self.found_lifetime(&lifetime, sink) {
-                                return;
+                                return true;
                             }
                         }
                     }
                 }
+
+                return false;
+            };
+
+            for (idx, _) in text.match_indices(pat) {
+                if handle_match(idx) {
+                    return;
+                }
+            }
+
+            if search_for_self {
+                for (idx, _) in text.match_indices("Self") {
+                    if handle_match(idx) {
+                        return;
+                    }
+                }
             }
         }
     }
@@ -422,6 +448,24 @@ impl<'a> FindUsages<'a> {
                 };
                 sink(file_id, reference)
             }
+            Some(NameRefClass::Definition(Definition::SelfType(impl_))) => {
+                let ty = impl_.self_ty(self.sema.db);
+
+                if let Some(adt) = ty.as_adt() {
+                    if &Definition::ModuleDef(ModuleDef::Adt(adt)) == self.def {
+                        let FileRange { file_id, range } =
+                            self.sema.original_range(name_ref.syntax());
+                        let reference = FileReference {
+                            range,
+                            name: ast::NameLike::NameRef(name_ref.clone()),
+                            access: None,
+                        };
+                        return sink(file_id, reference);
+                    }
+                }
+
+                false
+            }
             Some(NameRefClass::FieldShorthand { local_ref: local, field_ref: field }) => {
                 let FileRange { file_id, range } = self.sema.original_range(name_ref.syntax());
                 let reference = match self.def {
diff --git a/crates/syntax/src/ast/make.rs b/crates/syntax/src/ast/make.rs
index c6a7b99b7c..3a588e5400 100644
--- a/crates/syntax/src/ast/make.rs
+++ b/crates/syntax/src/ast/make.rs
@@ -133,6 +133,17 @@ pub fn use_(visibility: Option<ast::Visibility>, use_tree: ast::UseTree) -> ast:
     ast_from_text(&format!("{}use {};", visibility, use_tree))
 }
 
+pub fn record_expr(path: ast::Path, fields: ast::RecordExprFieldList) -> ast::RecordExpr {
+    ast_from_text(&format!("fn f() {{ {} {} }}", path, fields))
+}
+
+pub fn record_expr_field_list(
+    fields: impl IntoIterator<Item = ast::RecordExprField>,
+) -> ast::RecordExprFieldList {
+    let fields = fields.into_iter().join(", ");
+    ast_from_text(&format!("fn f() {{ S {{ {} }} }}", fields))
+}
+
 pub fn record_expr_field(name: ast::NameRef, expr: Option<ast::Expr>) -> ast::RecordExprField {
     return match expr {
         Some(expr) => from_text(&format!("{}: {}", name, expr)),
@@ -325,6 +336,21 @@ pub fn record_pat(path: ast::Path, pats: impl IntoIterator<Item = ast::Pat>) ->
     }
 }
 
+pub fn record_pat_with_fields(path: ast::Path, fields: ast::RecordPatFieldList) -> ast::RecordPat {
+    ast_from_text(&format!("fn f({} {}: ()))", path, fields))
+}
+
+pub fn record_pat_field_list(
+    fields: impl IntoIterator<Item = ast::RecordPatField>,
+) -> ast::RecordPatFieldList {
+    let fields = fields.into_iter().join(", ");
+    ast_from_text(&format!("fn f(S {{ {} }}: ()))", fields))
+}
+
+pub fn record_pat_field(name_ref: ast::NameRef, pat: ast::Pat) -> ast::RecordPatField {
+    ast_from_text(&format!("fn f(S {{ {}: {} }}: ()))", name_ref, pat))
+}
+
 /// Returns a `BindPat` if the path has just one segment, a `PathPat` otherwise.
 pub fn path_pat(path: ast::Path) -> ast::Pat {
     return from_text(&path.to_string());


Test patch:
diff --git a/crates/ide_assists/src/tests/generated.rs b/crates/ide_assists/src/tests/generated.rs
index 27a22ca10c..f4a4749c80 100644
--- a/crates/ide_assists/src/tests/generated.rs
+++ b/crates/ide_assists/src/tests/generated.rs
@@ -291,6 +291,47 @@ fn main() {
     )
 }
 
+#[test]
+fn doctest_convert_tuple_struct_to_named_struct() {
+    check_doc_test(
+        "convert_tuple_struct_to_named_struct",
+        r#####"
+struct Point$0(f32, f32);
+
+impl Point {
+    pub fn new(x: f32, y: f32) -> Self {
+        Point(x, y)
+    }
+
+    pub fn x(&self) -> f32 {
+        self.0
+    }
+
+    pub fn y(&self) -> f32 {
+        self.1
+    }
+}
+"#####,
+        r#####"
+struct Point { field1: f32, field2: f32 }
+
+impl Point {
+    pub fn new(x: f32, y: f32) -> Self {
+        Point { field1: x, field2: y }
+    }
+
+    pub fn x(&self) -> f32 {
+        self.field1
+    }
+
+    pub fn y(&self) -> f32 {
+        self.field2
+    }
+}
+"#####,
+    )
+}
+
 #[test]
 fn doctest_expand_glob_import() {
     check_doc_test(