blob: c9f272474e7e1678462328bb3d865eb03f607efb [file] [log] [blame]
use hir::{db::HirDatabase, HasSource, HasVisibility, ModuleDef, PathResolution, ScopeDef};
use ide_db::base_db::FileId;
use syntax::{
ast::{self, edit_in_place::HasVisibilityEdit, make, HasVisibility as _},
AstNode, TextRange,
};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// FIXME: this really should be a fix for diagnostic, rather than an assist.
// Assist: fix_visibility
//
// Makes inaccessible item public.
//
// ```
// mod m {
// fn frobnicate() {}
// }
// fn main() {
// m::frobnicate$0();
// }
// ```
// ->
// ```
// mod m {
// $0pub(crate) fn frobnicate() {}
// }
// fn main() {
// m::frobnicate();
// }
// ```
pub(crate) fn fix_visibility(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
add_vis_to_referenced_module_def(acc, ctx)
.or_else(|| add_vis_to_referenced_record_field(acc, ctx))
}
fn add_vis_to_referenced_module_def(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let path: ast::Path = ctx.find_node_at_offset()?;
let qualifier = path.qualifier()?;
let name_ref = path.segment()?.name_ref()?;
let qualifier_res = ctx.sema.resolve_path(&qualifier)?;
let PathResolution::Def(ModuleDef::Module(module)) = qualifier_res else {
return None;
};
let (_, def) = module
.scope(ctx.db(), None)
.into_iter()
.find(|(name, _)| name.to_smol_str() == name_ref.text().as_str())?;
let ScopeDef::ModuleDef(def) = def else {
return None;
};
let current_module = ctx.sema.scope(path.syntax())?.module();
let target_module = def.module(ctx.db())?;
if def.visibility(ctx.db()).is_visible_from(ctx.db(), current_module.into()) {
return None;
};
let (vis_owner, target, target_file, target_name) = target_data_for_def(ctx.db(), def)?;
let missing_visibility = if current_module.krate() == target_module.krate() {
make::visibility_pub_crate()
} else {
make::visibility_pub()
};
let assist_label = match target_name {
None => format!("Change visibility to {missing_visibility}"),
Some(name) => {
format!("Change visibility of {} to {missing_visibility}", name.display(ctx.db()))
}
};
acc.add(AssistId("fix_visibility", AssistKind::QuickFix), assist_label, target, |edit| {
edit.edit_file(target_file);
let vis_owner = edit.make_mut(vis_owner);
vis_owner.set_visibility(missing_visibility.clone_for_update());
if let Some((cap, vis)) = ctx.config.snippet_cap.zip(vis_owner.visibility()) {
edit.add_tabstop_before(cap, vis);
}
})
}
fn add_vis_to_referenced_record_field(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let record_field: ast::RecordExprField = ctx.find_node_at_offset()?;
let (record_field_def, _, _) = ctx.sema.resolve_record_field(&record_field)?;
let current_module = ctx.sema.scope(record_field.syntax())?.module();
let visibility = record_field_def.visibility(ctx.db());
if visibility.is_visible_from(ctx.db(), current_module.into()) {
return None;
}
let parent = record_field_def.parent_def(ctx.db());
let parent_name = parent.name(ctx.db());
let target_module = parent.module(ctx.db());
let in_file_source = record_field_def.source(ctx.db())?;
let (vis_owner, target) = match in_file_source.value {
hir::FieldSource::Named(it) => {
let range = it.syntax().text_range();
(ast::AnyHasVisibility::new(it), range)
}
hir::FieldSource::Pos(it) => {
let range = it.syntax().text_range();
(ast::AnyHasVisibility::new(it), range)
}
};
let missing_visibility = if current_module.krate() == target_module.krate() {
make::visibility_pub_crate()
} else {
make::visibility_pub()
};
let target_file = in_file_source.file_id.original_file(ctx.db());
let target_name = record_field_def.name(ctx.db());
let assist_label = format!(
"Change visibility of {}.{} to {missing_visibility}",
parent_name.display(ctx.db()),
target_name.display(ctx.db())
);
acc.add(AssistId("fix_visibility", AssistKind::QuickFix), assist_label, target, |edit| {
edit.edit_file(target_file);
let vis_owner = edit.make_mut(vis_owner);
vis_owner.set_visibility(missing_visibility.clone_for_update());
if let Some((cap, vis)) = ctx.config.snippet_cap.zip(vis_owner.visibility()) {
edit.add_tabstop_before(cap, vis);
}
})
}
fn target_data_for_def(
db: &dyn HirDatabase,
def: hir::ModuleDef,
) -> Option<(ast::AnyHasVisibility, TextRange, FileId, Option<hir::Name>)> {
fn offset_target_and_file_id<S, Ast>(
db: &dyn HirDatabase,
x: S,
) -> Option<(ast::AnyHasVisibility, TextRange, FileId)>
where
S: HasSource<Ast = Ast>,
Ast: AstNode + ast::HasVisibility,
{
let source = x.source(db)?;
let in_file_syntax = source.syntax();
let file_id = in_file_syntax.file_id;
let range = in_file_syntax.value.text_range();
Some((ast::AnyHasVisibility::new(source.value), range, file_id.original_file(db.upcast())))
}
let target_name;
let (offset, target, target_file) = match def {
hir::ModuleDef::Function(f) => {
target_name = Some(f.name(db));
offset_target_and_file_id(db, f)?
}
hir::ModuleDef::Adt(adt) => {
target_name = Some(adt.name(db));
match adt {
hir::Adt::Struct(s) => offset_target_and_file_id(db, s)?,
hir::Adt::Union(u) => offset_target_and_file_id(db, u)?,
hir::Adt::Enum(e) => offset_target_and_file_id(db, e)?,
}
}
hir::ModuleDef::Const(c) => {
target_name = c.name(db);
offset_target_and_file_id(db, c)?
}
hir::ModuleDef::Static(s) => {
target_name = Some(s.name(db));
offset_target_and_file_id(db, s)?
}
hir::ModuleDef::Trait(t) => {
target_name = Some(t.name(db));
offset_target_and_file_id(db, t)?
}
hir::ModuleDef::TraitAlias(t) => {
target_name = Some(t.name(db));
offset_target_and_file_id(db, t)?
}
hir::ModuleDef::TypeAlias(t) => {
target_name = Some(t.name(db));
offset_target_and_file_id(db, t)?
}
hir::ModuleDef::Module(m) => {
target_name = m.name(db);
let in_file_source = m.declaration_source(db)?;
let file_id = in_file_source.file_id.original_file(db.upcast());
let range = in_file_source.value.syntax().text_range();
(ast::AnyHasVisibility::new(in_file_source.value), range, file_id)
}
// FIXME
hir::ModuleDef::Macro(_) => return None,
// Enum variants can't be private, we can't modify builtin types
hir::ModuleDef::Variant(_) | hir::ModuleDef::BuiltinType(_) => return None,
};
Some((offset, target, target_file, target_name))
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn fix_visibility_of_fn() {
check_assist(
fix_visibility,
r"mod foo { fn foo() {} }
fn main() { foo::foo$0() } ",
r"mod foo { $0pub(crate) fn foo() {} }
fn main() { foo::foo() } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub fn foo() {} }
fn main() { foo::foo$0() } ",
)
}
#[test]
fn fix_visibility_of_adt_in_submodule() {
check_assist(
fix_visibility,
r"mod foo { struct Foo; }
fn main() { foo::Foo$0 } ",
r"mod foo { $0pub(crate) struct Foo; }
fn main() { foo::Foo } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub struct Foo; }
fn main() { foo::Foo$0 } ",
);
check_assist(
fix_visibility,
r"mod foo { enum Foo; }
fn main() { foo::Foo$0 } ",
r"mod foo { $0pub(crate) enum Foo; }
fn main() { foo::Foo } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub enum Foo; }
fn main() { foo::Foo$0 } ",
);
check_assist(
fix_visibility,
r"mod foo { union Foo; }
fn main() { foo::Foo$0 } ",
r"mod foo { $0pub(crate) union Foo; }
fn main() { foo::Foo } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub union Foo; }
fn main() { foo::Foo$0 } ",
);
}
#[test]
fn fix_visibility_of_adt_in_other_file() {
check_assist(
fix_visibility,
r"
//- /main.rs
mod foo;
fn main() { foo::Foo$0 }
//- /foo.rs
struct Foo;
",
r"$0pub(crate) struct Foo;
",
);
}
#[test]
fn fix_visibility_of_struct_field() {
check_assist(
fix_visibility,
r"mod foo { pub struct Foo { bar: (), } }
fn main() { foo::Foo { $0bar: () }; } ",
r"mod foo { pub struct Foo { $0pub(crate) bar: (), } }
fn main() { foo::Foo { bar: () }; } ",
);
check_assist(
fix_visibility,
r"
//- /lib.rs
mod foo;
fn main() { foo::Foo { $0bar: () }; }
//- /foo.rs
pub struct Foo { bar: () }
",
r"pub struct Foo { $0pub(crate) bar: () }
",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub struct Foo { pub bar: (), } }
fn main() { foo::Foo { $0bar: () }; } ",
);
check_assist_not_applicable(
fix_visibility,
r"
//- /lib.rs
mod foo;
fn main() { foo::Foo { $0bar: () }; }
//- /foo.rs
pub struct Foo { pub bar: () }
",
);
}
#[test]
fn fix_visibility_of_enum_variant_field() {
// Enum variants, as well as their fields, always get the enum's visibility. In fact, rustc
// rejects any visibility specifiers on them, so this assist should never fire on them.
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub enum Foo { Bar { bar: () } } }
fn main() { foo::Foo::Bar { $0bar: () }; } ",
);
check_assist_not_applicable(
fix_visibility,
r"
//- /lib.rs
mod foo;
fn main() { foo::Foo::Bar { $0bar: () }; }
//- /foo.rs
pub enum Foo { Bar { bar: () } }
",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub struct Foo { pub bar: (), } }
fn main() { foo::Foo { $0bar: () }; } ",
);
check_assist_not_applicable(
fix_visibility,
r"
//- /lib.rs
mod foo;
fn main() { foo::Foo { $0bar: () }; }
//- /foo.rs
pub struct Foo { pub bar: () }
",
);
}
#[test]
fn fix_visibility_of_union_field() {
check_assist(
fix_visibility,
r"mod foo { pub union Foo { bar: (), } }
fn main() { foo::Foo { $0bar: () }; } ",
r"mod foo { pub union Foo { $0pub(crate) bar: (), } }
fn main() { foo::Foo { bar: () }; } ",
);
check_assist(
fix_visibility,
r"
//- /lib.rs
mod foo;
fn main() { foo::Foo { $0bar: () }; }
//- /foo.rs
pub union Foo { bar: () }
",
r"pub union Foo { $0pub(crate) bar: () }
",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub union Foo { pub bar: (), } }
fn main() { foo::Foo { $0bar: () }; } ",
);
check_assist_not_applicable(
fix_visibility,
r"
//- /lib.rs
mod foo;
fn main() { foo::Foo { $0bar: () }; }
//- /foo.rs
pub union Foo { pub bar: () }
",
);
}
#[test]
fn fix_visibility_of_const() {
check_assist(
fix_visibility,
r"mod foo { const FOO: () = (); }
fn main() { foo::FOO$0 } ",
r"mod foo { $0pub(crate) const FOO: () = (); }
fn main() { foo::FOO } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub const FOO: () = (); }
fn main() { foo::FOO$0 } ",
);
}
#[test]
fn fix_visibility_of_static() {
check_assist(
fix_visibility,
r"mod foo { static FOO: () = (); }
fn main() { foo::FOO$0 } ",
r"mod foo { $0pub(crate) static FOO: () = (); }
fn main() { foo::FOO } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub static FOO: () = (); }
fn main() { foo::FOO$0 } ",
);
}
#[test]
fn fix_visibility_of_trait() {
check_assist(
fix_visibility,
r"mod foo { trait Foo { fn foo(&self) {} } }
fn main() { let x: &dyn foo::$0Foo; } ",
r"mod foo { $0pub(crate) trait Foo { fn foo(&self) {} } }
fn main() { let x: &dyn foo::Foo; } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub trait Foo { fn foo(&self) {} } }
fn main() { let x: &dyn foo::Foo$0; } ",
);
}
#[test]
fn fix_visibility_of_type_alias() {
check_assist(
fix_visibility,
r"mod foo { type Foo = (); }
fn main() { let x: foo::Foo$0; } ",
r"mod foo { $0pub(crate) type Foo = (); }
fn main() { let x: foo::Foo; } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub type Foo = (); }
fn main() { let x: foo::Foo$0; } ",
);
}
#[test]
fn fix_visibility_of_module() {
check_assist(
fix_visibility,
r"mod foo { mod bar { fn bar() {} } }
fn main() { foo::bar$0::bar(); } ",
r"mod foo { $0pub(crate) mod bar { fn bar() {} } }
fn main() { foo::bar::bar(); } ",
);
check_assist(
fix_visibility,
r"
//- /main.rs
mod foo;
fn main() { foo::bar$0::baz(); }
//- /foo.rs
mod bar {
pub fn baz() {}
}
",
r"$0pub(crate) mod bar {
pub fn baz() {}
}
",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub mod bar { pub fn bar() {} } }
fn main() { foo::bar$0::bar(); } ",
);
}
#[test]
fn fix_visibility_of_inline_module_in_other_file() {
check_assist(
fix_visibility,
r"
//- /main.rs
mod foo;
fn main() { foo::bar$0::baz(); }
//- /foo.rs
mod bar;
//- /foo/bar.rs
pub fn baz() {}
",
r"$0pub(crate) mod bar;
",
);
}
#[test]
fn fix_visibility_of_module_declaration_in_other_file() {
check_assist(
fix_visibility,
r"
//- /main.rs
mod foo;
fn main() { foo::bar$0>::baz(); }
//- /foo.rs
mod bar {
pub fn baz() {}
}
",
r"$0pub(crate) mod bar {
pub fn baz() {}
}
",
);
}
#[test]
fn adds_pub_when_target_is_in_another_crate() {
check_assist(
fix_visibility,
r"
//- /main.rs crate:a deps:foo
foo::Bar$0
//- /lib.rs crate:foo
struct Bar;
",
r"$0pub struct Bar;
",
)
}
#[test]
fn replaces_pub_crate_with_pub() {
check_assist(
fix_visibility,
r"
//- /main.rs crate:a deps:foo
foo::Bar$0
//- /lib.rs crate:foo
pub(crate) struct Bar;
",
r"$0pub struct Bar;
",
);
check_assist(
fix_visibility,
r"
//- /main.rs crate:a deps:foo
fn main() {
foo::Foo { $0bar: () };
}
//- /lib.rs crate:foo
pub struct Foo { pub(crate) bar: () }
",
r"pub struct Foo { $0pub bar: () }
",
);
}
#[test]
fn fix_visibility_of_reexport() {
// FIXME: broken test, this should fix visibility of the re-export
// rather than the struct.
check_assist(
fix_visibility,
r#"
mod foo {
use bar::Baz;
mod bar { pub(super) struct Baz; }
}
foo::Baz$0
"#,
r#"
mod foo {
use bar::Baz;
mod bar { $0pub(crate) struct Baz; }
}
foo::Baz
"#,
)
}
}