ide_assists/handlers/
change_visibility.rs

1use syntax::{
2    AstNode,
3    SyntaxKind::{
4        self, ASSOC_ITEM_LIST, CONST, ENUM, FN, MACRO_DEF, MODULE, SOURCE_FILE, STATIC, STRUCT,
5        TRAIT, TYPE_ALIAS, USE, VISIBILITY,
6    },
7    SyntaxNode, T,
8    ast::{self, HasName, HasVisibility},
9};
10
11use crate::{AssistContext, AssistId, Assists, utils::vis_offset};
12
13// Assist: change_visibility
14//
15// Adds or changes existing visibility specifier.
16//
17// ```
18// $0fn frobnicate() {}
19// ```
20// ->
21// ```
22// pub(crate) fn frobnicate() {}
23// ```
24pub(crate) fn change_visibility(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
25    if let Some(vis) = ctx.find_node_at_offset::<ast::Visibility>() {
26        return change_vis(acc, vis);
27    }
28    add_vis(acc, ctx)
29}
30
31fn add_vis(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
32    let item_keyword = ctx.token_at_offset().find(|leaf| {
33        matches!(
34            leaf.kind(),
35            T![const]
36                | T![static]
37                | T![fn]
38                | T![mod]
39                | T![struct]
40                | T![enum]
41                | T![trait]
42                | T![type]
43                | T![use]
44                | T![macro]
45        )
46    });
47
48    let (offset, target) = if let Some(keyword) = item_keyword {
49        let parent = keyword.parent()?;
50
51        if !can_add(&parent) {
52            return None;
53        }
54        // Already has visibility, do nothing
55        if parent.children().any(|child| child.kind() == VISIBILITY) {
56            return None;
57        }
58        (vis_offset(&parent), keyword.text_range())
59    } else if let Some(field_name) = ctx.find_node_at_offset::<ast::Name>() {
60        let field = field_name.syntax().ancestors().find_map(ast::RecordField::cast)?;
61        if field.name()? != field_name {
62            cov_mark::hit!(change_visibility_field_false_positive);
63            return None;
64        }
65        if field.visibility().is_some() {
66            return None;
67        }
68        check_is_not_variant(&field)?;
69        (vis_offset(field.syntax()), field_name.syntax().text_range())
70    } else if let Some(field) = ctx.find_node_at_offset::<ast::TupleField>() {
71        if field.visibility().is_some() {
72            return None;
73        }
74        check_is_not_variant(&field)?;
75        (vis_offset(field.syntax()), field.syntax().text_range())
76    } else {
77        return None;
78    };
79
80    acc.add(
81        AssistId::refactor_rewrite("change_visibility"),
82        "Change visibility to pub(crate)",
83        target,
84        |edit| {
85            edit.insert(offset, "pub(crate) ");
86        },
87    )
88}
89
90fn can_add(node: &SyntaxNode) -> bool {
91    const LEGAL: &[SyntaxKind] =
92        &[CONST, STATIC, TYPE_ALIAS, FN, MODULE, STRUCT, ENUM, TRAIT, USE, MACRO_DEF];
93
94    LEGAL.contains(&node.kind()) && {
95        let Some(p) = node.parent() else {
96            return false;
97        };
98
99        if p.kind() == ASSOC_ITEM_LIST {
100            p.parent()
101                .and_then(ast::Impl::cast)
102                // inherent impls i.e 'non-trait impls' have a non-local
103                // effect, thus can have visibility even when nested.
104                // so filter them out
105                .filter(|imp| imp.for_token().is_none())
106                .is_some()
107        } else {
108            matches!(p.kind(), SOURCE_FILE | MODULE)
109        }
110    }
111}
112
113fn change_vis(acc: &mut Assists, vis: ast::Visibility) -> Option<()> {
114    if vis.syntax().text() == "pub" {
115        let target = vis.syntax().text_range();
116        return acc.add(
117            AssistId::refactor_rewrite("change_visibility"),
118            "Change Visibility to pub(crate)",
119            target,
120            |edit| {
121                edit.replace(vis.syntax().text_range(), "pub(crate)");
122            },
123        );
124    }
125    if vis.syntax().text() == "pub(crate)" {
126        let target = vis.syntax().text_range();
127        return acc.add(
128            AssistId::refactor_rewrite("change_visibility"),
129            "Change visibility to pub",
130            target,
131            |edit| {
132                edit.replace(vis.syntax().text_range(), "pub");
133            },
134        );
135    }
136    None
137}
138
139fn check_is_not_variant(field: &impl AstNode) -> Option<()> {
140    let kind = field.syntax().parent()?.parent()?.kind();
141    (kind != SyntaxKind::VARIANT).then_some(())
142}
143
144#[cfg(test)]
145mod tests {
146    use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
147
148    use super::*;
149
150    #[test]
151    fn change_visibility_adds_pub_crate_to_items() {
152        check_assist(change_visibility, "$0fn foo() {}", "pub(crate) fn foo() {}");
153        check_assist(change_visibility, "f$0n foo() {}", "pub(crate) fn foo() {}");
154        check_assist(change_visibility, "$0struct Foo {}", "pub(crate) struct Foo {}");
155        check_assist(change_visibility, "$0mod foo {}", "pub(crate) mod foo {}");
156        check_assist(change_visibility, "$0trait Foo {}", "pub(crate) trait Foo {}");
157        check_assist(change_visibility, "m$0od {}", "pub(crate) mod {}");
158        check_assist(change_visibility, "unsafe f$0n foo() {}", "pub(crate) unsafe fn foo() {}");
159        check_assist(change_visibility, "$0macro foo() {}", "pub(crate) macro foo() {}");
160        check_assist(change_visibility, "$0use foo;", "pub(crate) use foo;");
161        check_assist(
162            change_visibility,
163            "impl Foo { f$0n foo() {} }",
164            "impl Foo { pub(crate) fn foo() {} }",
165        );
166        check_assist(
167            change_visibility,
168            "fn bar() { impl Foo { f$0n foo() {} } }",
169            "fn bar() { impl Foo { pub(crate) fn foo() {} } }",
170        );
171    }
172
173    #[test]
174    fn change_visibility_works_with_struct_fields() {
175        check_assist(
176            change_visibility,
177            r"struct S { $0field: u32 }",
178            r"struct S { pub(crate) field: u32 }",
179        );
180        check_assist(change_visibility, r"struct S ( $0u32 )", r"struct S ( pub(crate) u32 )");
181    }
182
183    #[test]
184    fn change_visibility_field_false_positive() {
185        cov_mark::check!(change_visibility_field_false_positive);
186        check_assist_not_applicable(
187            change_visibility,
188            r"struct S { field: [(); { let $0x = ();}] }",
189        )
190    }
191
192    #[test]
193    fn change_visibility_pub_to_pub_crate() {
194        check_assist(change_visibility, "$0pub fn foo() {}", "pub(crate) fn foo() {}")
195    }
196
197    #[test]
198    fn change_visibility_pub_crate_to_pub() {
199        check_assist(change_visibility, "$0pub(crate) fn foo() {}", "pub fn foo() {}")
200    }
201
202    #[test]
203    fn change_visibility_const() {
204        check_assist(change_visibility, "$0const FOO = 3u8;", "pub(crate) const FOO = 3u8;");
205    }
206
207    #[test]
208    fn change_visibility_static() {
209        check_assist(change_visibility, "$0static FOO = 3u8;", "pub(crate) static FOO = 3u8;");
210    }
211
212    #[test]
213    fn change_visibility_type_alias() {
214        check_assist(change_visibility, "$0type T = ();", "pub(crate) type T = ();");
215    }
216
217    #[test]
218    fn change_visibility_handles_comment_attrs() {
219        check_assist(
220            change_visibility,
221            r"
222            /// docs
223
224            // comments
225
226            #[derive(Debug)]
227            $0struct Foo;
228            ",
229            r"
230            /// docs
231
232            // comments
233
234            #[derive(Debug)]
235            pub(crate) struct Foo;
236            ",
237        )
238    }
239
240    #[test]
241    fn not_applicable_for_enum_variants() {
242        check_assist_not_applicable(
243            change_visibility,
244            r"mod foo { pub enum Foo {Foo1} }
245              fn main() { foo::Foo::Foo1$0 } ",
246        );
247    }
248
249    #[test]
250    fn not_applicable_for_enum_variant_fields() {
251        check_assist_not_applicable(change_visibility, r"pub enum Foo { Foo1($0i32) }");
252
253        check_assist_not_applicable(change_visibility, r"pub enum Foo { Foo1 { $0n: i32 } }");
254    }
255
256    #[test]
257    fn change_visibility_target() {
258        check_assist_target(change_visibility, "$0fn foo() {}", "fn");
259        check_assist_target(change_visibility, "pub(crate)$0 fn foo() {}", "pub(crate)");
260        check_assist_target(change_visibility, "struct S { $0field: u32 }", "field");
261    }
262
263    #[test]
264    fn not_applicable_for_items_within_traits() {
265        check_assist_not_applicable(change_visibility, "trait Foo { f$0n run() {} }");
266        check_assist_not_applicable(change_visibility, "trait Foo { con$0st FOO: u8 = 69; }");
267        check_assist_not_applicable(change_visibility, "impl Foo for Bar { f$0n quox() {} }");
268    }
269
270    #[test]
271    fn not_applicable_for_items_within_fns() {
272        check_assist_not_applicable(change_visibility, "fn foo() { f$0n inner() {} }");
273        check_assist_not_applicable(change_visibility, "fn foo() { unsafe f$0n inner() {} }");
274        check_assist_not_applicable(change_visibility, "fn foo() { const f$0n inner() {} }");
275        check_assist_not_applicable(change_visibility, "fn foo() { con$0st FOO: u8 = 69; }");
276        check_assist_not_applicable(change_visibility, "fn foo() { en$0um Foo {} }");
277        check_assist_not_applicable(change_visibility, "fn foo() { stru$0ct Foo {} }");
278        check_assist_not_applicable(change_visibility, "fn foo() { mo$0d foo {} }");
279        check_assist_not_applicable(change_visibility, "fn foo() { $0use foo; }");
280        check_assist_not_applicable(change_visibility, "fn foo() { $0type Foo = Bar<T>; }");
281        check_assist_not_applicable(change_visibility, "fn foo() { tr$0ait Foo {} }");
282        check_assist_not_applicable(
283            change_visibility,
284            "fn foo() { impl Trait for Bar { f$0n bar() {} } }",
285        );
286        check_assist_not_applicable(
287            change_visibility,
288            "fn foo() { impl Trait for Bar { con$0st FOO: u8 = 69; } }",
289        );
290    }
291}