ide_assists/handlers/
fix_visibility.rs

1use hir::{HasSource, HasVisibility, ModuleDef, PathResolution, ScopeDef, db::HirDatabase};
2use ide_db::FileId;
3use syntax::{
4    AstNode, TextRange,
5    ast::{self, HasVisibility as _, edit_in_place::HasVisibilityEdit, make},
6};
7
8use crate::{AssistContext, AssistId, Assists};
9
10// Assist: fix_visibility
11//
12// Note that there is some duplication between this and the no_such_field diagnostic.
13//
14// Makes inaccessible item public.
15//
16// ```
17// mod m {
18//     fn frobnicate() {}
19// }
20// fn main() {
21//     m::frobnicate$0();
22// }
23// ```
24// ->
25// ```
26// mod m {
27//     $0pub(crate) fn frobnicate() {}
28// }
29// fn main() {
30//     m::frobnicate();
31// }
32// ```
33pub(crate) fn fix_visibility(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
34    add_vis_to_referenced_module_def(acc, ctx)
35}
36
37fn add_vis_to_referenced_module_def(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
38    let path: ast::Path = ctx.find_node_at_offset()?;
39    let qualifier = path.qualifier()?;
40    let name_ref = path.segment()?.name_ref()?;
41    let qualifier_res = ctx.sema.resolve_path(&qualifier)?;
42    let PathResolution::Def(ModuleDef::Module(module)) = qualifier_res else {
43        return None;
44    };
45    let (_, def) = module
46        .scope(ctx.db(), None)
47        .into_iter()
48        .find(|(name, _)| name.as_str() == name_ref.text().trim_start_matches("r#"))?;
49    let ScopeDef::ModuleDef(def) = def else {
50        return None;
51    };
52
53    let current_module = ctx.sema.scope(path.syntax())?.module();
54    let target_module = def.module(ctx.db())?;
55
56    if def.visibility(ctx.db()).is_visible_from(ctx.db(), current_module.into()) {
57        return None;
58    };
59
60    let (vis_owner, target, target_file, target_name) = target_data_for_def(ctx.db(), def)?;
61
62    let missing_visibility = if current_module.krate(ctx.db()) == target_module.krate(ctx.db()) {
63        make::visibility_pub_crate()
64    } else {
65        make::visibility_pub()
66    };
67
68    let assist_label = match target_name {
69        None => format!("Change visibility to {missing_visibility}"),
70        Some(name) => {
71            format!(
72                "Change visibility of {} to {missing_visibility}",
73                name.display(ctx.db(), current_module.krate(ctx.db()).edition(ctx.db()))
74            )
75        }
76    };
77
78    acc.add(AssistId::quick_fix("fix_visibility"), assist_label, target, |edit| {
79        edit.edit_file(target_file);
80
81        let vis_owner = edit.make_mut(vis_owner);
82        vis_owner.set_visibility(Some(missing_visibility.clone_for_update()));
83
84        if let Some((cap, vis)) = ctx.config.snippet_cap.zip(vis_owner.visibility()) {
85            edit.add_tabstop_before(cap, vis);
86        }
87    })
88}
89
90fn target_data_for_def(
91    db: &dyn HirDatabase,
92    def: hir::ModuleDef,
93) -> Option<(ast::AnyHasVisibility, TextRange, FileId, Option<hir::Name>)> {
94    fn offset_target_and_file_id<S, Ast>(
95        db: &dyn HirDatabase,
96        x: S,
97    ) -> Option<(ast::AnyHasVisibility, TextRange, FileId)>
98    where
99        S: HasSource<Ast = Ast>,
100        Ast: AstNode + ast::HasVisibility,
101    {
102        let source = x.source(db)?;
103        let in_file_syntax = source.syntax();
104        let file_id = in_file_syntax.file_id;
105        let range = in_file_syntax.value.text_range();
106        Some((
107            ast::AnyHasVisibility::new(source.value),
108            range,
109            file_id.original_file(db).file_id(db),
110        ))
111    }
112
113    let target_name;
114    let (offset, target, target_file) = match def {
115        hir::ModuleDef::Function(f) => {
116            target_name = Some(f.name(db));
117            offset_target_and_file_id(db, f)?
118        }
119        hir::ModuleDef::Adt(adt) => {
120            target_name = Some(adt.name(db));
121            match adt {
122                hir::Adt::Struct(s) => offset_target_and_file_id(db, s)?,
123                hir::Adt::Union(u) => offset_target_and_file_id(db, u)?,
124                hir::Adt::Enum(e) => offset_target_and_file_id(db, e)?,
125            }
126        }
127        hir::ModuleDef::Const(c) => {
128            target_name = c.name(db);
129            offset_target_and_file_id(db, c)?
130        }
131        hir::ModuleDef::Static(s) => {
132            target_name = Some(s.name(db));
133            offset_target_and_file_id(db, s)?
134        }
135        hir::ModuleDef::Trait(t) => {
136            target_name = Some(t.name(db));
137            offset_target_and_file_id(db, t)?
138        }
139        hir::ModuleDef::TypeAlias(t) => {
140            target_name = Some(t.name(db));
141            offset_target_and_file_id(db, t)?
142        }
143        hir::ModuleDef::Module(m) => {
144            target_name = m.name(db);
145            let in_file_source = m.declaration_source(db)?;
146            let file_id = in_file_source.file_id.original_file(db);
147            let range = in_file_source.value.syntax().text_range();
148            (ast::AnyHasVisibility::new(in_file_source.value), range, file_id.file_id(db))
149        }
150        // FIXME
151        hir::ModuleDef::Macro(_) => return None,
152        // Enum variants can't be private, we can't modify builtin types
153        hir::ModuleDef::Variant(_) | hir::ModuleDef::BuiltinType(_) => return None,
154    };
155
156    Some((offset, target, target_file, target_name))
157}
158
159#[cfg(test)]
160mod tests {
161    use crate::tests::{check_assist, check_assist_not_applicable};
162
163    use super::*;
164
165    #[test]
166    fn fix_visibility_of_fn() {
167        check_assist(
168            fix_visibility,
169            r"mod foo { fn foo() {} }
170              fn main() { foo::foo$0() } ",
171            r"mod foo { $0pub(crate) fn foo() {} }
172              fn main() { foo::foo() } ",
173        );
174        check_assist_not_applicable(
175            fix_visibility,
176            r"mod foo { pub fn foo() {} }
177              fn main() { foo::foo$0() } ",
178        )
179    }
180
181    #[test]
182    fn fix_visibility_of_adt_in_submodule() {
183        check_assist(
184            fix_visibility,
185            r"mod foo { struct Foo; }
186              fn main() { foo::Foo$0 } ",
187            r"mod foo { $0pub(crate) struct Foo; }
188              fn main() { foo::Foo } ",
189        );
190        check_assist_not_applicable(
191            fix_visibility,
192            r"mod foo { pub struct Foo; }
193              fn main() { foo::Foo$0 } ",
194        );
195        check_assist(
196            fix_visibility,
197            r"mod foo { enum Foo; }
198              fn main() { foo::Foo$0 } ",
199            r"mod foo { $0pub(crate) enum Foo; }
200              fn main() { foo::Foo } ",
201        );
202        check_assist_not_applicable(
203            fix_visibility,
204            r"mod foo { pub enum Foo; }
205              fn main() { foo::Foo$0 } ",
206        );
207        check_assist(
208            fix_visibility,
209            r"mod foo { union Foo; }
210              fn main() { foo::Foo$0 } ",
211            r"mod foo { $0pub(crate) union Foo; }
212              fn main() { foo::Foo } ",
213        );
214        check_assist_not_applicable(
215            fix_visibility,
216            r"mod foo { pub union Foo; }
217              fn main() { foo::Foo$0 } ",
218        );
219    }
220
221    #[test]
222    fn fix_visibility_of_adt_in_other_file() {
223        check_assist(
224            fix_visibility,
225            r"
226//- /main.rs
227mod foo;
228fn main() { foo::Foo$0 }
229
230//- /foo.rs
231struct Foo;
232",
233            r"$0pub(crate) struct Foo;
234",
235        );
236    }
237
238    #[test]
239    fn fix_visibility_of_enum_variant_field() {
240        // Enum variants, as well as their fields, always get the enum's visibility. In fact, rustc
241        // rejects any visibility specifiers on them, so this assist should never fire on them.
242        check_assist_not_applicable(
243            fix_visibility,
244            r"mod foo { pub enum Foo { Bar { bar: () } } }
245              fn main() { foo::Foo::Bar { $0bar: () }; } ",
246        );
247        check_assist_not_applicable(
248            fix_visibility,
249            r"
250//- /lib.rs
251mod foo;
252fn main() { foo::Foo::Bar { $0bar: () }; }
253//- /foo.rs
254pub enum Foo { Bar { bar: () } }
255",
256        );
257        check_assist_not_applicable(
258            fix_visibility,
259            r"mod foo { pub struct Foo { pub bar: (), } }
260              fn main() { foo::Foo { $0bar: () }; } ",
261        );
262        check_assist_not_applicable(
263            fix_visibility,
264            r"
265//- /lib.rs
266mod foo;
267fn main() { foo::Foo { $0bar: () }; }
268//- /foo.rs
269pub struct Foo { pub bar: () }
270",
271        );
272    }
273
274    #[test]
275    fn fix_visibility_of_const() {
276        check_assist(
277            fix_visibility,
278            r"mod foo { const FOO: () = (); }
279              fn main() { foo::FOO$0 } ",
280            r"mod foo { $0pub(crate) const FOO: () = (); }
281              fn main() { foo::FOO } ",
282        );
283        check_assist_not_applicable(
284            fix_visibility,
285            r"mod foo { pub const FOO: () = (); }
286              fn main() { foo::FOO$0 } ",
287        );
288    }
289
290    #[test]
291    fn fix_visibility_of_static() {
292        check_assist(
293            fix_visibility,
294            r"mod foo { static FOO: () = (); }
295              fn main() { foo::FOO$0 } ",
296            r"mod foo { $0pub(crate) static FOO: () = (); }
297              fn main() { foo::FOO } ",
298        );
299        check_assist_not_applicable(
300            fix_visibility,
301            r"mod foo { pub static FOO: () = (); }
302              fn main() { foo::FOO$0 } ",
303        );
304    }
305
306    #[test]
307    fn fix_visibility_of_trait() {
308        check_assist(
309            fix_visibility,
310            r"mod foo { trait Foo { fn foo(&self) {} } }
311              fn main() { let x: &dyn foo::$0Foo; } ",
312            r"mod foo { $0pub(crate) trait Foo { fn foo(&self) {} } }
313              fn main() { let x: &dyn foo::Foo; } ",
314        );
315        check_assist_not_applicable(
316            fix_visibility,
317            r"mod foo { pub trait Foo { fn foo(&self) {} } }
318              fn main() { let x: &dyn foo::Foo$0; } ",
319        );
320    }
321
322    #[test]
323    fn fix_visibility_of_type_alias() {
324        check_assist(
325            fix_visibility,
326            r"mod foo { type Foo = (); }
327              fn main() { let x: foo::Foo$0; } ",
328            r"mod foo { $0pub(crate) type Foo = (); }
329              fn main() { let x: foo::Foo; } ",
330        );
331        check_assist_not_applicable(
332            fix_visibility,
333            r"mod foo { pub type Foo = (); }
334              fn main() { let x: foo::Foo$0; } ",
335        );
336    }
337
338    #[test]
339    fn fix_visibility_of_module() {
340        check_assist(
341            fix_visibility,
342            r"mod foo { mod bar { fn bar() {} } }
343              fn main() { foo::bar$0::bar(); } ",
344            r"mod foo { $0pub(crate) mod bar { fn bar() {} } }
345              fn main() { foo::bar::bar(); } ",
346        );
347
348        check_assist(
349            fix_visibility,
350            r"
351//- /main.rs
352mod foo;
353fn main() { foo::bar$0::baz(); }
354
355//- /foo.rs
356mod bar {
357    pub fn baz() {}
358}
359",
360            r"$0pub(crate) mod bar {
361    pub fn baz() {}
362}
363",
364        );
365
366        check_assist_not_applicable(
367            fix_visibility,
368            r"mod foo { pub mod bar { pub fn bar() {} } }
369              fn main() { foo::bar$0::bar(); } ",
370        );
371    }
372
373    #[test]
374    fn fix_visibility_of_inline_module_in_other_file() {
375        check_assist(
376            fix_visibility,
377            r"
378//- /main.rs
379mod foo;
380fn main() { foo::bar$0::baz(); }
381
382//- /foo.rs
383mod bar;
384//- /foo/bar.rs
385pub fn baz() {}
386",
387            r"$0pub(crate) mod bar;
388",
389        );
390    }
391
392    #[test]
393    fn fix_visibility_of_module_declaration_in_other_file() {
394        check_assist(
395            fix_visibility,
396            r"
397//- /main.rs
398mod foo;
399fn main() { foo::bar$0>::baz(); }
400
401//- /foo.rs
402mod bar {
403    pub fn baz() {}
404}
405",
406            r"$0pub(crate) mod bar {
407    pub fn baz() {}
408}
409",
410        );
411    }
412
413    #[test]
414    fn adds_pub_when_target_is_in_another_crate() {
415        check_assist(
416            fix_visibility,
417            r"
418//- /main.rs crate:a deps:foo
419foo::Bar$0
420//- /lib.rs crate:foo
421struct Bar;
422",
423            r"$0pub struct Bar;
424",
425        )
426    }
427
428    #[test]
429    fn replaces_pub_crate_with_pub() {
430        check_assist(
431            fix_visibility,
432            r"
433//- /main.rs crate:a deps:foo
434foo::Bar$0
435//- /lib.rs crate:foo
436pub(crate) struct Bar;
437",
438            r"$0pub struct Bar;
439",
440        );
441    }
442
443    #[test]
444    fn fix_visibility_of_reexport() {
445        // FIXME: broken test, this should fix visibility of the re-export
446        // rather than the struct.
447        check_assist(
448            fix_visibility,
449            r#"
450mod foo {
451    use bar::Baz;
452    mod bar { pub(super) struct Baz; }
453}
454foo::Baz$0
455"#,
456            r#"
457mod foo {
458    use bar::Baz;
459    mod bar { $0pub(crate) struct Baz; }
460}
461foo::Baz
462"#,
463        )
464    }
465}