Skip to main content

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 _, syntax_factory::SyntaxFactory},
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 make = SyntaxFactory::without_mappings();
63
64    let missing_visibility = if current_module.krate(ctx.db()) == target_module.krate(ctx.db()) {
65        make.visibility_pub_crate()
66    } else {
67        make.visibility_pub()
68    };
69
70    let assist_label = match target_name {
71        None => format!("Change visibility to {missing_visibility}"),
72        Some(name) => {
73            format!(
74                "Change visibility of {} to {missing_visibility}",
75                name.display(ctx.db(), current_module.krate(ctx.db()).edition(ctx.db()))
76            )
77        }
78    };
79
80    acc.add(AssistId::quick_fix("fix_visibility"), assist_label, target, |builder| {
81        let editor = builder.make_editor(vis_owner.syntax());
82
83        if let Some(current_visibility) = vis_owner.visibility() {
84            editor.replace(current_visibility.syntax(), missing_visibility.syntax());
85        } else {
86            let vis_before = vis_owner
87                .syntax()
88                .children_with_tokens()
89                .find(|it| {
90                    !matches!(
91                        it.kind(),
92                        syntax::SyntaxKind::WHITESPACE
93                            | syntax::SyntaxKind::COMMENT
94                            | syntax::SyntaxKind::ATTR
95                    )
96                })
97                .unwrap_or_else(|| vis_owner.syntax().first_child_or_token().unwrap());
98
99            editor.insert_all(
100                syntax::syntax_editor::Position::before(vis_before),
101                vec![missing_visibility.syntax().clone().into(), make.whitespace(" ").into()],
102            );
103        }
104
105        if let Some(cap) = ctx.config.snippet_cap {
106            editor.add_annotation(missing_visibility.syntax(), builder.make_tabstop_before(cap));
107        }
108
109        builder.add_file_edits(target_file, editor);
110    })
111}
112
113fn target_data_for_def(
114    db: &dyn HirDatabase,
115    def: hir::ModuleDef,
116) -> Option<(ast::AnyHasVisibility, TextRange, FileId, Option<hir::Name>)> {
117    fn offset_target_and_file_id<S, Ast>(
118        db: &dyn HirDatabase,
119        x: S,
120    ) -> Option<(ast::AnyHasVisibility, TextRange, FileId)>
121    where
122        S: HasSource<Ast = Ast>,
123        Ast: AstNode + ast::HasVisibility,
124    {
125        let source = x.source(db)?;
126        let in_file_syntax = source.syntax();
127        let file_id = in_file_syntax.file_id;
128        let range = in_file_syntax.value.text_range();
129        Some((
130            ast::AnyHasVisibility::new(source.value),
131            range,
132            file_id.original_file(db).file_id(db),
133        ))
134    }
135
136    let target_name;
137    let (offset, target, target_file) = match def {
138        hir::ModuleDef::Function(f) => {
139            target_name = Some(f.name(db));
140            offset_target_and_file_id(db, f)?
141        }
142        hir::ModuleDef::Adt(adt) => {
143            target_name = Some(adt.name(db));
144            match adt {
145                hir::Adt::Struct(s) => offset_target_and_file_id(db, s)?,
146                hir::Adt::Union(u) => offset_target_and_file_id(db, u)?,
147                hir::Adt::Enum(e) => offset_target_and_file_id(db, e)?,
148            }
149        }
150        hir::ModuleDef::Const(c) => {
151            target_name = c.name(db);
152            offset_target_and_file_id(db, c)?
153        }
154        hir::ModuleDef::Static(s) => {
155            target_name = Some(s.name(db));
156            offset_target_and_file_id(db, s)?
157        }
158        hir::ModuleDef::Trait(t) => {
159            target_name = Some(t.name(db));
160            offset_target_and_file_id(db, t)?
161        }
162        hir::ModuleDef::TypeAlias(t) => {
163            target_name = Some(t.name(db));
164            offset_target_and_file_id(db, t)?
165        }
166        hir::ModuleDef::Module(m) => {
167            target_name = m.name(db);
168            let in_file_source = m.declaration_source(db)?;
169            let file_id = in_file_source.file_id.original_file(db);
170            let range = in_file_source.value.syntax().text_range();
171            (ast::AnyHasVisibility::new(in_file_source.value), range, file_id.file_id(db))
172        }
173        // FIXME
174        hir::ModuleDef::Macro(_) => return None,
175        // Enum variants can't be private, we can't modify builtin types
176        hir::ModuleDef::EnumVariant(_) | hir::ModuleDef::BuiltinType(_) => return None,
177    };
178
179    Some((offset, target, target_file, target_name))
180}
181
182#[cfg(test)]
183mod tests {
184    use crate::tests::{check_assist, check_assist_not_applicable};
185
186    use super::*;
187
188    #[test]
189    fn fix_visibility_of_fn() {
190        check_assist(
191            fix_visibility,
192            r"mod foo { fn foo() {} }
193              fn main() { foo::foo$0() } ",
194            r"mod foo { $0pub(crate) fn foo() {} }
195              fn main() { foo::foo() } ",
196        );
197        check_assist_not_applicable(
198            fix_visibility,
199            r"mod foo { pub fn foo() {} }
200              fn main() { foo::foo$0() } ",
201        )
202    }
203
204    #[test]
205    fn fix_visibility_of_adt_in_submodule() {
206        check_assist(
207            fix_visibility,
208            r"mod foo { struct Foo; }
209              fn main() { foo::Foo$0 } ",
210            r"mod foo { $0pub(crate) struct Foo; }
211              fn main() { foo::Foo } ",
212        );
213        check_assist_not_applicable(
214            fix_visibility,
215            r"mod foo { pub struct Foo; }
216              fn main() { foo::Foo$0 } ",
217        );
218        check_assist(
219            fix_visibility,
220            r"mod foo { enum Foo; }
221              fn main() { foo::Foo$0 } ",
222            r"mod foo { $0pub(crate) enum Foo; }
223              fn main() { foo::Foo } ",
224        );
225        check_assist_not_applicable(
226            fix_visibility,
227            r"mod foo { pub enum Foo; }
228              fn main() { foo::Foo$0 } ",
229        );
230        check_assist(
231            fix_visibility,
232            r"mod foo { union Foo; }
233              fn main() { foo::Foo$0 } ",
234            r"mod foo { $0pub(crate) union Foo; }
235              fn main() { foo::Foo } ",
236        );
237        check_assist_not_applicable(
238            fix_visibility,
239            r"mod foo { pub union Foo; }
240              fn main() { foo::Foo$0 } ",
241        );
242    }
243
244    #[test]
245    fn fix_visibility_of_adt_in_other_file() {
246        check_assist(
247            fix_visibility,
248            r"
249//- /main.rs
250mod foo;
251fn main() { foo::Foo$0 }
252
253//- /foo.rs
254struct Foo;
255",
256            r"$0pub(crate) struct Foo;
257",
258        );
259    }
260
261    #[test]
262    fn fix_visibility_of_enum_variant_field() {
263        // Enum variants, as well as their fields, always get the enum's visibility. In fact, rustc
264        // rejects any visibility specifiers on them, so this assist should never fire on them.
265        check_assist_not_applicable(
266            fix_visibility,
267            r"mod foo { pub enum Foo { Bar { bar: () } } }
268              fn main() { foo::Foo::Bar { $0bar: () }; } ",
269        );
270        check_assist_not_applicable(
271            fix_visibility,
272            r"
273//- /lib.rs
274mod foo;
275fn main() { foo::Foo::Bar { $0bar: () }; }
276//- /foo.rs
277pub enum Foo { Bar { bar: () } }
278",
279        );
280        check_assist_not_applicable(
281            fix_visibility,
282            r"mod foo { pub struct Foo { pub bar: (), } }
283              fn main() { foo::Foo { $0bar: () }; } ",
284        );
285        check_assist_not_applicable(
286            fix_visibility,
287            r"
288//- /lib.rs
289mod foo;
290fn main() { foo::Foo { $0bar: () }; }
291//- /foo.rs
292pub struct Foo { pub bar: () }
293",
294        );
295    }
296
297    #[test]
298    fn fix_visibility_of_const() {
299        check_assist(
300            fix_visibility,
301            r"mod foo { const FOO: () = (); }
302              fn main() { foo::FOO$0 } ",
303            r"mod foo { $0pub(crate) const FOO: () = (); }
304              fn main() { foo::FOO } ",
305        );
306        check_assist_not_applicable(
307            fix_visibility,
308            r"mod foo { pub const FOO: () = (); }
309              fn main() { foo::FOO$0 } ",
310        );
311    }
312
313    #[test]
314    fn fix_visibility_of_static() {
315        check_assist(
316            fix_visibility,
317            r"mod foo { static FOO: () = (); }
318              fn main() { foo::FOO$0 } ",
319            r"mod foo { $0pub(crate) static FOO: () = (); }
320              fn main() { foo::FOO } ",
321        );
322        check_assist_not_applicable(
323            fix_visibility,
324            r"mod foo { pub static FOO: () = (); }
325              fn main() { foo::FOO$0 } ",
326        );
327    }
328
329    #[test]
330    fn fix_visibility_of_trait() {
331        check_assist(
332            fix_visibility,
333            r"mod foo { trait Foo { fn foo(&self) {} } }
334              fn main() { let x: &dyn foo::$0Foo; } ",
335            r"mod foo { $0pub(crate) trait Foo { fn foo(&self) {} } }
336              fn main() { let x: &dyn foo::Foo; } ",
337        );
338        check_assist_not_applicable(
339            fix_visibility,
340            r"mod foo { pub trait Foo { fn foo(&self) {} } }
341              fn main() { let x: &dyn foo::Foo$0; } ",
342        );
343    }
344
345    #[test]
346    fn fix_visibility_of_type_alias() {
347        check_assist(
348            fix_visibility,
349            r"mod foo { type Foo = (); }
350              fn main() { let x: foo::Foo$0; } ",
351            r"mod foo { $0pub(crate) type Foo = (); }
352              fn main() { let x: foo::Foo; } ",
353        );
354        check_assist_not_applicable(
355            fix_visibility,
356            r"mod foo { pub type Foo = (); }
357              fn main() { let x: foo::Foo$0; } ",
358        );
359    }
360
361    #[test]
362    fn fix_visibility_of_module() {
363        check_assist(
364            fix_visibility,
365            r"mod foo { mod bar { fn bar() {} } }
366              fn main() { foo::bar$0::bar(); } ",
367            r"mod foo { $0pub(crate) mod bar { fn bar() {} } }
368              fn main() { foo::bar::bar(); } ",
369        );
370
371        check_assist(
372            fix_visibility,
373            r"
374//- /main.rs
375mod foo;
376fn main() { foo::bar$0::baz(); }
377
378//- /foo.rs
379mod bar {
380    pub fn baz() {}
381}
382",
383            r"$0pub(crate) mod bar {
384    pub fn baz() {}
385}
386",
387        );
388
389        check_assist_not_applicable(
390            fix_visibility,
391            r"mod foo { pub mod bar { pub fn bar() {} } }
392              fn main() { foo::bar$0::bar(); } ",
393        );
394    }
395
396    #[test]
397    fn fix_visibility_of_inline_module_in_other_file() {
398        check_assist(
399            fix_visibility,
400            r"
401//- /main.rs
402mod foo;
403fn main() { foo::bar$0::baz(); }
404
405//- /foo.rs
406mod bar;
407//- /foo/bar.rs
408pub fn baz() {}
409",
410            r"$0pub(crate) mod bar;
411",
412        );
413    }
414
415    #[test]
416    fn fix_visibility_of_module_declaration_in_other_file() {
417        check_assist(
418            fix_visibility,
419            r"
420//- /main.rs
421mod foo;
422fn main() { foo::bar$0>::baz(); }
423
424//- /foo.rs
425mod bar {
426    pub fn baz() {}
427}
428",
429            r"$0pub(crate) mod bar {
430    pub fn baz() {}
431}
432",
433        );
434    }
435
436    #[test]
437    fn adds_pub_when_target_is_in_another_crate() {
438        check_assist(
439            fix_visibility,
440            r"
441//- /main.rs crate:a deps:foo
442foo::Bar$0
443//- /lib.rs crate:foo
444struct Bar;
445",
446            r"$0pub struct Bar;
447",
448        )
449    }
450
451    #[test]
452    fn replaces_pub_crate_with_pub() {
453        check_assist(
454            fix_visibility,
455            r"
456//- /main.rs crate:a deps:foo
457foo::Bar$0
458//- /lib.rs crate:foo
459pub(crate) struct Bar;
460",
461            r"$0pub struct Bar;
462",
463        );
464    }
465
466    #[test]
467    fn fix_visibility_of_reexport() {
468        // FIXME: broken test, this should fix visibility of the re-export
469        // rather than the struct.
470        check_assist(
471            fix_visibility,
472            r#"
473mod foo {
474    use bar::Baz;
475    mod bar { pub(super) struct Baz; }
476}
477foo::Baz$0
478"#,
479            r#"
480mod foo {
481    use bar::Baz;
482    mod bar { $0pub(crate) struct Baz; }
483}
484foo::Baz
485"#,
486        )
487    }
488}