use hir::{EditionedFileId, FileRange, HasCrate, HasSource, Semantics}; use ide_db::{RootDatabase, assists::Assist, source_change::SourceChange, text_edit::TextEdit}; use syntax::{ AstNode, TextRange, ast::{HasName, HasVisibility}, }; use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext, fix}; // Diagnostic: private-field // // This diagnostic is triggered if the accessed field is not visible from the current module. pub(crate) fn private_field(ctx: &DiagnosticsContext<'_>, d: &hir::PrivateField) -> Diagnostic { Diagnostic::new_with_syntax_node_ptr( ctx, DiagnosticCode::RustcHardError("E0616"), format!( "field `{}` of `{}` is private", d.field.name(ctx.sema.db).display(ctx.sema.db, ctx.edition), d.field.parent_def(ctx.sema.db).name(ctx.sema.db).display(ctx.sema.db, ctx.edition) ), d.expr.map(|it| it.into()), ) .stable() .with_fixes(field_is_private_fixes( &ctx.sema, d.expr.file_id.original_file(ctx.sema.db), d.field, ctx.sema.original_range(d.expr.to_node(ctx.sema.db).syntax()).range, )) } pub(crate) fn field_is_private_fixes( sema: &Semantics<'_, RootDatabase>, usage_file_id: EditionedFileId, private_field: hir::Field, fix_range: TextRange, ) -> Option> { let def_crate = private_field.krate(sema.db); let usage_crate = sema.file_to_module_def(usage_file_id.file_id(sema.db))?.krate(sema.db); let mut visibility_text = if usage_crate == def_crate { "pub(crate) " } else { "pub " }; let source = private_field.source(sema.db)?; let existing_visibility = match &source.value { hir::FieldSource::Named(it) => it.visibility(), hir::FieldSource::Pos(it) => it.visibility(), }; let range = match existing_visibility { Some(visibility) => { // If there is an existing visibility, don't insert whitespace after. visibility_text = visibility_text.trim_end(); source.with_value(visibility.syntax()).original_file_range_opt(sema.db)?.0 } None => { let (range, _) = source .map(|it| { Some(match it { hir::FieldSource::Named(it) => { it.unsafe_token().or(it.name()?.ident_token())?.text_range() } hir::FieldSource::Pos(it) => it.ty()?.syntax().text_range(), }) }) .transpose()? .original_node_file_range_opt(sema.db)?; FileRange { file_id: range.file_id, range: TextRange::empty(range.range.start()) } } }; let source_change = SourceChange::from_text_edit( range.file_id.file_id(sema.db), TextEdit::replace(range.range, visibility_text.into()), ); Some(vec![fix( "increase_field_visibility", "Increase field visibility", source_change, fix_range, )]) } #[cfg(test)] mod tests { use crate::tests::{check_diagnostics, check_fix}; #[test] fn private_field() { check_diagnostics( r#" mod module { pub struct Struct { field: u32 } } fn main(s: module::Struct) { s.field; //^^^^^^^ 💡 error: field `field` of `Struct` is private } "#, ); } #[test] fn private_tuple_field() { check_diagnostics( r#" mod module { pub struct Struct(u32); } fn main(s: module::Struct) { s.0; //^^^ 💡 error: field `0` of `Struct` is private } "#, ); } #[test] fn private_but_shadowed_in_deref() { check_diagnostics( r#" //- minicore: deref mod module { pub struct Struct { field: Inner } pub struct Inner { pub field: u32 } impl core::ops::Deref for Struct { type Target = Inner; fn deref(&self) -> &Inner { &self.field } } } fn main(s: module::Struct) { s.field; } "#, ); } #[test] fn block_module_madness() { check_diagnostics( r#" fn main() { let strukt = { use crate as ForceParentBlockDefMap; { pub struct Struct { field: (), } Struct { field: () } } }; strukt.field; } "#, ); } #[test] fn block_module_madness2() { check_diagnostics( r#" fn main() { use crate as ForceParentBlockDefMap; let strukt = { use crate as ForceParentBlockDefMap; { pub struct Struct { field: (), } { use crate as ForceParentBlockDefMap; { Struct { field: () } } } } }; strukt.field; } "#, ); } #[test] fn change_visibility_fix() { check_fix( r#" pub mod foo { pub mod bar { pub struct Struct { field: i32, } } } fn foo(v: foo::bar::Struct) { v.field$0; } "#, r#" pub mod foo { pub mod bar { pub struct Struct { pub(crate) field: i32, } } } fn foo(v: foo::bar::Struct) { v.field; } "#, ); } #[test] fn change_visibility_with_existing_visibility() { check_fix( r#" pub mod foo { pub mod bar { pub struct Struct { pub(super) field: i32, } } } fn foo(v: foo::bar::Struct) { v.field$0; } "#, r#" pub mod foo { pub mod bar { pub struct Struct { pub(crate) field: i32, } } } fn foo(v: foo::bar::Struct) { v.field; } "#, ); } #[test] fn change_visibility_of_field_with_doc_comment() { check_fix( r#" pub mod foo { pub struct Foo { /// This is a doc comment bar: u32, } } fn main() { let x = foo::Foo { bar: 0 }; x.bar$0; } "#, r#" pub mod foo { pub struct Foo { /// This is a doc comment pub(crate) bar: u32, } } fn main() { let x = foo::Foo { bar: 0 }; x.bar; } "#, ); } #[test] fn change_visibility_of_field_with_line_comment() { check_fix( r#" pub mod foo { pub struct Foo { // This is a line comment bar: u32, } } fn main() { let x = foo::Foo { bar: 0 }; x.bar$0; } "#, r#" pub mod foo { pub struct Foo { // This is a line comment pub(crate) bar: u32, } } fn main() { let x = foo::Foo { bar: 0 }; x.bar; } "#, ); } #[test] fn change_visibility_of_field_with_multiple_doc_comments() { check_fix( r#" pub mod foo { pub struct Foo { /// First line /// Second line bar: u32, } } fn main() { let x = foo::Foo { bar: 0 }; x.bar$0; } "#, r#" pub mod foo { pub struct Foo { /// First line /// Second line pub(crate) bar: u32, } } fn main() { let x = foo::Foo { bar: 0 }; x.bar; } "#, ); } #[test] fn change_visibility_of_field_with_attr_and_comment() { check_fix( r#" mod foo { pub struct Foo { #[rustfmt::skip] /// First line /// Second line bar: u32, } } fn main() { foo::Foo { $0bar: 42 }; } "#, r#" mod foo { pub struct Foo { #[rustfmt::skip] /// First line /// Second line pub(crate) bar: u32, } } fn main() { foo::Foo { bar: 42 }; } "#, ); } #[test] fn change_visibility_of_field_with_macro() { check_fix( r#" macro_rules! allow_unused { ($vis:vis $struct:ident $name:ident { $($fvis:vis $field:ident : $ty:ty,)* }) => { $vis $struct $name { $( #[allow(unused)] $fvis $field : $ty, )* } }; } mod foo { allow_unused!( pub struct Foo { x: i32, } ); } fn main() { let foo = foo::Foo { x: 2 }; let _ = foo.$0x } "#, r#" macro_rules! allow_unused { ($vis:vis $struct:ident $name:ident { $($fvis:vis $field:ident : $ty:ty,)* }) => { $vis $struct $name { $( #[allow(unused)] $fvis $field : $ty, )* } }; } mod foo { allow_unused!( pub struct Foo { pub(crate) x: i32, } ); } fn main() { let foo = foo::Foo { x: 2 }; let _ = foo.x } "#, ); } }