Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
fb569d3
Rework the way sealed items are handled
May 6, 2021
689b6cc
Implement snake case
May 6, 2021
eccd386
Add generic handling
May 6, 2021
9818f80
Update the dependencies
May 6, 2021
4ea39fb
Update the demo
May 6, 2021
8e20eed
Update the examples
May 6, 2021
5822901
Remove debug info
May 6, 2021
639f677
Update examples
May 6, 2021
52cc65d
Update tests
May 6, 2021
ec7f5f4
Add support for generics
May 6, 2021
55ee9e8
Add tests and examples for generics
May 6, 2021
e4560e3
Remove old comment
May 6, 2021
9d3f271
Bump version
May 6, 2021
713517c
Add tests for multiple traits
May 6, 2021
bb8969c
Update README
May 6, 2021
5e10596
Format
May 6, 2021
c322e7e
More format
May 6, 2021
0ec94e8
Fix version
May 6, 2021
68e8107
Use split_for_impl to avoid problems with where clauses
May 6, 2021
9ace5a6
Replace the macro with a function
May 6, 2021
568b426
Remove the heck dep
May 6, 2021
5c00a61
Remove comment
May 7, 2021
6c41596
Replace Display with fully qualified name
May 7, 2021
46e9472
Replace proc_macro2::TokenStream with TokenStream2
May 7, 2021
5569092
Bump versions
May 7, 2021
f336c99
Revert back to heck
May 7, 2021
170df32
Address raw idents
May 7, 2021
7e51a1c
Format Cargo.toml
May 7, 2021
ec25165
Address clippy::unnecessary-wraps
May 7, 2021
99c5141
Update examples comments
May 7, 2021
07900bd
Remove bound usage since it can break #seal
May 7, 2021
1c787dd
Add automatically_derived attribute
May 7, 2021
18f88cc
Typo fix
May 7, 2021
9b53b0d
Change resolver to v2
May 7, 2021
a158e38
Address raw idents
May 7, 2021
a9076a4
Make parse_sealed_impl return early
May 7, 2021
d8c1cd0
Shorten the seal
May 7, 2021
71f715c
Format
May 7, 2021
6330912
Update mini-docs
May 7, 2021
61c1af1
Apply clippy pedantic suggestions
May 7, 2021
ee0e196
Format tests
May 7, 2021
ed37d2a
Bring back the where clauses
May 10, 2021
f8e73fc
Add example
May 11, 2021
cb3eec6
Update example
May 11, 2021
b329e32
WIP - start implementing the erase arg
May 11, 2021
b82aa9a
Fix bug in erase
May 12, 2021
02dc84c
Update examples
May 12, 2021
81b845c
Add tests
May 12, 2021
234d893
Update library
May 12, 2021
7924217
Update README
May 12, 2021
d563403
Bump version
May 12, 2021
0609d80
Remove example with nightly features
May 12, 2021
32a1507
Remove feature
May 14, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sealed"
version = "0.1.3"
version = "0.2.0-rc2"
authors = ["José Duarte <jmg.duarte@campus.fct.unl.pt>"]
license = "MIT OR Apache-2.0"
description = "Macro for sealing traits and structures"
Expand All @@ -10,14 +10,10 @@ categories = ["development-tools", "rust-patterns"]
keywords = ["proc_macro", "sealed", "future-proofing"]
readme = "README.md"
edition = "2018"
exclude = [
"images/*"
]
exclude = ["images/*"]

[workspace]
members = [
"demo"
]
members = ["demo"]

[lib]
proc-macro = true
Expand All @@ -29,4 +25,5 @@ syn = { version = "1.0", features = ["extra-traits"] }
[dependencies]
syn = { version = "1.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
proc-macro2 = "1.0"
heck = "0.3"
66 changes: 50 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,30 @@ as described in the Rust API Guidelines [[1](https://rust-lang.github.io/api-gui

```toml
[dependencies]
sealed = "0.1"
sealed = "0.2.0-rc2"
```

## Example

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;

#[sealed]
trait T {}

#[sealed]
pub struct A;

#[sealed]
impl T for A {}

#[sealed]
pub struct B;

#[sealed]
impl T for B {}

pub struct C;
Expand All @@ -47,23 +47,57 @@ fn main() {

## 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_for_t {
pub trait Sealed {}
}
pub trait T: __seal_for_t::Sealed {}

// #[sealed]
// pub struct A;
pub struct A;
impl private::Sealed for A {}
pub struct B(i32);

impl __seal_for_t::Sealed for A {}
impl T for A {}

impl __seal_for_t::Sealed for B {}
impl T for B {}

fn main() {
return;
}
```
6 changes: 3 additions & 3 deletions demo/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ where
state: PhantomData<State>,
}

#[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<Idle> {
Expand Down
15 changes: 15 additions & 0 deletions examples/generics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Test provided in #4
// https://github.com/jmg-duarte/sealed-rs/issues/4

use sealed::sealed;

#[sealed]
pub trait Set<V> {}

#[sealed]
impl<T> Set<Option<T>> for T {}

#[sealed]
impl<T> Set<Option<T>> for Option<T> {}

fn main() {}
27 changes: 27 additions & 0 deletions examples/nesting.rs
Original file line number Diff line number Diff line change
@@ -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;
}
16 changes: 16 additions & 0 deletions examples/simple.rs
Original file line number Diff line number Diff line change
@@ -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;
}
44 changes: 44 additions & 0 deletions examples/syn.rs
Original file line number Diff line number Diff line change
@@ -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_for_as_span::Sealed {
// fn as_span(&self) -> Span;
// }
// mod __seal_for_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_for_as_span::Sealed for Span {}

#[sealed]
impl<T: Spanned> AsSpan for &T {
fn as_span(&self) -> Span {
self.span()
}
}
// expands to:
// impl<T: Spanned> AsSpan for &T {
// fn as_span(&self) -> Span { self.span() }
// }
// impl<T: Spanned> __seal_for_as_span::Sealed for &T {}

fn main() {}
65 changes: 46 additions & 19 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,11 @@
//! impl private::Sealed for A {}
//! ```

use heck::SnakeCase;
use proc_macro::TokenStream;
Comment thread
jmg-duarte marked this conversation as resolved.
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};

#[proc_macro_attribute]
pub fn sealed(_args: TokenStream, input: TokenStream) -> TokenStream {
Expand All @@ -75,10 +77,14 @@ pub fn sealed(_args: TokenStream, input: TokenStream) -> TokenStream {
})
}

fn parse_sealed(item: syn::Item) -> syn::Result<proc_macro2::TokenStream> {
fn seal_name<D: ::std::fmt::Display>(seal: D) -> syn::Ident {
::quote::format_ident!("__seal_for_{}", &seal.to_string().to_snake_case())
}

fn parse_sealed(item: syn::Item) -> syn::Result<TokenStream2> {
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)),
_ => Err(syn::Error::new(
proc_macro2::Span::call_site(),
"expected struct or trait",
Expand All @@ -87,21 +93,42 @@ fn parse_sealed(item: syn::Item) -> syn::Result<proc_macro2::TokenStream> {
}

// Care for https://gist.github.com/Koxiaet/8c05ebd4e0e9347eb05f265dfb7252e1#procedural-macros-support-renaming-the-crate
fn parse_sealed_struct(strct: syn::ItemStruct) -> syn::Result<proc_macro2::TokenStream> {
let ident = &strct.ident;
Ok(quote!(
#strct
impl private::Sealed for #ident {}
))
fn parse_sealed_trait(mut item_trait: syn::ItemTrait) -> TokenStream2 {
let trait_ident = &item_trait.ident.unraw();
let trait_generics = &item_trait.generics;
Comment thread
jmg-duarte marked this conversation as resolved.
let seal = seal_name(trait_ident);
Comment thread
jmg-duarte marked this conversation as resolved.
item_trait
.supertraits
.push(parse_quote!(#seal::Sealed #trait_generics));
quote!(
pub(crate) mod #seal {
pub trait Sealed #trait_generics {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, an important thing is to use super::* in the generate module. Since, well, we have trait bounds:

Playground

Copy link
Copy Markdown

@danielhenrymantilla danielhenrymantilla May 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed! That being said, use super::*; breaks https://rust-lang.github.io/api-guidelines/macros.html#item-macros-work-anywhere-that-items-are-allowed-c-anywhere, so if it can be avoided it will be a win. In this instance, it can be avoided by removing the bounds, so it's mostly a matter of adding:

// Only keep the introduced params (no bounds), since
// the bounds may break in the `#seal` submodule.
let trait_generics = trait_generics.split_for_impl().1;

before the quote! invocation

Copy link
Copy Markdown
Contributor

@tyranron tyranron May 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmg-duarte @danielhenrymantilla if so, we need to consider implicit Sized. So, something like this would work:

#[sealed]
pub MyTrait<T: ?Sized>: MyBounds {}

// As described above will desugar as:
pub MyTrait<T: ?Sized>: MyBounds + private::Sealed<T> {}
mod private {
    pub Sealed<T> {}
}

// Which breaks the original trait:
impl private::Sealed<dyn Error> for MyType {} // compiler error

// If we remove bounds, we should allways apply `?Sized` to type params,
// to be transparent asap:
pub MyTrait<T: ?Sized>: MyBounds + private::Sealed<T> {}
mod private {
    pub Sealed<T: ?Sized> {}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, true! The Sealed trait could then be made generic over ?Sized types to avoid this; although I do admit that at this point using use super::*; (so as to forward all the bounds) at the cost of not supporting being applied inside a function's body may be the simpler way to go. I don't have strong preferences anymore.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmg-duarte @danielhenrymantilla I'd prefer to start with a simpler use super::* solution and mess around only if there will be any real cases working badly this way.

}
#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<proc_macro2::TokenStream> {
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<TokenStream2> {
if let Some(impl_trait) = &item_impl.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);
sealed_path.push(parse_quote!(#seal));
sealed_path.push(parse_quote!(Sealed));

let self_type = &item_impl.self_ty;
let trait_generics = &item_impl.generics.split_for_impl().1;

Ok(quote! {
impl #trait_generics #sealed_path #arguments for #self_type {}
Comment thread
jmg-duarte marked this conversation as resolved.
Outdated
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am curious about this, what problem were the where clauses causing?

Basically the example I have in mind is:

use ::core::future::Future;

#[sealed]
trait GeneratedBy<F> {}

#[sealed]
impl<F> GeneratedBy<F> for F::Output
where
    F : Future,
{}

That is, the #where_clauses may need to be present for #self_type to be well-defined.

  • Another maybe less contrived example:

    #[sealed]
    trait Trait {}
    
    struct MyWrapper<T : Copy>(T);
    
    #[sealed]
    impl<T> Trait for MyWrapper<T>
    where
        T : Copy,
    {}
  • And more generally, a less constrained impl will allow downstream users to circumvent the whole seal:

    pub trait Is { type EqTo : ?Sized; }
    impl<T : ?Sized> Is for T { type EqTo = Self; }
    
    #[sealed]
    pub trait Trait {}
    
    #[sealed]
    impl<T> Trait for T
    where
        T : Is<EqTo = ()>, // where T = ()
    {}

    without the where bounds, we'd end up with impl<T> Sealed for T {}

  • This makes me also realize the case of unbounds:

    #[sealed]
    trait Trait {}
    
    #[sealed]
    impl<T> Trait for T where T : ?Sized {}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember there was a reason you pointed out. But maybe I just misunderstood and took them out.
I'll put them back in, my bad!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was this comment
#6 (comment)

Copy link
Copy Markdown

@danielhenrymantilla danielhenrymantilla May 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think a distinction must be made between "necessary bounds to ever impl Sealed" (the bounds on the trait definition), which, in that comment, I claimed they are not necessary (those will already be enforced by such bounds on the annotated trait) –with the exception of the ?Sized "unbound", which is the only bound that actually loosens genericity. So, technically, the Sealed definition ought to carry ?Sized unbounds on all its generic type parameters, and does not need anything more.

Each impl Sealed, however, must have the same degree of generality as the annotated impl, so it has to carry / forward all the bounds that the original impl had. This is less problematic than the trait definition, because we no longer need to be in a submodule and can thus just copy-paste, verbatim, such bounds without issues (whereas for the trait def, we cannot forward the clauses from without a submodule without a use super::*;, and such import breaks when it happens inside a function's body).

The situation is a bit subtle, granted but by thinking about this in advance we will be dodging many potential bugs / issues 🙂

#item_impl
})
} else {
Err(syn::Error::new_spanned(
item_impl,
"missing implentation trait",
))
Comment thread
jmg-duarte marked this conversation as resolved.
Outdated
}
}
8 changes: 6 additions & 2 deletions tests/compile/01.rs → tests/fail/01-general.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions tests/compile/01.stderr → tests/fail/01-general.stderr
Original file line number Diff line number Diff line change
@@ -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`
Loading