From 596e13c00da34dff4948a71e73c5f765c0699afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20Buga?= Date: Sat, 21 Dec 2024 02:49:35 +0100 Subject: [PATCH] feat: Allow marking impl blocks unstable/stable (#15) 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 --- CHANGELOG.md | 6 ++++ example/src/lib.rs | 6 ++++ src/item_like.rs | 22 +++++++++++++-- src/lib.rs | 15 ++++++++++ src/stable.rs | 22 +++++++++++++-- src/unstable.rs | 70 +++++++++++++++++++++++++++++++++------------- 6 files changed, 117 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbb270d..83c88d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/example/src/lib.rs b/example/src/lib.rs index aa1a834..84903d1 100644 --- a/example/src/lib.rs +++ b/example/src/lib.rs @@ -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 @@ -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. diff --git a/src/item_like.rs b/src/item_like.rs index 8f5e52d..e95d6fb 100644 --- a/src/item_like.rs +++ b/src/item_like.rs @@ -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); @@ -18,7 +20,7 @@ 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 } @@ -26,7 +28,9 @@ macro_rules! impl_has_visibility { fn push_attr(&mut self, attr: syn::Attribute) { self.attrs.push(attr); } + } + impl ItemLike for $ty { fn visibility(&self) -> &Visibility { &self.vis } @@ -50,7 +54,7 @@ impl_has_visibility!( syn::ItemUse, ); -impl ItemLike for syn::ItemStruct { +impl Stability for syn::ItemStruct { fn attrs(&self) -> &[syn::Attribute] { &self.attrs } @@ -58,7 +62,9 @@ impl ItemLike for syn::ItemStruct { fn push_attr(&mut self, attr: syn::Attribute) { self.attrs.push(attr); } +} +impl ItemLike for syn::ItemStruct { fn visibility(&self) -> &Visibility { &self.vis } @@ -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); + } +} diff --git a/src/lib.rs b/src/lib.rs index 7a8f67d..5e8baac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 @@ -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() diff --git a/src/stable.rs b/src/stable.rs index 7d496ba..5fbd8dc 100644 --- a/src/stable.rs +++ b/src/stable.rs @@ -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) { @@ -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()), @@ -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(); } + 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 @@ -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()); + } } diff --git a/src/unstable.rs b/src/unstable.rs index f0fb090..015a38c 100644 --- a/src/unstable.rs +++ b/src/unstable.rs @@ -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) { @@ -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()), @@ -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(); } - 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) }); @@ -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 { @@ -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()); + } }