From 78b3aa227f049df2012b622fefcc8322c8113c0d Mon Sep 17 00:00:00 2001 From: Sean Lynch <42618346+swlynch99@users.noreply.github.com> Date: Mon, 18 Aug 2025 13:00:41 -0700 Subject: [PATCH 1/4] Allow overriding field and struct visibility in substructs Currently substructs and their fields will have their visibility copied from the base struct. This commit adds the ability to override that. --- src/lib.rs | 32 +++++++++++++-- src/substruct.rs | 39 ++++++++++++++++++- tests/ui/fail/base-visibility-override.rs | 6 +++ tests/ui/fail/base-visibility-override.stderr | 5 +++ tests/ui/fail/inaccessible-field.rs | 19 +++++++++ tests/ui/fail/inaccessible-field.stderr | 5 +++ tests/ui/fail/inaccessible-struct.rs | 14 +++++++ tests/ui/fail/inaccessible-struct.stderr | 17 ++++++++ tests/ui/pass/expression-vis.rs | 23 +++++++++++ tests/ui/pass/no-warn-pub-self.rs | 13 +++++++ 10 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 tests/ui/fail/base-visibility-override.rs create mode 100644 tests/ui/fail/base-visibility-override.stderr create mode 100644 tests/ui/fail/inaccessible-field.rs create mode 100644 tests/ui/fail/inaccessible-field.stderr create mode 100644 tests/ui/fail/inaccessible-struct.rs create mode 100644 tests/ui/fail/inaccessible-struct.stderr create mode 100644 tests/ui/pass/expression-vis.rs create mode 100644 tests/ui/pass/no-warn-pub-self.rs diff --git a/src/lib.rs b/src/lib.rs index 7e1c28a..dee66af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,6 +105,32 @@ //! } //! ``` //! +//! # Overriding visibility for emitted structs and fields +//! Normally, `substruct` will copy the visibility definition over from the +//! base struct. However, you can override it by adding a visibility specifier +//! in front of the struct name within the `#[substruct]` attribute. +//! ``` +//! # use substruct::substruct; +//! # +//! // Both A and B are pub, but C is pub(crate). +//! #[substruct(B, pub(crate) C)] +//! pub struct A { +//! // This field is public in structs A and C, but only pub(crate) in C. +//! #[substruct(pub(crate) B, C)] +//! pub field1: u32, +//! +//! // Making a field private can be done by using pub(self) +//! #[substruct(pub(self) C)] +//! pub field2: u32, +//! +//! // You can also override the visibility when using expressions. +//! // The overridden visibility applies to all structs specified in the +//! // expression. +//! #[substruct(pub(crate) any(B, C))] +//! pub field3: u32, +//! } +//! ``` +//! //! # Managing attributes on generated structs //! Sometimes you may want attributes to only apply to some of the emitted //! structs. To do so, you can use the `#[substruct_attr]` macro to only emit @@ -166,7 +192,7 @@ //! } //! ``` //! -//! > The parent struct as always implicitly included in the set of structs +//! > The parent struct is always implicitly included in the set of structs //! > that each field is emitted for. This means that putting `not(A)` in the //! > the struct above would not exclude the field from `A` (and is, in fact, //! > equivalent to `all()`). @@ -190,8 +216,8 @@ //! } //! ``` //! -//! If multiple documentation overrides apply to a single field, then the first -//! one to apply will be used. +//! If multiple overrides apply to a single field, then the first one to apply +//! will be used. //! //! # Generics //! Generics are currently _mostly_ supported. You can use generics with diff --git a/src/substruct.rs b/src/substruct.rs index 89135cf..6ebebab 100644 --- a/src/substruct.rs +++ b/src/substruct.rs @@ -18,6 +18,7 @@ use crate::expr::Expr; /// ``` struct SubstructInputArg { docs: Vec, + vis: syn::Visibility, expr: Expr, } @@ -36,6 +37,7 @@ impl Parse for SubstructInputArg { Ok(Self { docs: attrs, + vis: input.parse()?, expr: input.parse()?, }) } @@ -78,6 +80,7 @@ impl Parse for SubstructAttrInput { struct TopLevelArg { docs: Vec, + vis: syn::Visibility, } struct Emitter<'a> { @@ -106,7 +109,13 @@ impl<'a> Emitter<'a> { .args .into_iter() .filter_map(|arg| match arg.expr { - Expr::Ident(ident) => Some((ident.clone(), TopLevelArg { docs: arg.docs })), + Expr::Ident(ident) => Some(( + ident.clone(), + TopLevelArg { + docs: arg.docs, + vis: arg.vis, + }, + )), expr => { errors.push(syn::Error::new_spanned( expr, @@ -118,7 +127,13 @@ impl<'a> Emitter<'a> { .collect(); if !args.contains_key(&input.ident) { - args.insert(input.ident.clone(), TopLevelArg { docs: Vec::new() }); + args.insert( + input.ident.clone(), + TopLevelArg { + docs: Vec::new(), + vis: syn::Visibility::Inherited, + }, + ); } Ok(Self { @@ -151,6 +166,17 @@ impl<'a> Emitter<'a> { let mut input = self.input.clone(); input.ident = name.clone(); + if !matches!(tla.vis, syn::Visibility::Inherited) { + if *name == self.input.ident { + self.errors.push(syn::Error::new_spanned( + &tla.vis, + "cannot override the visibility of the base struct", + )); + } + + input.vis = tla.vis.clone(); + } + if !tla.docs.is_empty() { input.attrs.retain(|attr| !attr.path().is_ident("doc")); input.attrs.extend_from_slice(&tla.docs); @@ -169,6 +195,10 @@ impl<'a> Emitter<'a> { syn::Data::Union(data) => self.filter_fields_named(&mut data.fields, name), }; + input + .attrs + .push(syn::parse_quote!(#[allow(clippy::needless_pub_self)])); + input.to_tokens(&mut self.tokens); if input.ident != self.input.ident { @@ -333,6 +363,7 @@ impl<'a> Emitter<'a> { substruct.args.push(SubstructInputArg { docs: Vec::new(), + vis: syn::Visibility::Inherited, expr: Expr::Ident(self.input.ident.clone()), }); @@ -343,6 +374,10 @@ impl<'a> Emitter<'a> { self.filter_attrs(&mut field.attrs, name); + if !matches!(arg.vis, syn::Visibility::Inherited) { + field.vis = arg.vis.clone(); + } + if !arg.docs.is_empty() { field.attrs.retain(|attr| !attr.path().is_ident("doc")); field.attrs.extend_from_slice(&arg.docs); diff --git a/tests/ui/fail/base-visibility-override.rs b/tests/ui/fail/base-visibility-override.rs new file mode 100644 index 0000000..202d154 --- /dev/null +++ b/tests/ui/fail/base-visibility-override.rs @@ -0,0 +1,6 @@ +use substruct::substruct; + +#[substruct(pub(crate) Test)] +pub struct Test {} + +fn main() {} diff --git a/tests/ui/fail/base-visibility-override.stderr b/tests/ui/fail/base-visibility-override.stderr new file mode 100644 index 0000000..aa356b3 --- /dev/null +++ b/tests/ui/fail/base-visibility-override.stderr @@ -0,0 +1,5 @@ +error: cannot override the visibility of the base struct + --> tests/ui/fail/base-visibility-override.rs:3:13 + | +3 | #[substruct(pub(crate) Test)] + | ^^^^^^^^^^ diff --git a/tests/ui/fail/inaccessible-field.rs b/tests/ui/fail/inaccessible-field.rs new file mode 100644 index 0000000..c11b59a --- /dev/null +++ b/tests/ui/fail/inaccessible-field.rs @@ -0,0 +1,19 @@ + +mod inner { + use substruct::substruct; + + #[substruct(A)] + #[derive(Default)] + pub struct Test { + #[substruct(pub(self) A)] + pub field1: u32, + pub field2: u32, + } +} + +fn main() { + use self::inner::*; + + let value = A::default(); + let _field = value.field1; +} diff --git a/tests/ui/fail/inaccessible-field.stderr b/tests/ui/fail/inaccessible-field.stderr new file mode 100644 index 0000000..4c83cd6 --- /dev/null +++ b/tests/ui/fail/inaccessible-field.stderr @@ -0,0 +1,5 @@ +error[E0616]: field `field1` of struct `inner::A` is private + --> tests/ui/fail/inaccessible-field.rs:18:24 + | +18 | let _field = value.field1; + | ^^^^^^ private field diff --git a/tests/ui/fail/inaccessible-struct.rs b/tests/ui/fail/inaccessible-struct.rs new file mode 100644 index 0000000..1daf99f --- /dev/null +++ b/tests/ui/fail/inaccessible-struct.rs @@ -0,0 +1,14 @@ +mod inner { + use substruct::substruct; + + #[substruct(pub(self) A)] + #[derive(Default)] + pub struct Test { + #[substruct(A)] + pub field: u32, + } +} + +fn main() { + let _ = crate::inner::A::default(); +} diff --git a/tests/ui/fail/inaccessible-struct.stderr b/tests/ui/fail/inaccessible-struct.stderr new file mode 100644 index 0000000..0531998 --- /dev/null +++ b/tests/ui/fail/inaccessible-struct.stderr @@ -0,0 +1,17 @@ +error[E0603]: struct `A` is private + --> tests/ui/fail/inaccessible-struct.rs:13:27 + | +13 | let _ = crate::inner::A::default(); + | ^ private struct + | +note: the struct `A` is defined here + --> tests/ui/fail/inaccessible-struct.rs:4:17 + | + 4 | #[substruct(pub(self) A)] + | _________________^ + 5 | | #[derive(Default)] + 6 | | pub struct Test { + 7 | | #[substruct(A)] + 8 | | pub field: u32, + 9 | | } + | |_____^ diff --git a/tests/ui/pass/expression-vis.rs b/tests/ui/pass/expression-vis.rs new file mode 100644 index 0000000..78cdb93 --- /dev/null +++ b/tests/ui/pass/expression-vis.rs @@ -0,0 +1,23 @@ + + +mod inner { + use substruct::substruct; + + #[substruct(A, B)] + #[derive(Default)] + pub struct Test + { + #[substruct(pub any(A, B))] + field: u32, + } +} + +fn main() { + use crate::inner::*; + + let a = A::default(); + let b = B::default(); + + let _ = a.field; + let _ = b.field; +} diff --git a/tests/ui/pass/no-warn-pub-self.rs b/tests/ui/pass/no-warn-pub-self.rs new file mode 100644 index 0000000..9e9b236 --- /dev/null +++ b/tests/ui/pass/no-warn-pub-self.rs @@ -0,0 +1,13 @@ +#![allow(dead_code)] +#![deny(clippy::needless_pub_self)] + +use substruct::substruct; + +#[substruct(pub(self) A)] +pub struct Test { + #[substruct(A)] + pub field: u32 +} + + +fn main() {} \ No newline at end of file From 1c46a7cfcdd7781bab1f5d3efab1192ee19901d9 Mon Sep 17 00:00:00 2001 From: Sean Lynch <42618346+swlynch99@users.noreply.github.com> Date: Mon, 18 Aug 2025 13:08:37 -0700 Subject: [PATCH 2/4] formatting --- tests/ui/fail/inaccessible-field.rs | 1 - tests/ui/fail/multiple-attr-on-field.rs | 1 - tests/ui/fail/multiple-errors.rs | 2 +- tests/ui/pass/expression-vis.rs | 5 +---- tests/ui/pass/no-warn-pub-self.rs | 5 ++--- 5 files changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/ui/fail/inaccessible-field.rs b/tests/ui/fail/inaccessible-field.rs index c11b59a..5c47fd3 100644 --- a/tests/ui/fail/inaccessible-field.rs +++ b/tests/ui/fail/inaccessible-field.rs @@ -1,4 +1,3 @@ - mod inner { use substruct::substruct; diff --git a/tests/ui/fail/multiple-attr-on-field.rs b/tests/ui/fail/multiple-attr-on-field.rs index e816e9e..2c86bc6 100644 --- a/tests/ui/fail/multiple-attr-on-field.rs +++ b/tests/ui/fail/multiple-attr-on-field.rs @@ -5,7 +5,6 @@ struct A { #[substruct(B)] #[substruct(C)] field: u32, - } fn main() {} diff --git a/tests/ui/fail/multiple-errors.rs b/tests/ui/fail/multiple-errors.rs index 35c7c44..07b8155 100644 --- a/tests/ui/fail/multiple-errors.rs +++ b/tests/ui/fail/multiple-errors.rs @@ -4,7 +4,7 @@ use substruct::substruct; pub struct A { #[substruct_attr(blah, blah, blah)] #[substruct_attr(blah, bleh, blah)] - pub x: u32 + pub x: u32, } fn main() {} diff --git a/tests/ui/pass/expression-vis.rs b/tests/ui/pass/expression-vis.rs index 78cdb93..3b8df8c 100644 --- a/tests/ui/pass/expression-vis.rs +++ b/tests/ui/pass/expression-vis.rs @@ -1,12 +1,9 @@ - - mod inner { use substruct::substruct; #[substruct(A, B)] #[derive(Default)] - pub struct Test - { + pub struct Test { #[substruct(pub any(A, B))] field: u32, } diff --git a/tests/ui/pass/no-warn-pub-self.rs b/tests/ui/pass/no-warn-pub-self.rs index 9e9b236..b929bf5 100644 --- a/tests/ui/pass/no-warn-pub-self.rs +++ b/tests/ui/pass/no-warn-pub-self.rs @@ -6,8 +6,7 @@ use substruct::substruct; #[substruct(pub(self) A)] pub struct Test { #[substruct(A)] - pub field: u32 + pub field: u32, } - -fn main() {} \ No newline at end of file +fn main() {} From 535c6ae71a3ead787c228eb009c0412b2e4c3434 Mon Sep 17 00:00:00 2001 From: Sean Lynch <42618346+swlynch99@users.noreply.github.com> Date: Mon, 18 Aug 2025 13:11:01 -0700 Subject: [PATCH 3/4] Fix test line numbers --- tests/ui/fail/inaccessible-field.stderr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ui/fail/inaccessible-field.stderr b/tests/ui/fail/inaccessible-field.stderr index 4c83cd6..c34e86a 100644 --- a/tests/ui/fail/inaccessible-field.stderr +++ b/tests/ui/fail/inaccessible-field.stderr @@ -1,5 +1,5 @@ error[E0616]: field `field1` of struct `inner::A` is private - --> tests/ui/fail/inaccessible-field.rs:18:24 + --> tests/ui/fail/inaccessible-field.rs:17:24 | -18 | let _field = value.field1; +17 | let _field = value.field1; | ^^^^^^ private field From 54e8fb8ca49151b87ae78e20a568ee536453776d Mon Sep 17 00:00:00 2001 From: Sean Lynch <42618346+swlynch99@users.noreply.github.com> Date: Mon, 18 Aug 2025 13:15:32 -0700 Subject: [PATCH 4/4] Update test cases for stable rustc --- tests/ui/fail/inaccessible-struct.stderr | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/ui/fail/inaccessible-struct.stderr b/tests/ui/fail/inaccessible-struct.stderr index 0531998..168f898 100644 --- a/tests/ui/fail/inaccessible-struct.stderr +++ b/tests/ui/fail/inaccessible-struct.stderr @@ -7,11 +7,11 @@ error[E0603]: struct `A` is private note: the struct `A` is defined here --> tests/ui/fail/inaccessible-struct.rs:4:17 | - 4 | #[substruct(pub(self) A)] +4 | #[substruct(pub(self) A)] | _________________^ - 5 | | #[derive(Default)] - 6 | | pub struct Test { - 7 | | #[substruct(A)] - 8 | | pub field: u32, - 9 | | } +5 | | #[derive(Default)] +6 | | pub struct Test { +7 | | #[substruct(A)] +8 | | pub field: u32, +9 | | } | |_____^