Skip to content

Commit

Permalink
feat: Allow marking impl blocks unstable/stable (#15)
Browse files Browse the repository at this point in the history
The stable and unstable attributes can now be applied to
trait implementation blocks. This is useful for documenting
unstable implementations of dependencies.

```rust
#[instability::stable(since = "v1.0.0")]
impl StableTrait for StableStruct {}

#[instability::unstable(feature = "trait")]
impl UnstableTrait for StableStruct {}
```

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
  • Loading branch information
bugadani and joshka authored Dec 21, 2024
1 parent e365305 commit 596e13c
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 24 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file.

## Unreleased

### Added

- Allow placing `instability` and `stability` on `impl` blocks ([#15](https://github.com/ratatui/instability/pull/15))

## [0.3.3](https://github.com/ratatui/instability/compare/instability-v0.3.2...instability-v0.3.3) - 2024-11-12

### Added
Expand Down
6 changes: 6 additions & 0 deletions example/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ pub trait StableTrait {
// fn unstable_trait_method(&self);
}

#[instability::stable(since = "v1.0.0")]
impl StableTrait for StableStruct {}

/// An unstable trait
///
/// This trait is unstable
Expand All @@ -160,6 +163,9 @@ pub trait UnstableTrait {
// fn unstable_trait_method(&self);
}

#[instability::unstable(feature = "trait")]
impl UnstableTrait for StableStruct {}

/// A stable enum
///
/// This enum is stable.
Expand Down
22 changes: 19 additions & 3 deletions src/item_like.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use syn::Visibility;

pub trait ItemLike {
pub trait Stability {
#[allow(unused)]
fn attrs(&self) -> &[syn::Attribute];

fn push_attr(&mut self, attr: syn::Attribute);
}

pub trait ItemLike: Stability {
fn visibility(&self) -> &Visibility;

fn set_visibility(&mut self, visibility: Visibility);
Expand All @@ -18,15 +20,17 @@ pub trait ItemLike {
macro_rules! impl_has_visibility {
($($ty:ty),+ $(,)?) => {
$(
impl ItemLike for $ty {
impl Stability for $ty {
fn attrs(&self) -> &[syn::Attribute] {
&self.attrs
}

fn push_attr(&mut self, attr: syn::Attribute) {
self.attrs.push(attr);
}
}

impl ItemLike for $ty {
fn visibility(&self) -> &Visibility {
&self.vis
}
Expand All @@ -50,15 +54,17 @@ impl_has_visibility!(
syn::ItemUse,
);

impl ItemLike for syn::ItemStruct {
impl Stability for syn::ItemStruct {
fn attrs(&self) -> &[syn::Attribute] {
&self.attrs
}

fn push_attr(&mut self, attr: syn::Attribute) {
self.attrs.push(attr);
}
}

impl ItemLike for syn::ItemStruct {
fn visibility(&self) -> &Visibility {
&self.vis
}
Expand All @@ -74,3 +80,13 @@ impl ItemLike for syn::ItemStruct {
self.vis = visibility;
}
}

impl Stability for syn::ItemImpl {
fn attrs(&self) -> &[syn::Attribute] {
&self.attrs
}

fn push_attr(&mut self, attr: syn::Attribute) {
self.attrs.push(attr);
}
}
15 changes: 15 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ mod unstable;
/// - Changes the visibility of the item from `pub` to `pub(crate)` unless a certain crate feature
/// is enabled. This ensures that internal code within the crate can always use the item, but
/// downstream consumers cannot access it unless they opt-in to the unstable API.
/// - Annotated `impl` blocks will instead be removed.
/// - Changes the Visibility of certain child items of the annotated item (such as struct fields) to
/// match the item's visibility. Children that are not public will not be affected.
/// - Appends an "Stability" section to the item's documentation that notes that the item is
Expand Down Expand Up @@ -104,6 +105,20 @@ mod unstable;
/// unimplemented!()
/// }
/// ```
///
/// We can also apply the attribute to an `impl` block like so:
///
/// ```
/// /// This structure is responsible for bar.
/// pub struct Foo;
///
/// #[instability::unstable(feature = "unstable-dependency")]
/// impl Default for Foo {
/// fn default() -> Self {
/// unimplemented!()
/// }
/// }
/// ```
#[proc_macro_attribute]
pub fn unstable(args: TokenStream, input: TokenStream) -> TokenStream {
unstable_macro(args.into(), input.into()).into()
Expand Down
22 changes: 20 additions & 2 deletions src/stable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::{parse_quote, Item};

use crate::item_like::ItemLike;
use crate::item_like::{ItemLike, Stability};

pub fn stable_macro(args: TokenStream, input: TokenStream) -> TokenStream {
let attributes = match NestedMeta::parse_meta_list(args) {
Expand All @@ -26,6 +26,7 @@ pub fn stable_macro(args: TokenStream, input: TokenStream) -> TokenStream {
Item::Const(item_const) => unstable_attribute.expand(item_const),
Item::Static(item_static) => unstable_attribute.expand(item_static),
Item::Use(item_use) => unstable_attribute.expand(item_use),
Item::Impl(item_impl) => unstable_attribute.expand_impl(item_impl),
_ => panic!("unsupported item type"),
},
Err(err) => return TokenStream::from(Error::from(err).write_errors()),

Check warning on line 32 in src/stable.rs

View workflow job for this annotation

GitHub Actions / Clippy (stable)

unneeded `return` statement

warning: unneeded `return` statement --> src/stable.rs:32:21 | 32 | Err(err) => return TokenStream::from(Error::from(err).write_errors()), | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_return = note: `#[warn(clippy::needless_return)]` on by default help: remove `return` | 32 | Err(err) => TokenStream::from(Error::from(err).write_errors()), | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Check warning on line 32 in src/stable.rs

View workflow job for this annotation

GitHub Actions / Clippy (stable)

useless conversion to the same type: `proc_macro2::TokenStream`

warning: useless conversion to the same type: `proc_macro2::TokenStream` --> src/stable.rs:32:28 | 32 | Err(err) => return TokenStream::from(Error::from(err).write_errors()), | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider removing `TokenStream::from()`: `Error::from(err).write_errors()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion

Check warning on line 32 in src/stable.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

unneeded `return` statement

warning: unneeded `return` statement --> src/stable.rs:32:21 | 32 | Err(err) => return TokenStream::from(Error::from(err).write_errors()), | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_return = note: `#[warn(clippy::needless_return)]` on by default help: remove `return` | 32 | Err(err) => TokenStream::from(Error::from(err).write_errors()), | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Check warning on line 32 in src/stable.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

useless conversion to the same type: `proc_macro2::TokenStream`

warning: useless conversion to the same type: `proc_macro2::TokenStream` --> src/stable.rs:32:28 | 32 | Err(err) => return TokenStream::from(Error::from(err).write_errors()), | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider removing `TokenStream::from()`: `Error::from(err).write_errors()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion
Expand All @@ -42,11 +43,15 @@ pub struct StableAttribute {
}

impl StableAttribute {
pub fn expand(&self, mut item: impl ItemLike + ToTokens + Clone) -> TokenStream {
pub fn expand(&self, item: impl ItemLike + ToTokens + Clone) -> TokenStream {
if !item.is_public() {
// We only care about public items.
return item.into_token_stream().into();

Check warning on line 49 in src/stable.rs

View workflow job for this annotation

GitHub Actions / Clippy (stable)

useless conversion to the same type: `proc_macro2::TokenStream`

warning: useless conversion to the same type: `proc_macro2::TokenStream` --> src/stable.rs:49:20 | 49 | return item.into_token_stream().into(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider removing `.into()`: `item.into_token_stream()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion

Check warning on line 49 in src/stable.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

useless conversion to the same type: `proc_macro2::TokenStream`

warning: useless conversion to the same type: `proc_macro2::TokenStream` --> src/stable.rs:49:20 | 49 | return item.into_token_stream().into(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider removing `.into()`: `item.into_token_stream()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion
}
self.expand_impl(item)
}

pub fn expand_impl(&self, mut item: impl Stability + ToTokens) -> TokenStream {
let doc = if let Some(ref version) = self.since {
formatdoc! {"
# Stability
Expand Down Expand Up @@ -280,4 +285,17 @@ mod tests {
};
assert_eq!(tokens.to_string(), expected.to_string());
}

#[test]
fn expand_impl_block() {
let item: syn::ItemImpl = parse_quote! {
impl Default for crate::foo::Foo {}
};
let tokens = StableAttribute::default().expand_impl(item);
let expected = quote! {
#[doc = #STABLE_DOC]
impl Default for crate::foo::Foo {}
};
assert_eq!(tokens.to_string(), expected.to_string());
}
}
70 changes: 51 additions & 19 deletions src/unstable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse_quote, Item};

use crate::item_like::ItemLike;
use crate::item_like::{ItemLike, Stability};

pub fn unstable_macro(args: TokenStream, input: TokenStream) -> TokenStream {
let attributes = match NestedMeta::parse_meta_list(args) {
Expand All @@ -26,6 +26,7 @@ pub fn unstable_macro(args: TokenStream, input: TokenStream) -> TokenStream {
Item::Const(item_const) => unstable_attribute.expand(item_const),
Item::Static(item_static) => unstable_attribute.expand(item_static),
Item::Use(item_use) => unstable_attribute.expand(item_use),
Item::Impl(item_impl) => unstable_attribute.expand_impl(item_impl),
_ => panic!("unsupported item type"),
},
Err(err) => return TokenStream::from(Error::from(err).write_errors()),

Check warning on line 32 in src/unstable.rs

View workflow job for this annotation

GitHub Actions / Clippy (stable)

unneeded `return` statement

warning: unneeded `return` statement --> src/unstable.rs:32:21 | 32 | Err(err) => return TokenStream::from(Error::from(err).write_errors()), | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_return help: remove `return` | 32 | Err(err) => TokenStream::from(Error::from(err).write_errors()), | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Check warning on line 32 in src/unstable.rs

View workflow job for this annotation

GitHub Actions / Clippy (stable)

useless conversion to the same type: `proc_macro2::TokenStream`

warning: useless conversion to the same type: `proc_macro2::TokenStream` --> src/unstable.rs:32:28 | 32 | Err(err) => return TokenStream::from(Error::from(err).write_errors()), | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider removing `TokenStream::from()`: `Error::from(err).write_errors()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion

Check warning on line 32 in src/unstable.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

unneeded `return` statement

warning: unneeded `return` statement --> src/unstable.rs:32:21 | 32 | Err(err) => return TokenStream::from(Error::from(err).write_errors()), | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_return help: remove `return` | 32 | Err(err) => TokenStream::from(Error::from(err).write_errors()), | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Check warning on line 32 in src/unstable.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

useless conversion to the same type: `proc_macro2::TokenStream`

warning: useless conversion to the same type: `proc_macro2::TokenStream` --> src/unstable.rs:32:28 | 32 | Err(err) => return TokenStream::from(Error::from(err).write_errors()), | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider removing `TokenStream::from()`: `Error::from(err).write_errors()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion
Expand All @@ -46,30 +47,14 @@ pub struct UnstableAttribute {
}

impl UnstableAttribute {
fn feature_flag(&self) -> String {
self.feature
.as_deref()
.map_or(String::from("unstable"), |name| format!("unstable-{name}"))
}

pub fn expand(&self, mut item: impl ItemLike + ToTokens + Clone) -> TokenStream {
if !item.is_public() {
// We only care about public items.
return item.into_token_stream().into();

Check warning on line 53 in src/unstable.rs

View workflow job for this annotation

GitHub Actions / Clippy (stable)

useless conversion to the same type: `proc_macro2::TokenStream`

warning: useless conversion to the same type: `proc_macro2::TokenStream` --> src/unstable.rs:53:20 | 53 | return item.into_token_stream().into(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider removing `.into()`: `item.into_token_stream()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion

Check warning on line 53 in src/unstable.rs

View workflow job for this annotation

GitHub Actions / Clippy (beta)

useless conversion to the same type: `proc_macro2::TokenStream`

warning: useless conversion to the same type: `proc_macro2::TokenStream` --> src/unstable.rs:53:20 | 53 | return item.into_token_stream().into(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider removing `.into()`: `item.into_token_stream()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion
}
let feature_flag = self.feature_flag();
let doc = formatdoc! {"
# Stability

**This API is marked as unstable** and is only available when the `{feature_flag}`
crate feature is enabled. This comes with no stability guarantees, and could be changed
or removed at any time."};
item.push_attr(parse_quote! { #[doc = #doc] });

if let Some(issue) = &self.issue {
let doc = format!("The tracking issue is: `{}`.", issue);
item.push_attr(parse_quote! { #[doc = #doc] });
}
let feature_flag = self.feature_flag();
self.add_doc(&mut item);

let mut hidden_item = item.clone();
hidden_item.set_visibility(parse_quote! { pub(crate) });
Expand All @@ -84,6 +69,38 @@ impl UnstableAttribute {
#hidden_item
})
}

pub fn expand_impl(&self, mut item: impl Stability + ToTokens) -> TokenStream {
let feature_flag = self.feature_flag();
self.add_doc(&mut item);
TokenStream::from(quote! {
#[cfg(any(doc, feature = #feature_flag))]
#[cfg_attr(docsrs, doc(cfg(feature = #feature_flag)))]
#item
})
}

fn add_doc(&self, item: &mut impl Stability) {
let feature_flag = self.feature_flag();
let doc = formatdoc! {"
# Stability
**This API is marked as unstable** and is only available when the `{feature_flag}`
crate feature is enabled. This comes with no stability guarantees, and could be changed
or removed at any time."};
item.push_attr(parse_quote! { #[doc = #doc] });

if let Some(issue) = &self.issue {
let doc = format!("The tracking issue is: `{}`.", issue);
item.push_attr(parse_quote! { #[doc = #doc] });
}
}

fn feature_flag(&self) -> String {
self.feature
.as_deref()
.map_or(String::from("unstable"), |name| format!("unstable-{name}"))
}
}
#[cfg(test)]
mod tests {
Expand Down Expand Up @@ -372,4 +389,19 @@ mod tests {
};
assert_eq!(tokens.to_string(), expected.to_string());
}

#[test]
fn expand_impl_block() {
let item: syn::ItemImpl = parse_quote! {
impl Default for crate::foo::Foo {}
};
let tokens = UnstableAttribute::default().expand_impl(item);
let expected = quote! {
#[cfg(any(doc, feature = "unstable"))]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable")))]
#[doc = #DEFAULT_DOC]
impl Default for crate::foo::Foo {}
};
assert_eq!(tokens.to_string(), expected.to_string());
}
}

0 comments on commit 596e13c

Please sign in to comment.