diff --git a/Cargo.toml b/Cargo.toml index 225a4a5..7ed3f6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sealed" -version = "0.1.3" +version = "0.2.0-rc3" authors = ["José Duarte "] license = "MIT OR Apache-2.0" description = "Macro for sealing traits and structures" @@ -10,14 +10,11 @@ categories = ["development-tools", "rust-patterns"] keywords = ["proc_macro", "sealed", "future-proofing"] readme = "README.md" edition = "2018" -exclude = [ - "images/*" -] +exclude = ["images/*"] +resolver = "2" [workspace] -members = [ - "demo" -] +members = ["demo"] [lib] proc-macro = true @@ -29,4 +26,5 @@ syn = { version = "1.0", features = ["extra-traits"] } [dependencies] syn = { version = "1.0", features = ["full"] } quote = "1.0" -proc-macro2 = "1.0" \ No newline at end of file +proc-macro2 = "1.0" +heck = "0.3" diff --git a/README.md b/README.md index 96f7773..e418651 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ as described in the Rust API Guidelines [[1](https://rust-lang.github.io/api-gui ```toml [dependencies] -sealed = "0.1" +sealed = "0.2.0-rc3" ``` ## Example @@ -18,7 +18,7 @@ sealed = "0.1" In the following code structs `A` and `B` implement the sealed trait `T`, the `C` struct, which is not sealed, will error during compilation. -You can see a demo in [`demo/`](demo/). +Examples are available in [`examples/`](examples/), you can also see a demo in [`demo/`](demo/). ```rust use sealed::sealed; @@ -26,14 +26,14 @@ use sealed::sealed; #[sealed] trait T {} -#[sealed] pub struct A; +#[sealed] impl T for A {} -#[sealed] pub struct B; +#[sealed] impl T for B {} pub struct C; @@ -45,25 +45,66 @@ fn main() { } ``` +## Attributes + +This is the list of attributes that can be used along `#[sealed]`: +- `#[sealed]`: the main attribute macro, without attribute parameters. +- `#[sealed(erase)]`: this option turns on bound erasure. This is useful when using the `#[sealed]` macro inside a function. +For an example, see [`bound-erasure-fn`](tests/pass/08-bound-erasure-fn.rs). + ## Details -The macro generates a `private` module when attached to a `trait` -(this raises the limitation that the `#[sealed]` macro can only be added to a single trait per module), -when attached to a `struct` the generated code simply implements the sealed trait for the respective structure. +The `#[sealed]` attribute can be attached to either a `trait` or an `impl`. +It supports: +- Several traits per module +- Generic parameters +- Foreign types +- Blanket `impl`s + +## Expansion -### Expansion +### Input ```rust -// #[sealed] -// trait T {} -trait T: private::Sealed {} -mod private { - trait Sealed {} +use sealed::sealed; + +#[sealed] +pub trait T {} + +pub struct A; +pub struct B(i32); + +#[sealed] +impl T for A {} +#[sealed] +impl T for B {} + +fn main() { + return; +} +``` + +### Expanded + +```rust +use sealed::sealed; + +pub(crate) mod __seal_t { + pub trait Sealed {} } +pub trait T: __seal_t::Sealed {} -// #[sealed] -// pub struct A; pub struct A; -impl private::Sealed for A {} +pub struct B(i32); + +impl __seal_t::Sealed for A {} +impl T for A {} + +impl __seal_t::Sealed for B {} +impl T for B {} + +fn main() { + return; +} ``` \ No newline at end of file diff --git a/demo/src/main.rs b/demo/src/main.rs index 9fbbade..57e843f 100644 --- a/demo/src/main.rs +++ b/demo/src/main.rs @@ -12,16 +12,16 @@ where state: PhantomData, } -#[sealed] pub struct Idle; +#[sealed] impl DroneState for Idle {} -#[sealed] pub struct Hovering; +#[sealed] impl DroneState for Hovering {} -#[sealed] pub struct Flying; +#[sealed] impl DroneState for Flying {} impl Drone { diff --git a/examples/bound-erasure-fn.rs b/examples/bound-erasure-fn.rs new file mode 100644 index 0000000..a749a59 --- /dev/null +++ b/examples/bound-erasure-fn.rs @@ -0,0 +1,14 @@ +use sealed::sealed; + +fn main() { + trait Foo {} + trait Bar {} + + #[sealed(erase)] + trait Trait {} + + struct Implementor {} + + #[sealed(erase)] + impl Trait for Implementor where T: Foo + Bar {} +} diff --git a/examples/bound-erasure.rs b/examples/bound-erasure.rs new file mode 100644 index 0000000..572d151 --- /dev/null +++ b/examples/bound-erasure.rs @@ -0,0 +1,5 @@ +use sealed::sealed; +trait Foo {} +#[sealed(erase)] +trait Trait {} +fn main() {} diff --git a/examples/generics.rs b/examples/generics.rs new file mode 100644 index 0000000..b858e32 --- /dev/null +++ b/examples/generics.rs @@ -0,0 +1,15 @@ +// Test provided in #4 +// https://github.com/jmg-duarte/sealed-rs/issues/4 + +use sealed::sealed; + +#[sealed] +pub trait Set {} + +#[sealed] +impl Set> for T {} + +#[sealed] +impl Set> for Option {} + +fn main() {} diff --git a/examples/nesting.rs b/examples/nesting.rs new file mode 100644 index 0000000..202b282 --- /dev/null +++ b/examples/nesting.rs @@ -0,0 +1,27 @@ +use sealed::sealed; + +mod lets { + pub mod attempt { + pub mod some { + pub mod nesting { + use sealed::sealed; + #[sealed] + pub trait LongerSnakeCaseType {} + } + } + } +} + +pub struct A; + +pub struct B(i32); + +#[sealed] +impl lets::attempt::some::nesting::LongerSnakeCaseType for A {} + +#[sealed] +impl lets::attempt::some::nesting::LongerSnakeCaseType for B {} + +fn main() { + return; +} diff --git a/examples/raw-ident.rs b/examples/raw-ident.rs new file mode 100644 index 0000000..7e7f880 --- /dev/null +++ b/examples/raw-ident.rs @@ -0,0 +1,12 @@ +use sealed::sealed; + +#[sealed] +pub trait r#Pub {} + +#[sealed] +impl r#Pub> for T {} + +#[sealed] +impl r#Pub> for Option {} + +fn main() {} diff --git a/examples/simple.rs b/examples/simple.rs new file mode 100644 index 0000000..3c1407f --- /dev/null +++ b/examples/simple.rs @@ -0,0 +1,16 @@ +use sealed::sealed; + +#[sealed] +pub trait T {} + +pub struct A; +pub struct B(i32); + +#[sealed] +impl T for A {} +#[sealed] +impl T for B {} + +fn main() { + return; +} diff --git a/examples/syn.rs b/examples/syn.rs new file mode 100644 index 0000000..51e5637 --- /dev/null +++ b/examples/syn.rs @@ -0,0 +1,44 @@ +// Example provided in #4 +// https://github.com/jmg-duarte/sealed-rs/issues/4 + +use proc_macro2::*; +use sealed::sealed; +use syn::spanned::Spanned; + +#[sealed] +pub trait AsSpan { + fn as_span(&self) -> Span; +} +// expands to: +// pub trait AsSpan: __seal_as_span::Sealed { +// fn as_span(&self) -> Span; +// } +// mod __seal_as_span { +// pub trait Sealed {} +// } + +#[sealed] +impl AsSpan for Span { + fn as_span(&self) -> Self { + *self + } +} +// expands to: +// impl AsSpan for Span { // foreign type, cannot place #[sealed] +// fn as_span(&self) -> Self { *self } +// } +// impl __seal_as_span::Sealed for Span {} + +#[sealed] +impl AsSpan for &T { + fn as_span(&self) -> Span { + self.span() + } +} +// expands to: +// impl AsSpan for &T { +// fn as_span(&self) -> Span { self.span() } +// } +// impl __seal_as_span::Sealed for &T {} + +fn main() {} diff --git a/src/lib.rs b/src/lib.rs index fa04aa8..9fc04db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,23 +62,51 @@ //! impl private::Sealed for A {} //! ``` +use heck::SnakeCase; use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; use quote::quote; -use syn::{parse_macro_input, parse_quote}; +use syn::{ext::IdentExt, parse_macro_input, parse_quote}; + +const TRAIT_ERASURE_ARG_IDENT: &str = "erase"; #[proc_macro_attribute] -pub fn sealed(_args: TokenStream, input: TokenStream) -> TokenStream { +pub fn sealed(args: TokenStream, input: TokenStream) -> TokenStream { + let erased = parse_macro_input!(args as Option); let input = parse_macro_input!(input as syn::Item); - TokenStream::from(match parse_sealed(input) { - Ok(ts) => ts, - Err(err) => err.to_compile_error(), - }) + if let Some(erased) = erased { + if erased == TRAIT_ERASURE_ARG_IDENT { + match parse_sealed(input, true) { + Ok(ts) => ts, + Err(err) => err.to_compile_error(), + } + } else { + syn::Error::new_spanned( + erased, + format!( + "The only accepted argument is `{}`.", + TRAIT_ERASURE_ARG_IDENT + ), + ) + .to_compile_error() + } + } else { + match parse_sealed(input, false) { + Ok(ts) => ts, + Err(err) => err.to_compile_error(), + } + } + .into() } -fn parse_sealed(item: syn::Item) -> syn::Result { +fn seal_name(seal: D) -> syn::Ident { + ::quote::format_ident!("__seal_{}", &seal.to_string().to_snake_case()) +} + +fn parse_sealed(item: syn::Item, erase: bool) -> syn::Result { match item { - syn::Item::Struct(item_struct) => parse_sealed_struct(item_struct), - syn::Item::Trait(item_trait) => parse_sealed_trait(item_trait), + syn::Item::Impl(item_impl) => parse_sealed_impl(&item_impl), + syn::Item::Trait(item_trait) => Ok(parse_sealed_trait(item_trait, erase)), _ => Err(syn::Error::new( proc_macro2::Span::call_site(), "expected struct or trait", @@ -87,21 +115,73 @@ fn parse_sealed(item: syn::Item) -> syn::Result { } // Care for https://gist.github.com/Koxiaet/8c05ebd4e0e9347eb05f265dfb7252e1#procedural-macros-support-renaming-the-crate -fn parse_sealed_struct(strct: syn::ItemStruct) -> syn::Result { - let ident = &strct.ident; - Ok(quote!( - #strct - impl private::Sealed for #ident {} - )) +fn parse_sealed_trait(mut item_trait: syn::ItemTrait, erase: bool) -> TokenStream2 { + let trait_ident = &item_trait.ident.unraw(); + let trait_generics = &item_trait.generics; + let seal = seal_name(trait_ident); + + let type_params = trait_generics + .type_params() + .map(|syn::TypeParam { ident, .. }| -> syn::TypeParam { parse_quote!( #ident ) }); + + item_trait + .supertraits + .push(parse_quote!(#seal::Sealed <#(#type_params, )*>)); + + if erase { + 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!( + #[automatically_derived] + pub(crate) mod #seal { + pub trait Sealed< #(#lifetimes ,)* #(#type_params ,)* #(#const_params ,)* > {} + } + #item_trait + ) + } else { + quote!( + #[automatically_derived] + pub(crate) mod #seal { + use super::*; + pub trait Sealed #trait_generics {} + } + #item_trait + ) + } } -// Care for https://gist.github.com/Koxiaet/8c05ebd4e0e9347eb05f265dfb7252e1#procedural-macros-support-renaming-the-crate -fn parse_sealed_trait(mut trt: syn::ItemTrait) -> syn::Result { - trt.supertraits.push(parse_quote!(private::Sealed)); - Ok(quote!( - #trt - mod private { - pub trait Sealed {} - } - )) +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 implentation 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 + }) } diff --git a/tests/compile/01.rs b/tests/fail/01-general.rs similarity index 98% rename from tests/compile/01.rs rename to tests/fail/01-general.rs index d9f368a..7e6ad4d 100644 --- a/tests/compile/01.rs +++ b/tests/fail/01-general.rs @@ -1,18 +1,22 @@ use sealed::sealed; -#[sealed] pub struct A; -#[sealed] + pub struct B { field_1: i32, } + pub struct C; #[sealed] trait T {} +#[sealed] impl T for A {} + +#[sealed] impl T for B {} + impl T for C {} fn main() { diff --git a/tests/compile/01.stderr b/tests/fail/01-general.stderr similarity index 82% rename from tests/compile/01.stderr rename to tests/fail/01-general.stderr index 5c38aa6..a8ca728 100644 --- a/tests/compile/01.stderr +++ b/tests/fail/01-general.stderr @@ -1,10 +1,10 @@ error[E0277]: the trait bound `C: Sealed` is not satisfied - --> $DIR/01.rs:16:6 + --> $DIR/01-general.rs:20:6 | 11 | #[sealed] | --------- required by this bound in `T` 12 | trait T {} | - required by a bound in this ... -16 | impl T for C {} +20 | impl T for C {} | ^ the trait `Sealed` is not implemented for `C` diff --git a/tests/fail/02-nesting.rs b/tests/fail/02-nesting.rs new file mode 100644 index 0000000..a5d72fc --- /dev/null +++ b/tests/fail/02-nesting.rs @@ -0,0 +1,30 @@ +use sealed::sealed; + +mod lets { + pub mod attempt { + pub mod some { + pub mod nesting { + use sealed::sealed; + #[sealed] + pub trait T {} + } + } + } +} + +pub struct A; +pub struct B { + field_1: i32, +} +pub struct C; + +#[sealed] +impl lets::attempt::some::nesting::T for A {} +#[sealed] +impl lets::attempt::some::nesting::T for B {} +// fails to compile +impl lets::attempt::some::nesting::T for C {} + +fn main() { + return; +} diff --git a/tests/fail/02-nesting.stderr b/tests/fail/02-nesting.stderr new file mode 100644 index 0000000..3097838 --- /dev/null +++ b/tests/fail/02-nesting.stderr @@ -0,0 +1,10 @@ +error[E0277]: the trait bound `C: Sealed` is not satisfied + --> $DIR/02-nesting.rs:26:6 + | +8 | #[sealed] + | --------- required by this bound in `T` +9 | pub trait T {} + | - required by a bound in this +... +26 | impl lets::attempt::some::nesting::T for C {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Sealed` is not implemented for `C` diff --git a/tests/pass/01-long-name.rs b/tests/pass/01-long-name.rs new file mode 100644 index 0000000..2cb4ca7 --- /dev/null +++ b/tests/pass/01-long-name.rs @@ -0,0 +1,28 @@ +use sealed::sealed; + +mod lets { + pub mod attempt { + pub mod some { + pub mod nesting { + use sealed::sealed; + #[sealed] + pub trait LongerSnakeCaseType {} + } + } + } +} + +pub struct A; + +pub struct B { + field_1: i32, +} + +#[sealed] +impl lets::attempt::some::nesting::LongerSnakeCaseType for A {} +#[sealed] +impl lets::attempt::some::nesting::LongerSnakeCaseType for B {} + +fn main() { + return; +} diff --git a/tests/pass/02-nesting.rs b/tests/pass/02-nesting.rs new file mode 100644 index 0000000..271c845 --- /dev/null +++ b/tests/pass/02-nesting.rs @@ -0,0 +1,29 @@ +use sealed::sealed; + +mod lets { + pub mod attempt { + pub mod some { + pub mod nesting { + use sealed::sealed; + #[sealed] + pub trait T {} + } + } + } +} + +pub struct A; + +pub struct B { + field_1: i32, +} + +#[sealed] +impl lets::attempt::some::nesting::T for A {} + +#[sealed] +impl lets::attempt::some::nesting::T for B {} + +fn main() { + return; +} diff --git a/tests/pass/03-generics.rs b/tests/pass/03-generics.rs new file mode 100644 index 0000000..be63385 --- /dev/null +++ b/tests/pass/03-generics.rs @@ -0,0 +1,21 @@ +// Test provided in #4 +// https://github.com/jmg-duarte/sealed-rs/issues/4 + +use sealed::sealed; + +#[sealed] +pub trait Set {} +// pub trait Set: __seal_for_set::Sealed {} +// mod __seal_for_set { +// pub trait Sealed {} +// } + +#[sealed] +impl Set> for T {} +// impl __seal_for_set::Sealed> for T {} + +#[sealed] +impl Set> for Option {} +// impl __seal_for_set::Sealed> for Option {} + +fn main() {} diff --git a/tests/pass/04-multiple-traits.rs b/tests/pass/04-multiple-traits.rs new file mode 100644 index 0000000..a2eb166 --- /dev/null +++ b/tests/pass/04-multiple-traits.rs @@ -0,0 +1,24 @@ +use sealed::sealed; + +#[sealed] +pub trait T {} + +#[sealed] +pub trait T1 {} + +pub struct A; +pub struct B(i32); + +#[sealed] +impl T for A {} +#[sealed] +impl T for B {} + +#[sealed] +impl T1 for A {} +#[sealed] +impl T1 for B {} + +fn main() { + return; +} diff --git a/tests/pass/05-raw-ident.rs b/tests/pass/05-raw-ident.rs new file mode 100644 index 0000000..7e7f880 --- /dev/null +++ b/tests/pass/05-raw-ident.rs @@ -0,0 +1,12 @@ +use sealed::sealed; + +#[sealed] +pub trait r#Pub {} + +#[sealed] +impl r#Pub> for T {} + +#[sealed] +impl r#Pub> for Option {} + +fn main() {} diff --git a/tests/pass/06-bounds.rs b/tests/pass/06-bounds.rs new file mode 100644 index 0000000..572d151 --- /dev/null +++ b/tests/pass/06-bounds.rs @@ -0,0 +1,5 @@ +use sealed::sealed; +trait Foo {} +#[sealed(erase)] +trait Trait {} +fn main() {} diff --git a/tests/pass/07-bound-erasure.rs b/tests/pass/07-bound-erasure.rs new file mode 100644 index 0000000..166a0b5 --- /dev/null +++ b/tests/pass/07-bound-erasure.rs @@ -0,0 +1,18 @@ +use sealed::sealed; + +trait Foo {} +trait Bar {} + +#[sealed(erase)] +trait Trait +where + T: ?Sized + Foo, +{ +} + +struct Implementor {} + +#[sealed(erase)] +impl Trait for Implementor where T: Foo + Bar {} + +fn main() {} diff --git a/tests/pass/08-bound-erasure-fn.rs b/tests/pass/08-bound-erasure-fn.rs new file mode 100644 index 0000000..9be502e --- /dev/null +++ b/tests/pass/08-bound-erasure-fn.rs @@ -0,0 +1,18 @@ +use sealed::sealed; + +fn main() { + trait Foo {} + trait Bar {} + + #[sealed(erase)] + trait Trait + where + T: ?Sized + Foo, + { + } + + struct Implementor {} + + #[sealed(erase)] + impl Trait for Implementor where T: Foo + Bar {} +} diff --git a/tests/test.rs b/tests/test.rs index 02dd27c..e097918 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,5 +1,6 @@ #[test] fn compile_test() { let t = trybuild::TestCases::new(); - t.compile_fail("tests/compile/01.rs"); + t.pass("tests/pass/*.rs"); + t.compile_fail("tests/fail/*.rs"); }