Skip to content

Commit

Permalink
Feature: Add #[since(version="1.0")] to specify first version of a …
Browse files Browse the repository at this point in the history
…feature

`#[since(version = "1.0.0")]` adds a doc line `/// Since: Version(1.0.0)`.

Example:

```rust,ignore
/// Foo function
///
/// Does something.
#[since(version = "1.0.0")]
fn foo() {}
```

The above code will be transformed into:

```rust,ignore
/// Foo function
///
/// Does something.
///
/// Since: Version(1.0.0)
fn foo() {}
```
  • Loading branch information
drmingdrmer committed Apr 14, 2024
1 parent c21fc43 commit 2322676
Show file tree
Hide file tree
Showing 8 changed files with 372 additions and 3 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ anyhow = "1.0.63"
async-entry = "0.3.1"
byte-unit = "4.0.12"
bytes = "1.0"
chrono = { version = "0.4" }
clap = { version = "4.1.11", features = ["derive", "env"] }
derive_more = { version="0.99.9" }
futures = "0.3"
Expand All @@ -29,6 +30,7 @@ pretty_assertions = "1.0.0"
proc-macro2 = "1.0"
quote = "1.0"
rand = "0.8"
semver = "1.0.14"
serde = { version="1.0.114", features=["derive", "rc"]}
serde_json = "1.0.57"
syn = "2.0"
Expand Down
4 changes: 3 additions & 1 deletion macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ repository = { workspace = true }
proc-macro = true

[dependencies]
chrono = { workspace = true }
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { workspace = true, features = ["full"] }
semver = { workspace = true }
syn = { workspace = true, features = ["full", "extra-traits"] }

[features]

Expand Down
54 changes: 54 additions & 0 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
#![doc = include_str!("lib_readme.md")]

mod since;
pub(crate) mod utils;

use proc_macro::TokenStream;
use quote::quote;
use since::Since;
use syn::parse2;
use syn::parse_macro_input;
use syn::parse_str;
Expand Down Expand Up @@ -103,3 +107,53 @@ fn add_send_bounds(item: TokenStream) -> TokenStream {
_ => panic!("add_async_trait can only be used with traits"),
}
}

/// Add a `Since` line of doc, such as `/// Since: 1.0.0`.
///
/// `#[since(version = "1.0.0")]` generates:
/// ```rust,ignore
/// /// Since: 1.0.0
/// ```
///
/// `#[since(version = "1.0.0", date = "2021-01-01")]` generates:
/// ```rust,ignore
/// /// Since: 1.0.0, Date(2021-01-01)
/// ```
///
/// - The `version` must be a valid semver string.
/// - The `date` must be a valid date string in the format `yyyy-mm-dd`.
///
/// ### Example
///
/// ```rust,ignore
/// /// Foo function
/// ///
/// /// Does something.
/// #[since(version = "1.0.0")]
/// fn foo() {}
/// ```
///
/// The above code will be transformed into:
///
/// ```rust,ignore
/// /// Foo function
/// ///
/// /// Does something.
/// ///
/// /// Since: 1.0.0
/// fn foo() {}
/// ```
#[proc_macro_attribute]
pub fn since(args: TokenStream, item: TokenStream) -> TokenStream {
let tokens = do_since(args, item.clone());
match tokens {
Ok(x) => x,
Err(e) => utils::token_stream_with_error(item, e),
}
}

fn do_since(args: TokenStream, item: TokenStream) -> Result<TokenStream, syn::Error> {
let since = Since::new(args)?;
let tokens = since.append_since_doc(item)?;
Ok(tokens)
}
32 changes: 30 additions & 2 deletions macros/src/lib_readme.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
Supporting utils for [Openraft](https://crates.io/crates/openraft).

# `add_async_trait`

`#[add_async_trait]` adds `Send` bounds to an async trait.

# Example
## Example

```
#[openraft_macros::add_async_trait]
Expand All @@ -17,4 +19,30 @@ The above code will be transformed into:
trait MyTrait {
fn my_method(&self) -> impl Future<Output=Result<(), String>> + Send;
}
```
```


# `since`

`#[since(version = "1.0.0")]` adds a doc line `/// Since: 1.0.0`.

## Example

```rust,ignore
/// Foo function
///
/// Does something.
#[since(version = "1.0.0")]
fn foo() {}
```

The above code will be transformed into:

```rust,ignore
/// Foo function
///
/// Does something.
///
/// Since: 1.0.0
fn foo() {}
```
198 changes: 198 additions & 0 deletions macros/src/since.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
use std::str::FromStr;

use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::parse::Parser;
use syn::spanned::Spanned;

use crate::utils;

pub struct Since {
pub(crate) version: Option<String>,
pub(crate) date: Option<String>,
}

impl Since {
/// Build a `Since` struct from the given attribute arguments.
pub(crate) fn new(args: TokenStream) -> Result<Since, syn::Error> {
let mut since = Since {
version: None,
date: None,
};

type AttributeArgs = syn::punctuated::Punctuated<syn::Meta, syn::Token![,]>;

let parsed_args = AttributeArgs::parse_terminated.parse(args.clone())?;

for arg in parsed_args {
match arg {
syn::Meta::NameValue(namevalue) => {
let q = namevalue
.path
.get_ident()
.ok_or_else(|| syn::Error::new_spanned(&namevalue, "Must have specified ident"))?;

let ident = q.to_string().to_lowercase();

match ident.as_str() {
"version" => {
since.set_version(namevalue.value.clone(), Spanned::span(&namevalue.value))?;
}

"date" => {
since.set_date(namevalue.value.clone(), Spanned::span(&namevalue.value))?;
}

name => {
let msg = format!(
"Unknown attribute {} is specified; expected one of: `version`, `date`",
name,
);
return Err(syn::Error::new_spanned(namevalue, msg));
}
}
}
other => {
return Err(syn::Error::new_spanned(other, "Unknown attribute inside the macro"));
}
}
}

if since.version.is_none() {
return Err(syn::Error::new_spanned(
proc_macro2::TokenStream::from(args),
"Missing `version` attribute",
));
}

Ok(since)
}
/// Append a `since` doc such as `Since: 1.0.0` to the bottom of the doc section.
pub(crate) fn append_since_doc(self, item: TokenStream) -> Result<TokenStream, syn::Error> {
let item = proc_macro2::TokenStream::from(item);

// Present docs to skip, in order to append `since` at bottom of doc section.
let mut present_docs = vec![];

// Tokens left after present docs.
let mut last_non_doc = vec![];

let mut it = item.clone().into_iter();
loop {
let Some(curr) = it.next() else {
break;
};
let Some(next) = it.next() else {
last_non_doc.push(curr);
break;
};

if utils::is_doc(&curr, &next) {
present_docs.push(curr);
present_docs.push(next);
} else {
last_non_doc.push(curr);
last_non_doc.push(next);
break;
}
}

let since_docs_str = if present_docs.is_empty() {
format!(r#"#[doc = " {}"]"#, self.to_doc_string())
} else {
// If there are already docs, insert a blank line.
format!(r#"#[doc = ""] #[doc = " {}"]"#, self.to_doc_string())
};
let since_docs = proc_macro2::TokenStream::from_str(&since_docs_str).unwrap();

let present_docs: proc_macro2::TokenStream = present_docs.into_iter().collect();
let last_non_docs: proc_macro2::TokenStream = last_non_doc.into_iter().collect();

// Other non doc tokens.
let other: proc_macro2::TokenStream = it.collect();

let tokens = quote! {
#present_docs
#since_docs
#last_non_docs
#other
};
Ok(tokens.into())
}

/// Build doc string: `Since: 1.0.0, Date(2021-01-01)` or `Since: 1.0.0`
fn to_doc_string(&self) -> String {
let mut s = String::new();
s.push_str("Since: ");

if let Some(version) = &self.version {
s.push_str(version.as_str());
}
if let Some(date) = &self.date {
s.push_str(&format!(", Date({})", date));
}
s
}

pub(crate) fn set_version(&mut self, ver_lit: syn::Expr, span: Span) -> Result<(), syn::Error> {
if self.version.is_some() {
return Err(syn::Error::new(span, "`version` set multiple times."));
}

let ver = Self::parse_str(ver_lit, "version", span)?;

semver::Version::parse(&ver)
.map_err(|_e| syn::Error::new(span, format!("`version`(`{}`) is not valid semver.", ver)))?;

self.version = Some(ver);

Ok(())
}

pub(crate) fn set_date(&mut self, date_lit: syn::Expr, span: Span) -> Result<(), syn::Error> {
if self.date.is_some() {
return Err(syn::Error::new(span, "`date` set multiple times."));
}

let date_str = Self::parse_str(date_lit, "date", span)?;

chrono::NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").map_err(|_e| {
syn::Error::new(
span,
format!(
"`date`(`{}`) is not valid date string. Expected format: yyyy-mm-dd",
date_str
),
)
})?;

self.date = Some(date_str);

Ok(())
}

/// Extract string from `foo` or `"foo"`
fn parse_str(expr: syn::Expr, field: &str, span: Span) -> Result<String, syn::Error> {
let s = match expr {
syn::Expr::Lit(s) => match s.lit {
syn::Lit::Str(s) => s.value(),
syn::Lit::Verbatim(s) => s.to_string(),
_ => {
return Err(syn::Error::new(
span,
format!("Failed to parse value of `{}` as string.", field),
))
}
},
syn::Expr::Verbatim(s) => s.to_string(),
_ => {
return Err(syn::Error::new(
span,
format!("Failed to parse value of `{}` as string.", field),
))
}
};
Ok(s)
}
}
49 changes: 49 additions & 0 deletions macros/src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use proc_macro::TokenStream;
use proc_macro2::TokenTree;

/// Check if the next two token is a doc attribute, such as `#[doc = "foo"]`.
///
/// An doc attribute is composed of a `#` token and a `Group` token with a `Bracket` delimiter:
/// ```ignore
/// Punct { ch: '#', },
/// Group {
/// delimiter: Bracket,
/// stream: TokenStream [
/// Ident { ident: "doc", },
/// Punct { ch: '=', },
/// Literal { kind: Str, symbol: " Doc", },
/// ],
/// },
/// ```
pub(crate) fn is_doc(curr: &TokenTree, next: &TokenTree) -> bool {
let TokenTree::Punct(p) = curr else {
return false;
};

if p.as_char() != '#' {
return false;
}

let TokenTree::Group(g) = &next else {
return false;
};

if g.delimiter() != proc_macro2::Delimiter::Bracket {
return false;
}
let first = g.stream().into_iter().next();
let Some(first) = first else {
return false;
};

let TokenTree::Ident(i) = first else {
return false;
};

i == "doc"
}

pub(crate) fn token_stream_with_error(mut item: TokenStream, e: syn::Error) -> TokenStream {
item.extend(TokenStream::from(e.into_compile_error()));
item
}
Loading

0 comments on commit 2322676

Please sign in to comment.