From 9500c24bad4454e02484f6e21860ade84166c7e2 Mon Sep 17 00:00:00 2001 From: TheLazyDutchman Date: Fri, 15 Dec 2023 20:40:17 +0100 Subject: [PATCH 01/10] Bless tests to be updated to the new compiler version --- tests/fail/01-general.stderr | 13 ++++++++++--- tests/fail/02-nesting.stderr | 13 ++++++++++--- tests/fail/03-private-by-default.stderr | 5 ++++- tests/fail/04-no-full-pub.stderr | 2 +- tests/fail/06-wrong-argument.stderr | 12 ++++++------ 5 files changed, 31 insertions(+), 14 deletions(-) diff --git a/tests/fail/01-general.stderr b/tests/fail/01-general.stderr index 1cdb7ae..c728d9d 100644 --- a/tests/fail/01-general.stderr +++ b/tests/fail/01-general.stderr @@ -1,14 +1,21 @@ error[E0277]: the trait bound `C: Sealed` is not satisfied - --> tests/fail/01-general.rs:20:6 + --> tests/fail/01-general.rs:20:12 | 20 | impl T for C {} - | ^ the trait `Sealed` is not implemented for `C` + | ^ the trait `Sealed` is not implemented for `C` | + = help: the following other types implement trait `Sealed`: + A + B note: required by a bound in `T` --> tests/fail/01-general.rs:11:1 | 11 | #[sealed] | ^^^^^^^^^ required by this bound in `T` 12 | trait T {} - | - required by a bound in this + | - required by a bound in this trait + = note: `T` is a "sealed trait", because to implement it you also need to implement `__seal_t::Sealed`, which is not accessible; this is usually done to force you to use one of the provided types that already implement it + = help: the following types implement the trait: + A + B = note: this error originates in the attribute macro `sealed` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/fail/02-nesting.stderr b/tests/fail/02-nesting.stderr index 64f6be7..1910c7f 100644 --- a/tests/fail/02-nesting.stderr +++ b/tests/fail/02-nesting.stderr @@ -1,14 +1,21 @@ error[E0277]: the trait bound `C: Sealed` is not satisfied - --> tests/fail/02-nesting.rs:26:6 + --> tests/fail/02-nesting.rs:26:42 | 26 | impl lets::attempt::some::nesting::T for C {} - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Sealed` is not implemented for `C` + | ^ the trait `Sealed` is not implemented for `C` | + = help: the following other types implement trait `Sealed`: + A + B note: required by a bound in `T` --> tests/fail/02-nesting.rs:8:17 | 8 | #[sealed(pub(crate))] | ^^^^^^^^^^^^^^^^^^^^^ required by this bound in `T` 9 | pub trait T {} - | - required by a bound in this + | - required by a bound in this trait + = note: `T` is a "sealed trait", because to implement it you also need to implement `lets::attempt::some::nesting::__seal_t::Sealed`, which is not accessible; this is usually done to force you to use one of the provided types that already implement it + = help: the following types implement the trait: + A + B = note: this error originates in the attribute macro `sealed` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/fail/03-private-by-default.stderr b/tests/fail/03-private-by-default.stderr index 42a59ca..0dfbe5a 100644 --- a/tests/fail/03-private-by-default.stderr +++ b/tests/fail/03-private-by-default.stderr @@ -2,7 +2,10 @@ error[E0603]: module `__seal_t` is private --> tests/fail/03-private-by-default.rs:17:1 | 17 | #[sealed] - | ^^^^^^^^^ private module + | ^^^^^^^^^ + | | + | private module + | trait `Sealed` is not publicly re-exported | note: the module `__seal_t` is defined here --> tests/fail/03-private-by-default.rs:8:17 diff --git a/tests/fail/04-no-full-pub.stderr b/tests/fail/04-no-full-pub.stderr index 34115fd..2b4a660 100644 --- a/tests/fail/04-no-full-pub.stderr +++ b/tests/fail/04-no-full-pub.stderr @@ -1,5 +1,5 @@ error: `pub` visibility breaks the seal as allows to use it outside its crate. -Consider tightening the visibility (e.g. `pub(crate)`) if you actually need sealing. + Consider tightening the visibility (e.g. `pub(crate)`) if you actually need sealing. --> tests/fail/04-no-full-pub.rs:8:26 | 8 | #[sealed(pub)] diff --git a/tests/fail/06-wrong-argument.stderr b/tests/fail/06-wrong-argument.stderr index 6d0e7c5..b36a538 100644 --- a/tests/fail/06-wrong-argument.stderr +++ b/tests/fail/06-wrong-argument.stderr @@ -4,6 +4,12 @@ error: unknown `erased` attribute argument 3 | #[sealed(erased)] | ^^^^^^ +error[E0405]: cannot find trait `T` in this scope + --> tests/fail/06-wrong-argument.rs:9:6 + | +9 | impl T for A {} + | ^ not found in this scope + error[E0433]: failed to resolve: use of undeclared crate or module `__seal_t` --> tests/fail/06-wrong-argument.rs:8:1 | @@ -11,9 +17,3 @@ error[E0433]: failed to resolve: use of undeclared crate or module `__seal_t` | ^^^^^^^^^ use of undeclared crate or module `__seal_t` | = note: this error originates in the attribute macro `sealed` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0405]: cannot find trait `T` in this scope - --> tests/fail/06-wrong-argument.rs:9:6 - | -9 | impl T for A {} - | ^ not found in this scope From fa5689374d45bc316b67b9c3b8c0916fa564df04 Mon Sep 17 00:00:00 2001 From: TheLazyDutchman Date: Fri, 15 Dec 2023 22:16:37 +0100 Subject: [PATCH 02/10] Added the ability to seal a function (in the definition) --- examples/partial.rs | 30 ++++ src/lib.rs | 403 +++++++++++++++++++++++++++++--------------- 2 files changed, 295 insertions(+), 138 deletions(-) create mode 100644 examples/partial.rs diff --git a/examples/partial.rs b/examples/partial.rs new file mode 100644 index 0000000..771a1e2 --- /dev/null +++ b/examples/partial.rs @@ -0,0 +1,30 @@ +use sealed::sealed; + +#[sealed] +pub trait A { + #[seal(callable)] + fn has_default( + Email(_email): Email, + User { + name: _name, + email: Email(_email2), + age: _age, + }: User, + ) { + } +} + +pub struct Email(String); + +pub struct User { + name: String, + email: Email, + age: u8, +} + +pub struct AImpl; + +#[sealed] +impl A for AImpl {} + +fn main() {} diff --git a/src/lib.rs b/src/lib.rs index 169ce7f..691baaa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,171 +124,298 @@ use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{format_ident, quote}; use syn::{ - ext::IdentExt, - parse::{Parse, ParseStream}, - parse_macro_input, parse_quote, - spanned::Spanned, - token, + ext::IdentExt, + parse::{Parse, ParseStream}, + parse_macro_input, parse_quote, + spanned::Spanned, + token, }; #[proc_macro_attribute] pub fn sealed(args: TokenStream, input: TokenStream) -> TokenStream { - match parse_macro_input!(input) { - syn::Item::Impl(item_impl) => parse_sealed_impl(&item_impl), - syn::Item::Trait(item_trait) => { - Ok(parse_sealed_trait(item_trait, parse_macro_input!(args))) - } - _ => Err(syn::Error::new(Span::call_site(), "expected impl or trait")), - } - .unwrap_or_else(|e| e.to_compile_error()) - .into() + match parse_macro_input!(input) { + syn::Item::Impl(item_impl) => parse_sealed_impl(&item_impl), + syn::Item::Trait(item_trait) => { + Ok(parse_sealed_trait(item_trait, parse_macro_input!(args))) + } + _ => Err(syn::Error::new(Span::call_site(), "expected impl or trait")), + } + .unwrap_or_else(|e| e.to_compile_error()) + .into() } // Care for https://gist.github.com/Kestrer/8c05ebd4e0e9347eb05f265dfb7252e1#procedural-macros-support-renaming-the-crate fn parse_sealed_trait(mut item_trait: syn::ItemTrait, args: TraitArguments) -> TokenStream2 { - let trait_ident = &item_trait.ident.unraw(); - let trait_generics = &item_trait.generics; - let seal = seal_name(trait_ident); - let vis = &args.visibility; - - let (_, ty_generics, where_clause) = trait_generics.split_for_impl(); - item_trait - .supertraits - .push(parse_quote!( #seal::Sealed #ty_generics )); - - let mod_code = if args.erased { - let lifetimes = trait_generics.lifetimes(); - let const_params = trait_generics.const_params(); - let type_params = - trait_generics - .type_params() - .map(|syn::TypeParam { ident, .. }| -> syn::TypeParam { - parse_quote!( #ident : ?Sized ) - }); - - quote! { - pub trait Sealed< #(#lifetimes ,)* #(#type_params ,)* #(#const_params ,)* > {} - } - } else { - // `trait_generics` does not output its where clause when tokenized (due - // to supertraits in the middle). So we output them separately. - quote! { - use super::*; - pub trait Sealed #trait_generics #where_clause {} - } - }; - - quote! { - #[automatically_derived] - #vis mod #seal { - #mod_code - } - #item_trait - } + let trait_ident = &item_trait.ident.unraw(); + let trait_generics = &item_trait.generics; + let seal = seal_name(trait_ident); + let vis = &args.visibility; + + let (_, ty_generics, where_clause) = trait_generics.split_for_impl(); + item_trait + .supertraits + .push(parse_quote!( #seal::Sealed #ty_generics )); + + let mut wrappers = Vec::::new(); + + for item in &mut item_trait.items { + if let syn::TraitItem::Fn(item) = item { + match parse_function_definition(trait_ident.clone(), item) { + Ok(Some(wrapper)) => wrappers.push(wrapper.into()), + Ok(None) => {} + Err(err) => { + return err + .into_compile_error() + .into() + } + } + } + } + + item_trait + .items + .extend(wrappers); + + let mod_code = if args.erased { + let lifetimes = trait_generics.lifetimes(); + let const_params = trait_generics.const_params(); + let type_params = trait_generics + .type_params() + .map(|syn::TypeParam { ident, .. }| -> syn::TypeParam { + parse_quote!( #ident : ?Sized ) + }); + + // Token here is used as an argument to a function in order to seal it. + quote! { + pub trait Sealed< #(#lifetimes ,)* #(#type_params ,)* #(#const_params ,)* > {} + pub struct Token; + } + } else { + // `trait_generics` does not output its where clause when tokenized (due + // to supertraits in the middle). So we output them separately. + quote! { + use super::*; + pub trait Sealed #trait_generics #where_clause {} + pub struct Token; + } + }; + + quote! { + #[automatically_derived] + #vis mod #seal { + #mod_code + } + #item_trait + } } fn parse_sealed_impl(item_impl: &syn::ItemImpl) -> syn::Result { - let impl_trait = item_impl - .trait_ - .as_ref() - .ok_or_else(|| syn::Error::new_spanned(item_impl, "missing implementation trait"))?; - - let mut sealed_path = impl_trait.1.segments.clone(); - - // since `impl for ...` is not allowed, this path will *always* have at least length 1 - // thus both `first` and `last` are safe to unwrap - let syn::PathSegment { ident, arguments } = sealed_path.pop().unwrap().into_value(); - let seal = seal_name(ident.unraw()); - sealed_path.push(parse_quote!( #seal )); - sealed_path.push(parse_quote!(Sealed)); - - let self_type = &item_impl.self_ty; - - // Only keep the introduced params (no bounds), since - // the bounds may break in the `#seal` submodule. - let (trait_generics, _, where_clauses) = item_impl.generics.split_for_impl(); - - Ok(quote! { - #[automatically_derived] - impl #trait_generics #sealed_path #arguments for #self_type #where_clauses {} - #item_impl - }) + let impl_trait = item_impl + .trait_ + .as_ref() + .ok_or_else(|| syn::Error::new_spanned(item_impl, "missing implementation trait"))?; + + let mut sealed_path = impl_trait.1.segments.clone(); + + // since `impl for ...` is not allowed, this path will *always* have at least length 1 + // thus both `first` and `last` are safe to unwrap + let syn::PathSegment { ident, arguments } = sealed_path + .pop() + .unwrap() + .into_value(); + let seal = seal_name(ident.unraw()); + sealed_path.push(parse_quote!( #seal )); + sealed_path.push(parse_quote!(Sealed)); + + let self_type = &item_impl.self_ty; + + // Only keep the introduced params (no bounds), since + // the bounds may break in the `#seal` submodule. + let (trait_generics, _, where_clauses) = item_impl + .generics + .split_for_impl(); + + Ok(quote! { + #[automatically_derived] + impl #trait_generics #sealed_path #arguments for #self_type #where_clauses {} + #item_impl + }) } /// Constructs [`syn::Ident`] of a sealing module name. fn seal_name(seal: D) -> syn::Ident { - format_ident!("__seal_{}", &seal.to_string().to_snake_case()) + format_ident!( + "__seal_{}", + &seal + .to_string() + .to_snake_case() + ) +} + +/// Constructs [`syn::Ident`] of a function wrapped by another function +fn seal_function_name(seal: D) -> syn::Ident { + format_ident!("_{}", &seal.to_string()) +} + +/// Seal a specific function (to avoid people overwriting behaviour of +/// specific functions, or even calling them) +fn parse_function_definition( + trait_ident: syn::Ident, + function: &mut syn::TraitItemFn, +) -> Result, syn::Error> { + let Some(attr) = function + .attrs + .iter() + .find(|attr| attr.path().is_ident("seal")) + .cloned() + else { + return Ok(None); + }; + + function + .attrs + .retain(|x| x != &attr); + + let wrapper_sig = function.sig.clone(); + let seal = seal_name(trait_ident.clone()); + + function + .sig + .inputs + .push(parse_quote!(_token: #seal::Token)); + + let syn::Meta::List(list) = attr.meta else { + return Ok(None); + }; + + let input = syn::parse::(list.tokens.into())?; + + let mut value = None; + + if input.callable { + function.sig.ident = seal_function_name(function.sig.ident.clone()); + let inner_name = &function.sig.ident; + + let args = wrapper_sig + .inputs + .iter() + .map(|input| match input { + syn::FnArg::Receiver(_) => quote!(self), + syn::FnArg::Typed(input) => { + let pat = &input.pat; + quote!(#pat) + } + }); + + value = Some(parse_quote!(#wrapper_sig { + ::#inner_name(#(#args,)* #seal::Token) + })); + } + Ok(value) } /// Arguments accepted by `#[sealed]` attribute when placed on a trait /// definition. struct TraitArguments { - /// `erase` argument indicating whether trait bounds erasure should be used. - /// - /// Default is `false`. - erased: bool, - - /// `pub` argument defining visibility of the generated sealing module. - /// - /// Default is [`syn::Visibility::Inherited`]. - visibility: syn::Visibility, + /// `erase` argument indicating whether trait bounds erasure should be used. + /// + /// Default is `false`. + erased: bool, + + /// `pub` argument defining visibility of the generated sealing module. + /// + /// Default is [`syn::Visibility::Inherited`]. + visibility: syn::Visibility, } impl Default for TraitArguments { - fn default() -> Self { - Self { - erased: false, - visibility: syn::Visibility::Inherited, - } - } + fn default() -> Self { + Self { + erased: false, + visibility: syn::Visibility::Inherited, + } + } } impl Parse for TraitArguments { - fn parse(input: ParseStream<'_>) -> syn::Result { - let mut out = Self::default(); - - while !input.is_empty() { - let ident = syn::Ident::parse_any(&input.fork())?; - - match ident.to_string().as_str() { - "erase" => { - syn::Ident::parse_any(input)?; - out.erased = true; - } - - "pub" => { - out.visibility = input.parse()?; - if matches!(out.visibility, syn::Visibility::Public(_)) { - return Err(syn::Error::new( - out.visibility.span(), - "`pub` visibility breaks the seal as allows to use \ + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut out = Self::default(); + + while !input.is_empty() { + let ident = syn::Ident::parse_any(&input.fork())?; + + match ident.to_string().as_str() { + "erase" => { + syn::Ident::parse_any(input)?; + out.erased = true; + } + + "pub" => { + out.visibility = input.parse()?; + if matches!(out.visibility, syn::Visibility::Public(_)) { + return Err(syn::Error::new( + out.visibility.span(), + "`pub` visibility breaks the seal as allows to use \ it outside its crate.\n\ Consider tightening the visibility (e.g. \ `pub(crate)`) if you actually need sealing.", - )); - } - } - - unknown => { - return Err(syn::Error::new( - ident.span(), - format!("unknown `{}` attribute argument", unknown), - )) - } - } - - if input - .lookahead1() - .peek(token::Comma) - .then(|| input.parse::()) - .transpose()? - .is_none() - && !input.is_empty() - { - return Err(syn::Error::new(ident.span(), "expected followed by `,`")); - } - } - - Ok(out) - } + )); + } + } + + unknown => { + return Err(syn::Error::new( + ident.span(), + format!("unknown `{}` attribute argument", unknown), + )) + } + } + + if input + .lookahead1() + .peek(token::Comma) + .then(|| input.parse::()) + .transpose()? + .is_none() && !input.is_empty() + { + return Err(syn::Error::new(ident.span(), "expected followed by `,`")); + } + } + + Ok(out) + } +} + +/// Arguments accepted by `#[seal]` attribute when placed on functions in a (partially) sealed trait +struct FunctionArguments { + /// Determines whether the sealed function is wrapped by a public function that is callable + callable: bool, +} + +impl Default for FunctionArguments { + fn default() -> Self { + FunctionArguments { callable: false } + } +} + +impl Parse for FunctionArguments { + fn parse(input: ParseStream) -> syn::Result { + let mut out = FunctionArguments::default(); + let ident = syn::Ident::parse(&input.fork())?; + + match ident.to_string().as_str() { + "callable" => { + syn::Ident::parse(input)?; + out.callable = true + } + unknown => { + return Err(syn::Error::new( + ident.span(), + format!("unknown `{}` attribute argument", unknown), + )) + } + } + + Ok(out) + } } From 2a6a22d7a6f64fa7dbf31c6cc9508615e73c5726 Mon Sep 17 00:00:00 2001 From: TheLazyDutchman Date: Fri, 15 Dec 2023 22:40:04 +0100 Subject: [PATCH 03/10] Added sealing of specific functions in the implementation --- examples/partial.rs | 16 ++++++++++-- src/lib.rs | 59 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/examples/partial.rs b/examples/partial.rs index 771a1e2..e1e218d 100644 --- a/examples/partial.rs +++ b/examples/partial.rs @@ -22,9 +22,21 @@ pub struct User { age: u8, } -pub struct AImpl; +pub struct Impl; #[sealed] -impl A for AImpl {} +impl A for Impl {} + +#[sealed] +pub trait B { + #[seal(callable)] + fn no_default(email: Email); +} + +#[sealed] +impl B for Impl { + #[seal(callable)] + fn no_default(_email: Email) {} +} fn main() {} diff --git a/src/lib.rs b/src/lib.rs index 691baaa..f85bde2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -134,7 +134,7 @@ use syn::{ #[proc_macro_attribute] pub fn sealed(args: TokenStream, input: TokenStream) -> TokenStream { match parse_macro_input!(input) { - syn::Item::Impl(item_impl) => parse_sealed_impl(&item_impl), + syn::Item::Impl(mut item_impl) => parse_sealed_impl(&mut item_impl), syn::Item::Trait(item_trait) => { Ok(parse_sealed_trait(item_trait, parse_macro_input!(args))) } @@ -160,7 +160,7 @@ fn parse_sealed_trait(mut item_trait: syn::ItemTrait, args: TraitArguments) -> T for item in &mut item_trait.items { if let syn::TraitItem::Fn(item) = item { - match parse_function_definition(trait_ident.clone(), item) { + match parse_function_definition(trait_ident, item) { Ok(Some(wrapper)) => wrappers.push(wrapper.into()), Ok(None) => {} Err(err) => { @@ -209,11 +209,13 @@ fn parse_sealed_trait(mut item_trait: syn::ItemTrait, args: TraitArguments) -> T } } -fn parse_sealed_impl(item_impl: &syn::ItemImpl) -> syn::Result { +fn parse_sealed_impl(item_impl: &mut syn::ItemImpl) -> syn::Result { let impl_trait = item_impl .trait_ .as_ref() - .ok_or_else(|| syn::Error::new_spanned(item_impl, "missing implementation trait"))?; + .ok_or_else(|| { + syn::Error::new_spanned(item_impl.clone(), "missing implementation trait") + })?; let mut sealed_path = impl_trait.1.segments.clone(); @@ -235,6 +237,12 @@ fn parse_sealed_impl(item_impl: &syn::ItemImpl) -> syn::Result { .generics .split_for_impl(); + for item in &mut item_impl.items { + if let syn::ImplItem::Fn(item) = item { + parse_function_implementation(&ident, item)?; + } + } + Ok(quote! { #[automatically_derived] impl #trait_generics #sealed_path #arguments for #self_type #where_clauses {} @@ -260,7 +268,7 @@ fn seal_function_name(seal: D) -> syn::Ident { /// Seal a specific function (to avoid people overwriting behaviour of /// specific functions, or even calling them) fn parse_function_definition( - trait_ident: syn::Ident, + trait_ident: &syn::Ident, function: &mut syn::TraitItemFn, ) -> Result, syn::Error> { let Some(attr) = function @@ -277,7 +285,7 @@ fn parse_function_definition( .retain(|x| x != &attr); let wrapper_sig = function.sig.clone(); - let seal = seal_name(trait_ident.clone()); + let seal = seal_name(trait_ident); function .sig @@ -293,7 +301,7 @@ fn parse_function_definition( let mut value = None; if input.callable { - function.sig.ident = seal_function_name(function.sig.ident.clone()); + function.sig.ident = seal_function_name(&function.sig.ident); let inner_name = &function.sig.ident; let args = wrapper_sig @@ -314,6 +322,43 @@ fn parse_function_definition( Ok(value) } +fn parse_function_implementation( + trait_ident: &syn::Ident, + function: &mut syn::ImplItemFn, +) -> Result<(), syn::Error> { + let Some(attr) = function + .attrs + .iter() + .find(|attr| attr.path().is_ident("seal")) + .cloned() + else { + return Ok(()); + }; + + function + .attrs + .retain(|x| x != &attr); + + let seal = seal_name(trait_ident); + + function + .sig + .inputs + .push(parse_quote!(_token: #seal::Token)); + + let syn::Meta::List(list) = attr.meta else { + return Ok(()); + }; + + let input = syn::parse::(list.tokens.into())?; + + if input.callable { + function.sig.ident = seal_function_name(&function.sig.ident); + } + + Ok(()) +} + /// Arguments accepted by `#[sealed]` attribute when placed on a trait /// definition. struct TraitArguments { From a2c661c437ffd06c3f5f39e039cb05287bf2e236 Mon Sep 17 00:00:00 2001 From: TheLazyDutchman Date: Fri, 15 Dec 2023 23:00:02 +0100 Subject: [PATCH 04/10] Added tests for sealing functions --- tests/fail/07-sealed-function.rs | 29 ++++++++++++++++++++++++++ tests/fail/07-sealed-function.stderr | 28 +++++++++++++++++++++++++ tests/pass/14-partial-seal.rs | 31 ++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 tests/fail/07-sealed-function.rs create mode 100644 tests/fail/07-sealed-function.stderr create mode 100644 tests/pass/14-partial-seal.rs diff --git a/tests/fail/07-sealed-function.rs b/tests/fail/07-sealed-function.rs new file mode 100644 index 0000000..6e389e4 --- /dev/null +++ b/tests/fail/07-sealed-function.rs @@ -0,0 +1,29 @@ +pub mod inner { + use sealed::sealed; + + #[sealed] + pub trait PartialSealed { + #[seal] + fn a(); + + fn b(); + } + + pub struct A; + + #[sealed] + impl PartialSealed for A { + #[seal] + fn a() {} + + fn b() {} + } +} + +use crate::inner::PartialSealed; + +fn main() { + inner::A::a(); + inner::A::a(Token); + inner::A::b(); +} diff --git a/tests/fail/07-sealed-function.stderr b/tests/fail/07-sealed-function.stderr new file mode 100644 index 0000000..c6b993e --- /dev/null +++ b/tests/fail/07-sealed-function.stderr @@ -0,0 +1,28 @@ +error[E0425]: cannot find value `Token` in this scope + --> tests/fail/07-sealed-function.rs:27:14 + | +27 | inner::A::a(Token); + | ^^^^^ not found in this scope + | +note: unit struct `crate::inner::__seal_partial_sealed::Token` exists but is inaccessible + --> tests/fail/07-sealed-function.rs:4:2 + | +4 | #[sealed] + | ^^^^^^^^^ not accessible + = note: this error originates in the attribute macro `sealed` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0061]: this function takes 1 argument but 0 arguments were supplied + --> tests/fail/07-sealed-function.rs:26:2 + | +26 | inner::A::a(); + | ^^^^^^^^^^^-- an argument of type `Token` is missing + | +note: associated function defined here + --> tests/fail/07-sealed-function.rs:7:6 + | +7 | fn a(); + | ^ +help: provide the argument + | +26 | inner::A::a(/* Token */); + | ~~~~~~~~~~~~~ diff --git a/tests/pass/14-partial-seal.rs b/tests/pass/14-partial-seal.rs new file mode 100644 index 0000000..5381fa9 --- /dev/null +++ b/tests/pass/14-partial-seal.rs @@ -0,0 +1,31 @@ +use sealed::sealed; + +#[sealed] +pub trait PartialSealed { + #[seal] + fn default(_value: String) {} + + #[seal] + fn no_default(value: u8); +} + +pub struct A; + +#[sealed] +impl PartialSealed for A { + #[seal] + fn no_default(_value: u8) {} +} + +pub struct B; + +#[sealed] +impl PartialSealed for B { + #[seal] + fn default(_value: String) {} + + #[seal] + fn no_default(_value: u8) {} +} + +fn main() {} From 0ecbf426af0dd800f135ae4b2c0058ac5242274b Mon Sep 17 00:00:00 2001 From: TheLazyDutchman Date: Sat, 16 Dec 2023 14:42:40 +0100 Subject: [PATCH 05/10] Added partial to trait attr --- examples/partial.rs | 17 ++- src/lib.rs | 115 +++++++++++------- tests/fail/08-partial-seal.rs | 9 ++ tests/fail/08-partial-seal.stderr | 5 + ...partial-seal.rs => 14-sealed-functions.rs} | 0 tests/pass/15-partial-seal.rs | 11 ++ 6 files changed, 113 insertions(+), 44 deletions(-) create mode 100644 tests/fail/08-partial-seal.rs create mode 100644 tests/fail/08-partial-seal.stderr rename tests/pass/{14-partial-seal.rs => 14-sealed-functions.rs} (100%) create mode 100644 tests/pass/15-partial-seal.rs diff --git a/examples/partial.rs b/examples/partial.rs index e1e218d..be02773 100644 --- a/examples/partial.rs +++ b/examples/partial.rs @@ -1,6 +1,6 @@ use sealed::sealed; -#[sealed] +#[sealed(partial)] pub trait A { #[seal(callable)] fn has_default( @@ -27,7 +27,7 @@ pub struct Impl; #[sealed] impl A for Impl {} -#[sealed] +#[sealed(partial)] pub trait B { #[seal(callable)] fn no_default(email: Email); @@ -39,4 +39,17 @@ impl B for Impl { fn no_default(_email: Email) {} } +#[sealed] +pub trait NoPartial { + #[seal] + fn has_default() {} + + fn no_default(); +} + +#[sealed] +impl NoPartial for Impl { + fn no_default() {} +} + fn main() {} diff --git a/src/lib.rs b/src/lib.rs index f85bde2..ef0e1bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -160,7 +160,35 @@ fn parse_sealed_trait(mut item_trait: syn::ItemTrait, args: TraitArguments) -> T for item in &mut item_trait.items { if let syn::TraitItem::Fn(item) = item { - match parse_function_definition(trait_ident, item) { + let input = match parse_function_arguments(&item.attrs) { + Ok(input) => input, + Err(err) => { + return err + .into_compile_error() + .into() + } + }; + + let Some((attr, input)) = input else { + continue; + }; + + let attr = attr.clone(); + + item.attrs + .retain(|x| x != &attr); + + match parse_function_definition(trait_ident, input, item) { + Ok(Some(_)) if !args.partial => { + return syn::Error::new( + attr.span(), + "Sealing a function from implementation but allowing it to be called is already done by sealing the trait itself. \ + If you want to seal this function, but not others, \ + consider adding `partial` to the `sealed` attr on the trait: `#[sealed(partial)]`" + ) + .into_compile_error() + .into() + } Ok(Some(wrapper)) => wrappers.push(wrapper.into()), Ok(None) => {} Err(err) => { @@ -239,7 +267,18 @@ fn parse_sealed_impl(item_impl: &mut syn::ItemImpl) -> syn::Result for item in &mut item_impl.items { if let syn::ImplItem::Fn(item) = item { - parse_function_implementation(&ident, item)?; + let input = parse_function_arguments(&item.attrs)?; + + let Some((attr, input)) = input else { + continue; + }; + + let attr = attr.clone(); + + item.attrs + .retain(|x| x != &attr); + + parse_function_implementation(&ident, input, item)?; } } @@ -265,25 +304,31 @@ fn seal_function_name(seal: D) -> syn::Ident { format_ident!("_{}", &seal.to_string()) } +fn parse_function_arguments( + attrs: &[syn::Attribute], +) -> Result, syn::Error> { + for attr in attrs { + if attr.path().is_ident("seal") { + let value = if let syn::Meta::List(list) = &attr.meta { + syn::parse(list.tokens.clone().into())? + } else { + FunctionArguments::default() + }; + + return Ok(Some((attr, value))); + } + } + + Ok(None) +} + /// Seal a specific function (to avoid people overwriting behaviour of /// specific functions, or even calling them) fn parse_function_definition( trait_ident: &syn::Ident, + args: FunctionArguments, function: &mut syn::TraitItemFn, ) -> Result, syn::Error> { - let Some(attr) = function - .attrs - .iter() - .find(|attr| attr.path().is_ident("seal")) - .cloned() - else { - return Ok(None); - }; - - function - .attrs - .retain(|x| x != &attr); - let wrapper_sig = function.sig.clone(); let seal = seal_name(trait_ident); @@ -292,15 +337,9 @@ fn parse_function_definition( .inputs .push(parse_quote!(_token: #seal::Token)); - let syn::Meta::List(list) = attr.meta else { - return Ok(None); - }; - - let input = syn::parse::(list.tokens.into())?; - let mut value = None; - if input.callable { + if args.callable { function.sig.ident = seal_function_name(&function.sig.ident); let inner_name = &function.sig.ident; @@ -324,21 +363,9 @@ fn parse_function_definition( fn parse_function_implementation( trait_ident: &syn::Ident, + args: FunctionArguments, function: &mut syn::ImplItemFn, ) -> Result<(), syn::Error> { - let Some(attr) = function - .attrs - .iter() - .find(|attr| attr.path().is_ident("seal")) - .cloned() - else { - return Ok(()); - }; - - function - .attrs - .retain(|x| x != &attr); - let seal = seal_name(trait_ident); function @@ -346,13 +373,7 @@ fn parse_function_implementation( .inputs .push(parse_quote!(_token: #seal::Token)); - let syn::Meta::List(list) = attr.meta else { - return Ok(()); - }; - - let input = syn::parse::(list.tokens.into())?; - - if input.callable { + if args.callable { function.sig.ident = seal_function_name(&function.sig.ident); } @@ -371,6 +392,10 @@ struct TraitArguments { /// /// Default is [`syn::Visibility::Inherited`]. visibility: syn::Visibility, + + /// `partial` indicates that the trait itself should not be sealed, but some of the functions + /// are sealed + partial: bool, } impl Default for TraitArguments { @@ -378,6 +403,7 @@ impl Default for TraitArguments { Self { erased: false, visibility: syn::Visibility::Inherited, + partial: false, } } } @@ -408,6 +434,11 @@ impl Parse for TraitArguments { } } + "partial" => { + syn::Ident::parse_any(input)?; + out.partial = true; + } + unknown => { return Err(syn::Error::new( ident.span(), diff --git a/tests/fail/08-partial-seal.rs b/tests/fail/08-partial-seal.rs new file mode 100644 index 0000000..d5bfd03 --- /dev/null +++ b/tests/fail/08-partial-seal.rs @@ -0,0 +1,9 @@ +use sealed::sealed; + +#[sealed] +pub trait A { + #[seal(callable)] + fn b() {} +} + +fn main() {} diff --git a/tests/fail/08-partial-seal.stderr b/tests/fail/08-partial-seal.stderr new file mode 100644 index 0000000..f8af7ce --- /dev/null +++ b/tests/fail/08-partial-seal.stderr @@ -0,0 +1,5 @@ +error: Sealing a function from implementation but allowing it to be called is already done by sealing the trait itself. If you want to seal this function, but not others, consider adding `partial` to the `sealed` attr on the trait: `#[sealed(partial)]` + --> tests/fail/08-partial-seal.rs:5:2 + | +5 | #[seal(callable)] + | ^^^^^^^^^^^^^^^^^ diff --git a/tests/pass/14-partial-seal.rs b/tests/pass/14-sealed-functions.rs similarity index 100% rename from tests/pass/14-partial-seal.rs rename to tests/pass/14-sealed-functions.rs diff --git a/tests/pass/15-partial-seal.rs b/tests/pass/15-partial-seal.rs new file mode 100644 index 0000000..9dab208 --- /dev/null +++ b/tests/pass/15-partial-seal.rs @@ -0,0 +1,11 @@ +use sealed::sealed; + +#[sealed(partial)] +pub trait A { + #[seal(callable)] + fn sealed() {} + + fn normal(); +} + +fn main() {} From 875acc3d6e71fa86c310156926911ddd73bac41a Mon Sep 17 00:00:00 2001 From: TheLazyDutchman Date: Sat, 16 Dec 2023 16:03:08 +0100 Subject: [PATCH 06/10] Added new errors --- examples/partial.rs | 12 ++++++-- src/lib.rs | 31 ++++++++++++++++++-- tests/fail/09-partial-argument.rs | 14 +++++++++ tests/fail/09-partial-argument.stderr | 11 +++++++ tests/fail/10-partial-without-default.rs | 9 ++++++ tests/fail/10-partial-without-default.stderr | 5 ++++ 6 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 tests/fail/09-partial-argument.rs create mode 100644 tests/fail/09-partial-argument.stderr create mode 100644 tests/fail/10-partial-without-default.rs create mode 100644 tests/fail/10-partial-without-default.stderr diff --git a/examples/partial.rs b/examples/partial.rs index be02773..bb0c7e8 100644 --- a/examples/partial.rs +++ b/examples/partial.rs @@ -12,6 +12,8 @@ pub trait A { }: User, ) { } + + fn not_sealed(); } pub struct Email(String); @@ -25,18 +27,24 @@ pub struct User { pub struct Impl; #[sealed] -impl A for Impl {} +impl A for Impl { + fn not_sealed() {} +} #[sealed(partial)] pub trait B { #[seal(callable)] - fn no_default(email: Email); + fn no_default(_email: Email) {} + + fn hello(); } #[sealed] impl B for Impl { #[seal(callable)] fn no_default(_email: Email) {} + + fn hello() {} } #[sealed] diff --git a/src/lib.rs b/src/lib.rs index ef0e1bf..f8c2e96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -158,6 +158,9 @@ fn parse_sealed_trait(mut item_trait: syn::ItemTrait, args: TraitArguments) -> T let mut wrappers = Vec::::new(); + let mut all_implementable = true; + let mut none_implementable = true; + for item in &mut item_trait.items { if let syn::TraitItem::Fn(item) = item { let input = match parse_function_arguments(&item.attrs) { @@ -170,6 +173,7 @@ fn parse_sealed_trait(mut item_trait: syn::ItemTrait, args: TraitArguments) -> T }; let Some((attr, input)) = input else { + none_implementable = false; continue; }; @@ -189,8 +193,20 @@ fn parse_sealed_trait(mut item_trait: syn::ItemTrait, args: TraitArguments) -> T .into_compile_error() .into() } - Ok(Some(wrapper)) => wrappers.push(wrapper.into()), - Ok(None) => {} + Ok(_) if args.partial && item.default.is_none() => { + return syn::Error::new( + attr.span(), + "This function is sealed from implementation, \ + but it does not have a default implementation. \ + This effectively seals the entire trait, \ + which would be clearer to do by not having the trait seal be partial." + ).into_compile_error().into() + } + Ok(Some(wrapper)) => { + all_implementable = false; + wrappers.push(wrapper.into()) + }, + Ok(None) => all_implementable = false, Err(err) => { return err .into_compile_error() @@ -200,6 +216,17 @@ fn parse_sealed_trait(mut item_trait: syn::ItemTrait, args: TraitArguments) -> T } } + if args.partial && all_implementable { + return syn::Error::new(item_trait.ident.span(), "This trait is partially sealed, \ + however none of its methods are sealed, so it does nothing. \ + Either seal a function or remove `partial` from the `sealed` attribute.").to_compile_error().into(); + } + + if args.partial && none_implementable { + return syn::Error::new(item_trait.ident.span(), "This trait is partially sealed, \ + however none of its methods are implementable, so the `partial` argument does nothing.").into_compile_error().into(); + } + item_trait .items .extend(wrappers); diff --git a/tests/fail/09-partial-argument.rs b/tests/fail/09-partial-argument.rs new file mode 100644 index 0000000..4d83af4 --- /dev/null +++ b/tests/fail/09-partial-argument.rs @@ -0,0 +1,14 @@ +use sealed::sealed; + +#[sealed(partial)] +pub trait A { + #[seal] + fn sealed() {} +} + +#[sealed(partial)] +pub trait B { + fn not_sealed(); +} + +fn main() {} diff --git a/tests/fail/09-partial-argument.stderr b/tests/fail/09-partial-argument.stderr new file mode 100644 index 0000000..3b9edf2 --- /dev/null +++ b/tests/fail/09-partial-argument.stderr @@ -0,0 +1,11 @@ +error: This trait is partially sealed, however none of its methods are implementable, so the `partial` argument does nothing. + --> tests/fail/09-partial-argument.rs:4:11 + | +4 | pub trait A { + | ^ + +error: This trait is partially sealed, however none of its methods are sealed, so it does nothing. Either seal a function or remove `partial` from the `sealed` attribute. + --> tests/fail/09-partial-argument.rs:10:11 + | +10 | pub trait B { + | ^ diff --git a/tests/fail/10-partial-without-default.rs b/tests/fail/10-partial-without-default.rs new file mode 100644 index 0000000..30c2938 --- /dev/null +++ b/tests/fail/10-partial-without-default.rs @@ -0,0 +1,9 @@ +use sealed::sealed; + +#[sealed(partial)] +pub trait A { + #[seal] + fn sealed(); +} + +fn main() {} diff --git a/tests/fail/10-partial-without-default.stderr b/tests/fail/10-partial-without-default.stderr new file mode 100644 index 0000000..9c39733 --- /dev/null +++ b/tests/fail/10-partial-without-default.stderr @@ -0,0 +1,5 @@ +error: This function is sealed from implementation, but it does not have a default implementation. This effectively seals the entire trait, which would be clearer to do by not having the trait seal be partial. + --> tests/fail/10-partial-without-default.rs:5:2 + | +5 | #[seal] + | ^^^^^^^ From fea6ee72fa0cba1b0ef7bdda9fe9f06ef0a4b47b Mon Sep 17 00:00:00 2001 From: TheLazyDutchman Date: Mon, 18 Dec 2023 17:22:51 +0100 Subject: [PATCH 07/10] Fixed tests after rebase --- src/lib.rs | 598 +++++++++---------- tests/fail/01-general.stderr | 13 +- tests/fail/02-nesting.stderr | 13 +- tests/fail/03-private-by-default.stderr | 5 +- tests/fail/04-no-full-pub.stderr | 2 +- tests/fail/06-wrong-argument.stderr | 12 +- tests/fail/07-sealed-function.stderr | 16 +- tests/fail/08-partial-seal.stderr | 8 +- tests/fail/10-partial-without-default.stderr | 6 +- 9 files changed, 318 insertions(+), 355 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f8c2e96..87ef38c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,68 +124,64 @@ use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{format_ident, quote}; use syn::{ - ext::IdentExt, - parse::{Parse, ParseStream}, - parse_macro_input, parse_quote, - spanned::Spanned, - token, + ext::IdentExt, + parse::{Parse, ParseStream}, + parse_macro_input, parse_quote, + spanned::Spanned, + token, }; #[proc_macro_attribute] pub fn sealed(args: TokenStream, input: TokenStream) -> TokenStream { - match parse_macro_input!(input) { - syn::Item::Impl(mut item_impl) => parse_sealed_impl(&mut item_impl), - syn::Item::Trait(item_trait) => { - Ok(parse_sealed_trait(item_trait, parse_macro_input!(args))) - } - _ => Err(syn::Error::new(Span::call_site(), "expected impl or trait")), - } - .unwrap_or_else(|e| e.to_compile_error()) - .into() + match parse_macro_input!(input) { + syn::Item::Impl(mut item_impl) => parse_sealed_impl(&mut item_impl), + syn::Item::Trait(item_trait) => { + Ok(parse_sealed_trait(item_trait, parse_macro_input!(args))) + } + _ => Err(syn::Error::new(Span::call_site(), "expected impl or trait")), + } + .unwrap_or_else(|e| e.to_compile_error()) + .into() } // Care for https://gist.github.com/Kestrer/8c05ebd4e0e9347eb05f265dfb7252e1#procedural-macros-support-renaming-the-crate fn parse_sealed_trait(mut item_trait: syn::ItemTrait, args: TraitArguments) -> TokenStream2 { - let trait_ident = &item_trait.ident.unraw(); - let trait_generics = &item_trait.generics; - let seal = seal_name(trait_ident); - let vis = &args.visibility; - - let (_, ty_generics, where_clause) = trait_generics.split_for_impl(); - item_trait - .supertraits - .push(parse_quote!( #seal::Sealed #ty_generics )); - - let mut wrappers = Vec::::new(); - - let mut all_implementable = true; - let mut none_implementable = true; - - for item in &mut item_trait.items { - if let syn::TraitItem::Fn(item) = item { - let input = match parse_function_arguments(&item.attrs) { - Ok(input) => input, - Err(err) => { - return err - .into_compile_error() - .into() - } - }; - - let Some((attr, input)) = input else { - none_implementable = false; - continue; - }; - - let attr = attr.clone(); - - item.attrs - .retain(|x| x != &attr); - - match parse_function_definition(trait_ident, input, item) { + let trait_ident = &item_trait.ident.unraw(); + let trait_generics = &item_trait.generics; + let seal = seal_name(trait_ident); + let vis = &args.visibility; + + let (_, ty_generics, where_clause) = trait_generics.split_for_impl(); + item_trait + .supertraits + .push(parse_quote!( #seal::Sealed #ty_generics )); + + let mut wrappers = Vec::::new(); + + let mut all_implementable = true; + let mut none_implementable = true; + + for item in &mut item_trait.items { + if let syn::TraitItem::Fn(item) = item { + let input = match parse_function_arguments(&item.attrs) { + Ok(input) => input, + Err(err) => return err.into_compile_error().into(), + }; + + let input = match input { + Some(value) => value, + None => { + none_implementable = false; + continue; + } + }; + + item.attrs.retain(|x| !x.path().is_ident("seal")); + + match parse_function_definition(trait_ident, input, item) { Ok(Some(_)) if !args.partial => { return syn::Error::new( - attr.span(), + item.sig.ident.span(), "Sealing a function from implementation but allowing it to be called is already done by sealing the trait itself. \ If you want to seal this function, but not others, \ consider adding `partial` to the `sealed` attr on the trait: `#[sealed(partial)]`" @@ -195,7 +191,7 @@ fn parse_sealed_trait(mut item_trait: syn::ItemTrait, args: TraitArguments) -> T } Ok(_) if args.partial && item.default.is_none() => { return syn::Error::new( - attr.span(), + item.sig.ident.span(), "This function is sealed from implementation, \ but it does not have a default implementation. \ This effectively seals the entire trait, \ @@ -213,312 +209,298 @@ fn parse_sealed_trait(mut item_trait: syn::ItemTrait, args: TraitArguments) -> T .into() } } - } - } + } + } - if args.partial && all_implementable { - return syn::Error::new(item_trait.ident.span(), "This trait is partially sealed, \ + if args.partial && all_implementable { + return syn::Error::new( + item_trait.ident.span(), + "This trait is partially sealed, \ however none of its methods are sealed, so it does nothing. \ - Either seal a function or remove `partial` from the `sealed` attribute.").to_compile_error().into(); - } - - if args.partial && none_implementable { - return syn::Error::new(item_trait.ident.span(), "This trait is partially sealed, \ - however none of its methods are implementable, so the `partial` argument does nothing.").into_compile_error().into(); - } - - item_trait - .items - .extend(wrappers); - - let mod_code = if args.erased { - let lifetimes = trait_generics.lifetimes(); - let const_params = trait_generics.const_params(); - let type_params = trait_generics - .type_params() - .map(|syn::TypeParam { ident, .. }| -> syn::TypeParam { - parse_quote!( #ident : ?Sized ) - }); - - // Token here is used as an argument to a function in order to seal it. - quote! { - pub trait Sealed< #(#lifetimes ,)* #(#type_params ,)* #(#const_params ,)* > {} - pub struct Token; - } - } else { - // `trait_generics` does not output its where clause when tokenized (due - // to supertraits in the middle). So we output them separately. - quote! { - use super::*; - pub trait Sealed #trait_generics #where_clause {} - pub struct Token; - } - }; - - quote! { - #[automatically_derived] - #vis mod #seal { - #mod_code - } - #item_trait - } + Either seal a function or remove `partial` from the `sealed` attribute.", + ) + .to_compile_error() + .into(); + } + + if args.partial && none_implementable { + return syn::Error::new( + item_trait.ident.span(), + "This trait is partially sealed, \ + however none of its methods are implementable, so the `partial` argument does nothing.", + ) + .into_compile_error() + .into(); + } + + item_trait.items.extend(wrappers); + + let mod_code = if args.erased { + let lifetimes = trait_generics.lifetimes(); + let const_params = trait_generics.const_params(); + let type_params = + trait_generics + .type_params() + .map(|syn::TypeParam { ident, .. }| -> syn::TypeParam { + parse_quote!( #ident : ?Sized ) + }); + + // Token here is used as an argument to a function in order to seal it. + quote! { + pub trait Sealed< #(#lifetimes ,)* #(#type_params ,)* #(#const_params ,)* > {} + pub struct Token; + } + } else { + // `trait_generics` does not output its where clause when tokenized (due + // to supertraits in the middle). So we output them separately. + quote! { + use super::*; + pub trait Sealed #trait_generics #where_clause {} + pub struct Token; + } + }; + + quote! { + #[automatically_derived] + #vis mod #seal { + #mod_code + } + #item_trait + } } fn parse_sealed_impl(item_impl: &mut syn::ItemImpl) -> syn::Result { - let impl_trait = item_impl - .trait_ - .as_ref() - .ok_or_else(|| { - syn::Error::new_spanned(item_impl.clone(), "missing implementation trait") - })?; - - let mut sealed_path = impl_trait.1.segments.clone(); - - // since `impl for ...` is not allowed, this path will *always* have at least length 1 - // thus both `first` and `last` are safe to unwrap - let syn::PathSegment { ident, arguments } = sealed_path - .pop() - .unwrap() - .into_value(); - let seal = seal_name(ident.unraw()); - sealed_path.push(parse_quote!( #seal )); - sealed_path.push(parse_quote!(Sealed)); - - let self_type = &item_impl.self_ty; - - // Only keep the introduced params (no bounds), since - // the bounds may break in the `#seal` submodule. - let (trait_generics, _, where_clauses) = item_impl - .generics - .split_for_impl(); - - for item in &mut item_impl.items { - if let syn::ImplItem::Fn(item) = item { - let input = parse_function_arguments(&item.attrs)?; - - let Some((attr, input)) = input else { - continue; - }; - - let attr = attr.clone(); - - item.attrs - .retain(|x| x != &attr); - - parse_function_implementation(&ident, input, item)?; - } - } - - Ok(quote! { - #[automatically_derived] - impl #trait_generics #sealed_path #arguments for #self_type #where_clauses {} - #item_impl - }) + let impl_trait = item_impl.trait_.as_ref().ok_or_else(|| { + syn::Error::new_spanned(item_impl.clone(), "missing implementation trait") + })?; + + let mut sealed_path = impl_trait.1.segments.clone(); + + // since `impl for ...` is not allowed, this path will *always* have at least length 1 + // thus both `first` and `last` are safe to unwrap + let syn::PathSegment { ident, arguments } = sealed_path.pop().unwrap().into_value(); + let seal = seal_name(ident.unraw()); + sealed_path.push(parse_quote!( #seal )); + sealed_path.push(parse_quote!(Sealed)); + + let self_type = &item_impl.self_ty; + + // Only keep the introduced params (no bounds), since + // the bounds may break in the `#seal` submodule. + let (trait_generics, _, where_clauses) = item_impl.generics.split_for_impl(); + + for item in &mut item_impl.items { + if let syn::ImplItem::Fn(item) = item { + let input = parse_function_arguments(&item.attrs)?; + + let input = match input { + Some(value) => value, + None => continue, + }; + + item.attrs.retain(|x| !x.path().is_ident("seal")); + + parse_function_implementation(&ident, input, item)?; + } + } + + Ok(quote! { + #[automatically_derived] + impl #trait_generics #sealed_path #arguments for #self_type #where_clauses {} + #item_impl + }) } /// Constructs [`syn::Ident`] of a sealing module name. fn seal_name(seal: D) -> syn::Ident { - format_ident!( - "__seal_{}", - &seal - .to_string() - .to_snake_case() - ) + format_ident!("__seal_{}", &seal.to_string().to_snake_case()) } /// Constructs [`syn::Ident`] of a function wrapped by another function fn seal_function_name(seal: D) -> syn::Ident { - format_ident!("_{}", &seal.to_string()) + format_ident!("_{}", &seal.to_string()) } fn parse_function_arguments( - attrs: &[syn::Attribute], -) -> Result, syn::Error> { - for attr in attrs { - if attr.path().is_ident("seal") { - let value = if let syn::Meta::List(list) = &attr.meta { - syn::parse(list.tokens.clone().into())? - } else { - FunctionArguments::default() - }; - - return Ok(Some((attr, value))); - } - } - - Ok(None) + attrs: &[syn::Attribute], +) -> Result, syn::Error> { + for attr in attrs { + if attr.path().is_ident("seal") { + let value = if let syn::Meta::List(list) = &attr.meta { + syn::parse(list.tokens.clone().into())? + } else { + FunctionArguments::default() + }; + + return Ok(Some(value)); + } + } + + Ok(None) } /// Seal a specific function (to avoid people overwriting behaviour of /// specific functions, or even calling them) fn parse_function_definition( - trait_ident: &syn::Ident, - args: FunctionArguments, - function: &mut syn::TraitItemFn, + trait_ident: &syn::Ident, + args: FunctionArguments, + function: &mut syn::TraitItemFn, ) -> Result, syn::Error> { - let wrapper_sig = function.sig.clone(); - let seal = seal_name(trait_ident); - - function - .sig - .inputs - .push(parse_quote!(_token: #seal::Token)); - - let mut value = None; - - if args.callable { - function.sig.ident = seal_function_name(&function.sig.ident); - let inner_name = &function.sig.ident; - - let args = wrapper_sig - .inputs - .iter() - .map(|input| match input { - syn::FnArg::Receiver(_) => quote!(self), - syn::FnArg::Typed(input) => { - let pat = &input.pat; - quote!(#pat) - } - }); - - value = Some(parse_quote!(#wrapper_sig { - ::#inner_name(#(#args,)* #seal::Token) - })); - } - Ok(value) + let wrapper_sig = function.sig.clone(); + let seal = seal_name(trait_ident); + + function.sig.inputs.push(parse_quote!(_token: #seal::Token)); + + let mut value = None; + + if args.callable { + function.sig.ident = seal_function_name(&function.sig.ident); + let inner_name = &function.sig.ident; + + let args = wrapper_sig.inputs.iter().map(|input| match input { + syn::FnArg::Receiver(_) => quote!(self), + syn::FnArg::Typed(input) => { + let pat = &input.pat; + quote!(#pat) + } + }); + + value = Some(parse_quote!(#wrapper_sig { + ::#inner_name(#(#args,)* #seal::Token) + })); + } + Ok(value) } fn parse_function_implementation( - trait_ident: &syn::Ident, - args: FunctionArguments, - function: &mut syn::ImplItemFn, + trait_ident: &syn::Ident, + args: FunctionArguments, + function: &mut syn::ImplItemFn, ) -> Result<(), syn::Error> { - let seal = seal_name(trait_ident); + let seal = seal_name(trait_ident); - function - .sig - .inputs - .push(parse_quote!(_token: #seal::Token)); + function.sig.inputs.push(parse_quote!(_token: #seal::Token)); - if args.callable { - function.sig.ident = seal_function_name(&function.sig.ident); - } + if args.callable { + function.sig.ident = seal_function_name(&function.sig.ident); + } - Ok(()) + Ok(()) } /// Arguments accepted by `#[sealed]` attribute when placed on a trait /// definition. struct TraitArguments { - /// `erase` argument indicating whether trait bounds erasure should be used. - /// - /// Default is `false`. - erased: bool, - - /// `pub` argument defining visibility of the generated sealing module. - /// - /// Default is [`syn::Visibility::Inherited`]. - visibility: syn::Visibility, - - /// `partial` indicates that the trait itself should not be sealed, but some of the functions - /// are sealed - partial: bool, + /// `erase` argument indicating whether trait bounds erasure should be used. + /// + /// Default is `false`. + erased: bool, + + /// `pub` argument defining visibility of the generated sealing module. + /// + /// Default is [`syn::Visibility::Inherited`]. + visibility: syn::Visibility, + + /// `partial` indicates that the trait itself should not be sealed, but some of the functions + /// are sealed + partial: bool, } impl Default for TraitArguments { - fn default() -> Self { - Self { - erased: false, - visibility: syn::Visibility::Inherited, - partial: false, - } - } + fn default() -> Self { + Self { + erased: false, + visibility: syn::Visibility::Inherited, + partial: false, + } + } } impl Parse for TraitArguments { - fn parse(input: ParseStream<'_>) -> syn::Result { - let mut out = Self::default(); - - while !input.is_empty() { - let ident = syn::Ident::parse_any(&input.fork())?; - - match ident.to_string().as_str() { - "erase" => { - syn::Ident::parse_any(input)?; - out.erased = true; - } - - "pub" => { - out.visibility = input.parse()?; - if matches!(out.visibility, syn::Visibility::Public(_)) { - return Err(syn::Error::new( - out.visibility.span(), - "`pub` visibility breaks the seal as allows to use \ + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut out = Self::default(); + + while !input.is_empty() { + let ident = syn::Ident::parse_any(&input.fork())?; + + match ident.to_string().as_str() { + "erase" => { + syn::Ident::parse_any(input)?; + out.erased = true; + } + + "pub" => { + out.visibility = input.parse()?; + if matches!(out.visibility, syn::Visibility::Public(_)) { + return Err(syn::Error::new( + out.visibility.span(), + "`pub` visibility breaks the seal as allows to use \ it outside its crate.\n\ Consider tightening the visibility (e.g. \ `pub(crate)`) if you actually need sealing.", - )); - } - } - - "partial" => { - syn::Ident::parse_any(input)?; - out.partial = true; - } - - unknown => { - return Err(syn::Error::new( - ident.span(), - format!("unknown `{}` attribute argument", unknown), - )) - } - } - - if input - .lookahead1() - .peek(token::Comma) - .then(|| input.parse::()) - .transpose()? - .is_none() && !input.is_empty() - { - return Err(syn::Error::new(ident.span(), "expected followed by `,`")); - } - } - - Ok(out) - } + )); + } + } + + "partial" => { + syn::Ident::parse_any(input)?; + out.partial = true; + } + + unknown => { + return Err(syn::Error::new( + ident.span(), + format!("unknown `{}` attribute argument", unknown), + )) + } + } + + if input + .lookahead1() + .peek(token::Comma) + .then(|| input.parse::()) + .transpose()? + .is_none() + && !input.is_empty() + { + return Err(syn::Error::new(ident.span(), "expected followed by `,`")); + } + } + + Ok(out) + } } /// Arguments accepted by `#[seal]` attribute when placed on functions in a (partially) sealed trait struct FunctionArguments { - /// Determines whether the sealed function is wrapped by a public function that is callable - callable: bool, + /// Determines whether the sealed function is wrapped by a public function that is callable + callable: bool, } impl Default for FunctionArguments { - fn default() -> Self { - FunctionArguments { callable: false } - } + fn default() -> Self { + FunctionArguments { callable: false } + } } impl Parse for FunctionArguments { - fn parse(input: ParseStream) -> syn::Result { - let mut out = FunctionArguments::default(); - let ident = syn::Ident::parse(&input.fork())?; - - match ident.to_string().as_str() { - "callable" => { - syn::Ident::parse(input)?; - out.callable = true - } - unknown => { - return Err(syn::Error::new( - ident.span(), - format!("unknown `{}` attribute argument", unknown), - )) - } - } - - Ok(out) - } + fn parse(input: ParseStream) -> syn::Result { + let mut out = FunctionArguments::default(); + let ident = syn::Ident::parse(&input.fork())?; + + match ident.to_string().as_str() { + "callable" => { + syn::Ident::parse(input)?; + out.callable = true + } + unknown => { + return Err(syn::Error::new( + ident.span(), + format!("unknown `{}` attribute argument", unknown), + )) + } + } + + Ok(out) + } } diff --git a/tests/fail/01-general.stderr b/tests/fail/01-general.stderr index c728d9d..1cdb7ae 100644 --- a/tests/fail/01-general.stderr +++ b/tests/fail/01-general.stderr @@ -1,21 +1,14 @@ error[E0277]: the trait bound `C: Sealed` is not satisfied - --> tests/fail/01-general.rs:20:12 + --> tests/fail/01-general.rs:20:6 | 20 | impl T for C {} - | ^ the trait `Sealed` is not implemented for `C` + | ^ the trait `Sealed` is not implemented for `C` | - = help: the following other types implement trait `Sealed`: - A - B note: required by a bound in `T` --> tests/fail/01-general.rs:11:1 | 11 | #[sealed] | ^^^^^^^^^ required by this bound in `T` 12 | trait T {} - | - required by a bound in this trait - = note: `T` is a "sealed trait", because to implement it you also need to implement `__seal_t::Sealed`, which is not accessible; this is usually done to force you to use one of the provided types that already implement it - = help: the following types implement the trait: - A - B + | - required by a bound in this = note: this error originates in the attribute macro `sealed` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/fail/02-nesting.stderr b/tests/fail/02-nesting.stderr index 1910c7f..64f6be7 100644 --- a/tests/fail/02-nesting.stderr +++ b/tests/fail/02-nesting.stderr @@ -1,21 +1,14 @@ error[E0277]: the trait bound `C: Sealed` is not satisfied - --> tests/fail/02-nesting.rs:26:42 + --> tests/fail/02-nesting.rs:26:6 | 26 | impl lets::attempt::some::nesting::T for C {} - | ^ the trait `Sealed` is not implemented for `C` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Sealed` is not implemented for `C` | - = help: the following other types implement trait `Sealed`: - A - B note: required by a bound in `T` --> tests/fail/02-nesting.rs:8:17 | 8 | #[sealed(pub(crate))] | ^^^^^^^^^^^^^^^^^^^^^ required by this bound in `T` 9 | pub trait T {} - | - required by a bound in this trait - = note: `T` is a "sealed trait", because to implement it you also need to implement `lets::attempt::some::nesting::__seal_t::Sealed`, which is not accessible; this is usually done to force you to use one of the provided types that already implement it - = help: the following types implement the trait: - A - B + | - required by a bound in this = note: this error originates in the attribute macro `sealed` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/fail/03-private-by-default.stderr b/tests/fail/03-private-by-default.stderr index 0dfbe5a..42a59ca 100644 --- a/tests/fail/03-private-by-default.stderr +++ b/tests/fail/03-private-by-default.stderr @@ -2,10 +2,7 @@ error[E0603]: module `__seal_t` is private --> tests/fail/03-private-by-default.rs:17:1 | 17 | #[sealed] - | ^^^^^^^^^ - | | - | private module - | trait `Sealed` is not publicly re-exported + | ^^^^^^^^^ private module | note: the module `__seal_t` is defined here --> tests/fail/03-private-by-default.rs:8:17 diff --git a/tests/fail/04-no-full-pub.stderr b/tests/fail/04-no-full-pub.stderr index 2b4a660..34115fd 100644 --- a/tests/fail/04-no-full-pub.stderr +++ b/tests/fail/04-no-full-pub.stderr @@ -1,5 +1,5 @@ error: `pub` visibility breaks the seal as allows to use it outside its crate. - Consider tightening the visibility (e.g. `pub(crate)`) if you actually need sealing. +Consider tightening the visibility (e.g. `pub(crate)`) if you actually need sealing. --> tests/fail/04-no-full-pub.rs:8:26 | 8 | #[sealed(pub)] diff --git a/tests/fail/06-wrong-argument.stderr b/tests/fail/06-wrong-argument.stderr index b36a538..6d0e7c5 100644 --- a/tests/fail/06-wrong-argument.stderr +++ b/tests/fail/06-wrong-argument.stderr @@ -4,12 +4,6 @@ error: unknown `erased` attribute argument 3 | #[sealed(erased)] | ^^^^^^ -error[E0405]: cannot find trait `T` in this scope - --> tests/fail/06-wrong-argument.rs:9:6 - | -9 | impl T for A {} - | ^ not found in this scope - error[E0433]: failed to resolve: use of undeclared crate or module `__seal_t` --> tests/fail/06-wrong-argument.rs:8:1 | @@ -17,3 +11,9 @@ error[E0433]: failed to resolve: use of undeclared crate or module `__seal_t` | ^^^^^^^^^ use of undeclared crate or module `__seal_t` | = note: this error originates in the attribute macro `sealed` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0405]: cannot find trait `T` in this scope + --> tests/fail/06-wrong-argument.rs:9:6 + | +9 | impl T for A {} + | ^ not found in this scope diff --git a/tests/fail/07-sealed-function.stderr b/tests/fail/07-sealed-function.stderr index c6b993e..2e217a8 100644 --- a/tests/fail/07-sealed-function.stderr +++ b/tests/fail/07-sealed-function.stderr @@ -4,25 +4,21 @@ error[E0425]: cannot find value `Token` in this scope 27 | inner::A::a(Token); | ^^^^^ not found in this scope | -note: unit struct `crate::inner::__seal_partial_sealed::Token` exists but is inaccessible - --> tests/fail/07-sealed-function.rs:4:2 +help: consider importing this unit struct + | +23 | use crate::inner::__seal_partial_sealed::Token; | -4 | #[sealed] - | ^^^^^^^^^ not accessible - = note: this error originates in the attribute macro `sealed` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0061]: this function takes 1 argument but 0 arguments were supplied --> tests/fail/07-sealed-function.rs:26:2 | 26 | inner::A::a(); - | ^^^^^^^^^^^-- an argument of type `Token` is missing + | ^^^^^^^^^^^-- supplied 0 arguments + | | + | expected 1 argument | note: associated function defined here --> tests/fail/07-sealed-function.rs:7:6 | 7 | fn a(); | ^ -help: provide the argument - | -26 | inner::A::a(/* Token */); - | ~~~~~~~~~~~~~ diff --git a/tests/fail/08-partial-seal.stderr b/tests/fail/08-partial-seal.stderr index f8af7ce..8104ecd 100644 --- a/tests/fail/08-partial-seal.stderr +++ b/tests/fail/08-partial-seal.stderr @@ -1,5 +1,7 @@ error: Sealing a function from implementation but allowing it to be called is already done by sealing the trait itself. If you want to seal this function, but not others, consider adding `partial` to the `sealed` attr on the trait: `#[sealed(partial)]` - --> tests/fail/08-partial-seal.rs:5:2 + --> tests/fail/08-partial-seal.rs:3:1 | -5 | #[seal(callable)] - | ^^^^^^^^^^^^^^^^^ +3 | #[sealed] + | ^^^^^^^^^ + | + = note: this error originates in the attribute macro `sealed` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/fail/10-partial-without-default.stderr b/tests/fail/10-partial-without-default.stderr index 9c39733..0cf0dc8 100644 --- a/tests/fail/10-partial-without-default.stderr +++ b/tests/fail/10-partial-without-default.stderr @@ -1,5 +1,5 @@ error: This function is sealed from implementation, but it does not have a default implementation. This effectively seals the entire trait, which would be clearer to do by not having the trait seal be partial. - --> tests/fail/10-partial-without-default.rs:5:2 + --> tests/fail/10-partial-without-default.rs:6:5 | -5 | #[seal] - | ^^^^^^^ +6 | fn sealed(); + | ^^^^^^ From 5538f9e3172f873f85d1191ca8a18f8b055395f1 Mon Sep 17 00:00:00 2001 From: TheLazyDutchman Date: Mon, 18 Dec 2023 17:38:15 +0100 Subject: [PATCH 08/10] Added `uncallable` argument to `seal` attribute --- examples/partial.rs | 52 ++++++++++---------- src/lib.rs | 42 ++++++++++------ tests/fail/07-sealed-function.rs | 34 ++++++------- tests/fail/07-sealed-function.stderr | 6 +-- tests/fail/09-partial-argument.rs | 6 +-- tests/fail/10-partial-without-default.rs | 4 +- tests/fail/10-partial-without-default.stderr | 2 +- tests/pass/14-sealed-functions.rs | 20 ++++---- 8 files changed, 89 insertions(+), 77 deletions(-) diff --git a/examples/partial.rs b/examples/partial.rs index bb0c7e8..7356167 100644 --- a/examples/partial.rs +++ b/examples/partial.rs @@ -2,62 +2,62 @@ use sealed::sealed; #[sealed(partial)] pub trait A { - #[seal(callable)] - fn has_default( - Email(_email): Email, - User { - name: _name, - email: Email(_email2), - age: _age, - }: User, - ) { - } - - fn not_sealed(); + #[seal(callable)] + fn has_default( + Email(_email): Email, + User { + name: _name, + email: Email(_email2), + age: _age, + }: User, + ) { + } + + fn not_sealed(); } pub struct Email(String); pub struct User { - name: String, - email: Email, - age: u8, + name: String, + email: Email, + age: u8, } pub struct Impl; #[sealed] impl A for Impl { - fn not_sealed() {} + fn not_sealed() {} } #[sealed(partial)] pub trait B { - #[seal(callable)] - fn no_default(_email: Email) {} + #[seal(callable)] + fn no_default(_email: Email) {} - fn hello(); + fn hello(); } #[sealed] impl B for Impl { - #[seal(callable)] - fn no_default(_email: Email) {} + #[seal(callable)] + fn no_default(_email: Email) {} - fn hello() {} + fn hello() {} } #[sealed] pub trait NoPartial { - #[seal] - fn has_default() {} + #[seal(uncallable)] + fn has_default() {} - fn no_default(); + fn no_default(); } #[sealed] impl NoPartial for Impl { - fn no_default() {} + fn no_default() {} } fn main() {} diff --git a/src/lib.rs b/src/lib.rs index 87ef38c..4e10915 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -134,7 +134,7 @@ use syn::{ #[proc_macro_attribute] pub fn sealed(args: TokenStream, input: TokenStream) -> TokenStream { match parse_macro_input!(input) { - syn::Item::Impl(mut item_impl) => parse_sealed_impl(&mut item_impl), + syn::Item::Impl(item_impl) => parse_sealed_impl(item_impl), syn::Item::Trait(item_trait) => { Ok(parse_sealed_trait(item_trait, parse_macro_input!(args))) } @@ -163,7 +163,7 @@ fn parse_sealed_trait(mut item_trait: syn::ItemTrait, args: TraitArguments) -> T for item in &mut item_trait.items { if let syn::TraitItem::Fn(item) = item { - let input = match parse_function_arguments(&item.attrs) { + let input = match parse_seal_attribute(&item.attrs) { Ok(input) => input, Err(err) => return err.into_compile_error().into(), }; @@ -269,7 +269,7 @@ fn parse_sealed_trait(mut item_trait: syn::ItemTrait, args: TraitArguments) -> T } } -fn parse_sealed_impl(item_impl: &mut syn::ItemImpl) -> syn::Result { +fn parse_sealed_impl(mut item_impl: syn::ItemImpl) -> syn::Result { let impl_trait = item_impl.trait_.as_ref().ok_or_else(|| { syn::Error::new_spanned(item_impl.clone(), "missing implementation trait") })?; @@ -291,7 +291,7 @@ fn parse_sealed_impl(item_impl: &mut syn::ItemImpl) -> syn::Result for item in &mut item_impl.items { if let syn::ImplItem::Fn(item) = item { - let input = parse_function_arguments(&item.attrs)?; + let input = parse_seal_attribute(&item.attrs)?; let input = match input { Some(value) => value, @@ -321,15 +321,18 @@ fn seal_function_name(seal: D) -> syn::Ident { format_ident!("_{}", &seal.to_string()) } -fn parse_function_arguments( +fn parse_seal_attribute( attrs: &[syn::Attribute], -) -> Result, syn::Error> { +) -> Result, syn::Error> { for attr in attrs { if attr.path().is_ident("seal") { let value = if let syn::Meta::List(list) = &attr.meta { syn::parse(list.tokens.clone().into())? } else { - FunctionArguments::default() + return Err(syn::Error::new_spanned( + attr, + "Missing argument. Expected one of `callable` or `uncallable`.", + )); }; return Ok(Some(value)); @@ -343,7 +346,7 @@ fn parse_function_arguments( /// specific functions, or even calling them) fn parse_function_definition( trait_ident: &syn::Ident, - args: FunctionArguments, + args: SealAttributeArguments, function: &mut syn::TraitItemFn, ) -> Result, syn::Error> { let wrapper_sig = function.sig.clone(); @@ -374,7 +377,7 @@ fn parse_function_definition( fn parse_function_implementation( trait_ident: &syn::Ident, - args: FunctionArguments, + args: SealAttributeArguments, function: &mut syn::ImplItemFn, ) -> Result<(), syn::Error> { let seal = seal_name(trait_ident); @@ -472,20 +475,20 @@ impl Parse for TraitArguments { } /// Arguments accepted by `#[seal]` attribute when placed on functions in a (partially) sealed trait -struct FunctionArguments { +struct SealAttributeArguments { /// Determines whether the sealed function is wrapped by a public function that is callable callable: bool, } -impl Default for FunctionArguments { +impl Default for SealAttributeArguments { fn default() -> Self { - FunctionArguments { callable: false } + SealAttributeArguments { callable: false } } } -impl Parse for FunctionArguments { +impl Parse for SealAttributeArguments { fn parse(input: ParseStream) -> syn::Result { - let mut out = FunctionArguments::default(); + let mut out = SealAttributeArguments::default(); let ident = syn::Ident::parse(&input.fork())?; match ident.to_string().as_str() { @@ -493,10 +496,19 @@ impl Parse for FunctionArguments { syn::Ident::parse(input)?; out.callable = true } + + "uncallable" => { + syn::Ident::parse(input)?; + out.callable = false; + } + unknown => { return Err(syn::Error::new( ident.span(), - format!("unknown `{}` attribute argument", unknown), + format!( + "unknown `{}` attribute argument, expected `callable` or `uncallable`.", + unknown + ), )) } } diff --git a/tests/fail/07-sealed-function.rs b/tests/fail/07-sealed-function.rs index 6e389e4..8783065 100644 --- a/tests/fail/07-sealed-function.rs +++ b/tests/fail/07-sealed-function.rs @@ -1,29 +1,29 @@ pub mod inner { - use sealed::sealed; + use sealed::sealed; - #[sealed] - pub trait PartialSealed { - #[seal] - fn a(); + #[sealed] + pub trait PartialSealed { + #[seal(uncallable)] + fn a(); - fn b(); - } + fn b(); + } - pub struct A; + pub struct A; - #[sealed] - impl PartialSealed for A { - #[seal] - fn a() {} + #[sealed] + impl PartialSealed for A { + #[seal(uncallable)] + fn a() {} - fn b() {} - } + fn b() {} + } } use crate::inner::PartialSealed; fn main() { - inner::A::a(); - inner::A::a(Token); - inner::A::b(); + inner::A::a(); + inner::A::a(Token); + inner::A::b(); } diff --git a/tests/fail/07-sealed-function.stderr b/tests/fail/07-sealed-function.stderr index 2e217a8..e89c10e 100644 --- a/tests/fail/07-sealed-function.stderr +++ b/tests/fail/07-sealed-function.stderr @@ -1,5 +1,5 @@ error[E0425]: cannot find value `Token` in this scope - --> tests/fail/07-sealed-function.rs:27:14 + --> tests/fail/07-sealed-function.rs:27:17 | 27 | inner::A::a(Token); | ^^^^^ not found in this scope @@ -10,7 +10,7 @@ help: consider importing this unit struct | error[E0061]: this function takes 1 argument but 0 arguments were supplied - --> tests/fail/07-sealed-function.rs:26:2 + --> tests/fail/07-sealed-function.rs:26:5 | 26 | inner::A::a(); | ^^^^^^^^^^^-- supplied 0 arguments @@ -18,7 +18,7 @@ error[E0061]: this function takes 1 argument but 0 arguments were supplied | expected 1 argument | note: associated function defined here - --> tests/fail/07-sealed-function.rs:7:6 + --> tests/fail/07-sealed-function.rs:7:12 | 7 | fn a(); | ^ diff --git a/tests/fail/09-partial-argument.rs b/tests/fail/09-partial-argument.rs index 4d83af4..706e06d 100644 --- a/tests/fail/09-partial-argument.rs +++ b/tests/fail/09-partial-argument.rs @@ -2,13 +2,13 @@ use sealed::sealed; #[sealed(partial)] pub trait A { - #[seal] - fn sealed() {} + #[seal(uncallable)] + fn sealed() {} } #[sealed(partial)] pub trait B { - fn not_sealed(); + fn not_sealed(); } fn main() {} diff --git a/tests/fail/10-partial-without-default.rs b/tests/fail/10-partial-without-default.rs index 30c2938..5887ca4 100644 --- a/tests/fail/10-partial-without-default.rs +++ b/tests/fail/10-partial-without-default.rs @@ -2,8 +2,8 @@ use sealed::sealed; #[sealed(partial)] pub trait A { - #[seal] - fn sealed(); + #[seal(uncallable)] + fn sealed(); } fn main() {} diff --git a/tests/fail/10-partial-without-default.stderr b/tests/fail/10-partial-without-default.stderr index 0cf0dc8..49836ca 100644 --- a/tests/fail/10-partial-without-default.stderr +++ b/tests/fail/10-partial-without-default.stderr @@ -1,5 +1,5 @@ error: This function is sealed from implementation, but it does not have a default implementation. This effectively seals the entire trait, which would be clearer to do by not having the trait seal be partial. - --> tests/fail/10-partial-without-default.rs:6:5 + --> tests/fail/10-partial-without-default.rs:6:8 | 6 | fn sealed(); | ^^^^^^ diff --git a/tests/pass/14-sealed-functions.rs b/tests/pass/14-sealed-functions.rs index 5381fa9..b6f0b8e 100644 --- a/tests/pass/14-sealed-functions.rs +++ b/tests/pass/14-sealed-functions.rs @@ -2,30 +2,30 @@ use sealed::sealed; #[sealed] pub trait PartialSealed { - #[seal] - fn default(_value: String) {} + #[seal(uncallable)] + fn default(_value: String) {} - #[seal] - fn no_default(value: u8); + #[seal(uncallable)] + fn no_default(value: u8); } pub struct A; #[sealed] impl PartialSealed for A { - #[seal] - fn no_default(_value: u8) {} + #[seal(uncallable)] + fn no_default(_value: u8) {} } pub struct B; #[sealed] impl PartialSealed for B { - #[seal] - fn default(_value: String) {} + #[seal(uncallable)] + fn default(_value: String) {} - #[seal] - fn no_default(_value: u8) {} + #[seal(uncallable)] + fn no_default(_value: u8) {} } fn main() {} From 38cff225da3b2d23ed3a85ed8ff4be7bbd7fe6b9 Mon Sep 17 00:00:00 2001 From: TheLazyDutchman Date: Mon, 18 Dec 2023 18:23:01 +0100 Subject: [PATCH 09/10] Actually make partial work --- src/lib.rs | 206 ++++++++++-------- tests/fail/11-implementing-sealed-function.rs | 32 +++ .../11-implementing-sealed-function.stderr | 38 ++++ tests/fail/12-calling-sealed-function.rs | 25 +++ tests/fail/12-calling-sealed-function.stderr | 27 +++ 5 files changed, 232 insertions(+), 96 deletions(-) create mode 100644 tests/fail/11-implementing-sealed-function.rs create mode 100644 tests/fail/11-implementing-sealed-function.stderr create mode 100644 tests/fail/12-calling-sealed-function.rs create mode 100644 tests/fail/12-calling-sealed-function.stderr diff --git a/src/lib.rs b/src/lib.rs index 4e10915..8cd150d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -147,94 +147,24 @@ pub fn sealed(args: TokenStream, input: TokenStream) -> TokenStream { // Care for https://gist.github.com/Kestrer/8c05ebd4e0e9347eb05f265dfb7252e1#procedural-macros-support-renaming-the-crate fn parse_sealed_trait(mut item_trait: syn::ItemTrait, args: TraitArguments) -> TokenStream2 { let trait_ident = &item_trait.ident.unraw(); - let trait_generics = &item_trait.generics; let seal = seal_name(trait_ident); let vis = &args.visibility; - let (_, ty_generics, where_clause) = trait_generics.split_for_impl(); - item_trait - .supertraits - .push(parse_quote!( #seal::Sealed #ty_generics )); - - let mut wrappers = Vec::::new(); - - let mut all_implementable = true; - let mut none_implementable = true; - - for item in &mut item_trait.items { - if let syn::TraitItem::Fn(item) = item { - let input = match parse_seal_attribute(&item.attrs) { - Ok(input) => input, - Err(err) => return err.into_compile_error().into(), - }; - - let input = match input { - Some(value) => value, - None => { - none_implementable = false; - continue; - } - }; - - item.attrs.retain(|x| !x.path().is_ident("seal")); - - match parse_function_definition(trait_ident, input, item) { - Ok(Some(_)) if !args.partial => { - return syn::Error::new( - item.sig.ident.span(), - "Sealing a function from implementation but allowing it to be called is already done by sealing the trait itself. \ - If you want to seal this function, but not others, \ - consider adding `partial` to the `sealed` attr on the trait: `#[sealed(partial)]`" - ) - .into_compile_error() - .into() - } - Ok(_) if args.partial && item.default.is_none() => { - return syn::Error::new( - item.sig.ident.span(), - "This function is sealed from implementation, \ - but it does not have a default implementation. \ - This effectively seals the entire trait, \ - which would be clearer to do by not having the trait seal be partial." - ).into_compile_error().into() - } - Ok(Some(wrapper)) => { - all_implementable = false; - wrappers.push(wrapper.into()) - }, - Ok(None) => all_implementable = false, - Err(err) => { - return err - .into_compile_error() - .into() - } - } - } + match handle_function_definitions(trait_ident, &args, &mut item_trait.items) { + Ok(wrappers) => item_trait.items.extend(wrappers), + Err(err) => return err.to_compile_error().into(), } - if args.partial && all_implementable { - return syn::Error::new( - item_trait.ident.span(), - "This trait is partially sealed, \ - however none of its methods are sealed, so it does nothing. \ - Either seal a function or remove `partial` from the `sealed` attribute.", - ) - .to_compile_error() - .into(); - } + let trait_generics = &item_trait.generics; + let (_, ty_generics, where_clause) = trait_generics.split_for_impl(); - if args.partial && none_implementable { - return syn::Error::new( - item_trait.ident.span(), - "This trait is partially sealed, \ - however none of its methods are implementable, so the `partial` argument does nothing.", - ) - .into_compile_error() - .into(); + // A partial seal does not seal the trait, just some of the methods. + if !args.partial { + item_trait + .supertraits + .push(parse_quote!( #seal::Sealed #ty_generics )); } - item_trait.items.extend(wrappers); - let mod_code = if args.erased { let lifetimes = trait_generics.lifetimes(); let const_params = trait_generics.const_params(); @@ -289,20 +219,7 @@ fn parse_sealed_impl(mut item_impl: syn::ItemImpl) -> syn::Result // the bounds may break in the `#seal` submodule. let (trait_generics, _, where_clauses) = item_impl.generics.split_for_impl(); - for item in &mut item_impl.items { - if let syn::ImplItem::Fn(item) = item { - let input = parse_seal_attribute(&item.attrs)?; - - let input = match input { - Some(value) => value, - None => continue, - }; - - item.attrs.retain(|x| !x.path().is_ident("seal")); - - parse_function_implementation(&ident, input, item)?; - } - } + handle_function_implementations(&ident, &mut item_impl.items)?; Ok(quote! { #[automatically_derived] @@ -342,9 +259,84 @@ fn parse_seal_attribute( Ok(None) } +fn handle_function_definitions( + trait_ident: &syn::Ident, + args: &TraitArguments, + item_trait: &mut [syn::TraitItem], +) -> syn::Result> { + let mut wrappers = Vec::::new(); + + let mut all_implementable = true; + let mut none_implementable = true; + + for item in item_trait { + if let syn::TraitItem::Fn(item) = item { + let input = match parse_seal_attribute(&item.attrs) { + Ok(input) => input, + Err(err) => return Err(err), + }; + + let input = match input { + Some(value) => value, + None => { + none_implementable = false; + continue; + } + }; + + item.attrs.retain(|x| !x.path().is_ident("seal")); + + match handle_function_definition(trait_ident, input, item) { + Ok(Some(_)) if !args.partial => { + return Err(syn::Error::new( + item.sig.ident.span(), + "Sealing a function from implementation but allowing it to be called is already done by sealing the trait itself. \ + If you want to seal this function, but not others, \ + consider adding `partial` to the `sealed` attr on the trait: `#[sealed(partial)]`" + )); + } + Ok(_) if args.partial && item.default.is_none() => { + return Err(syn::Error::new( + item.sig.ident.span(), + "This function is sealed from implementation, \ + but it does not have a default implementation. \ + This effectively seals the entire trait, \ + which would be clearer to do by not having the trait seal be partial.", + )); + } + Ok(Some(wrapper)) => { + all_implementable = false; + wrappers.push(wrapper.into()) + } + Ok(None) => all_implementable = false, + Err(err) => return Err(err), + } + } + } + + if args.partial && all_implementable { + return Err(syn::Error::new( + trait_ident.span(), + "This trait is partially sealed, \ + however none of its methods are sealed, so it does nothing. \ + Either seal a function or remove `partial` from the `sealed` attribute.", + )); + } + + if args.partial && none_implementable { + return Err(syn::Error::new( + trait_ident.span(), + "This trait is partially sealed, \ + however none of its methods are implementable, so the `partial` argument does nothing.", + )); + } + + Ok(wrappers) +} + /// Seal a specific function (to avoid people overwriting behaviour of /// specific functions, or even calling them) -fn parse_function_definition( +fn handle_function_definition( trait_ident: &syn::Ident, args: SealAttributeArguments, function: &mut syn::TraitItemFn, @@ -375,7 +367,29 @@ fn parse_function_definition( Ok(value) } -fn parse_function_implementation( +fn handle_function_implementations( + ident: &syn::Ident, + items: &mut [syn::ImplItem], +) -> syn::Result<()> { + for item in items { + if let syn::ImplItem::Fn(item) = item { + let input = parse_seal_attribute(&item.attrs)?; + + let input = match input { + Some(value) => value, + None => continue, + }; + + item.attrs.retain(|x| !x.path().is_ident("seal")); + + handle_function_implementation(&ident, input, item)?; + } + } + + Ok(()) +} + +fn handle_function_implementation( trait_ident: &syn::Ident, args: SealAttributeArguments, function: &mut syn::ImplItemFn, diff --git a/tests/fail/11-implementing-sealed-function.rs b/tests/fail/11-implementing-sealed-function.rs new file mode 100644 index 0000000..c099e78 --- /dev/null +++ b/tests/fail/11-implementing-sealed-function.rs @@ -0,0 +1,32 @@ +mod inner { + use sealed::sealed; + + #[sealed(partial)] + pub trait A { + #[seal(uncallable)] + fn sealed() {} + + fn unsealed(); + } +} + +use inner::A; + +struct B; + +impl A for B { + fn sealed() {} + fn unsealed() {} +} + +struct C; + +impl A for C { + fn sealed(_token: Token) {} + fn unsealed() {} +} + +fn main() { + B::sealed(); + B::unsealed(); +} diff --git a/tests/fail/11-implementing-sealed-function.stderr b/tests/fail/11-implementing-sealed-function.stderr new file mode 100644 index 0000000..e5a39be --- /dev/null +++ b/tests/fail/11-implementing-sealed-function.stderr @@ -0,0 +1,38 @@ +error[E0412]: cannot find type `Token` in this scope + --> tests/fail/11-implementing-sealed-function.rs:25:23 + | +25 | fn sealed(_token: Token) {} + | ^^^^^ not found in this scope + | +help: consider importing one of these items + | +13 | use crate::inner::__seal_a::Token; + | +13 | use syn::token::Token; + | + +error[E0050]: method `sealed` has 0 parameters but the declaration in trait `A::sealed` has 1 + --> tests/fail/11-implementing-sealed-function.rs:18:5 + | +4 | #[sealed(partial)] + | ------------------ trait requires 1 parameter +... +18 | fn sealed() {} + | ^^^^^^^^^^^ expected 1 parameter, found 0 + +error[E0061]: this function takes 1 argument but 0 arguments were supplied + --> tests/fail/11-implementing-sealed-function.rs:30:5 + | +30 | B::sealed(); + | ^^^^^^^^^-- supplied 0 arguments + | | + | expected 1 argument + | +note: associated function defined here + --> tests/fail/11-implementing-sealed-function.rs:7:12 + | +4 | / #[sealed(partial)] +5 | | pub trait A { +6 | | #[seal(uncallable)] +7 | | fn sealed() {} + | |____________^^^^^- diff --git a/tests/fail/12-calling-sealed-function.rs b/tests/fail/12-calling-sealed-function.rs new file mode 100644 index 0000000..3e08c43 --- /dev/null +++ b/tests/fail/12-calling-sealed-function.rs @@ -0,0 +1,25 @@ +mod inner { + use sealed::sealed; + + #[sealed(partial)] + pub trait A { + #[seal(uncallable)] + fn sealed() {} + + fn unsealed(); + } +} + +use inner::A; + +struct B; + +impl A for B { + fn unsealed() {} +} + +fn main() { + B::sealed(); + B::sealed(Token); + B::unsealed(); +} diff --git a/tests/fail/12-calling-sealed-function.stderr b/tests/fail/12-calling-sealed-function.stderr new file mode 100644 index 0000000..89fbcf7 --- /dev/null +++ b/tests/fail/12-calling-sealed-function.stderr @@ -0,0 +1,27 @@ +error[E0425]: cannot find value `Token` in this scope + --> tests/fail/12-calling-sealed-function.rs:23:15 + | +23 | B::sealed(Token); + | ^^^^^ not found in this scope + | +help: consider importing this unit struct + | +13 | use crate::inner::__seal_a::Token; + | + +error[E0061]: this function takes 1 argument but 0 arguments were supplied + --> tests/fail/12-calling-sealed-function.rs:22:5 + | +22 | B::sealed(); + | ^^^^^^^^^-- supplied 0 arguments + | | + | expected 1 argument + | +note: associated function defined here + --> tests/fail/12-calling-sealed-function.rs:7:12 + | +4 | / #[sealed(partial)] +5 | | pub trait A { +6 | | #[seal(uncallable)] +7 | | fn sealed() {} + | |____________^^^^^- From 0e541a478d722589e051e6b816027ac48b2237a0 Mon Sep 17 00:00:00 2001 From: TheLazyDutchman Date: Tue, 19 Dec 2023 15:41:08 +0100 Subject: [PATCH 10/10] Trying to fix demo --- demo/src/main.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/demo/src/main.rs b/demo/src/main.rs index d317a81..de8645b 100644 --- a/demo/src/main.rs +++ b/demo/src/main.rs @@ -1,8 +1,6 @@ use std::marker::PhantomData; -use sealed::sealed; - -#[sealed] +#[sealed::sealed] pub trait DroneState {} pub struct Drone @@ -15,15 +13,15 @@ where } pub struct Idle; -#[sealed] +#[sealed::sealed] impl DroneState for Idle {} pub struct Hovering; -#[sealed] +#[sealed::sealed] impl DroneState for Hovering {} pub struct Flying; -#[sealed] +#[sealed::sealed] impl DroneState for Flying {} impl Drone {