-
Notifications
You must be signed in to change notification settings - Fork 157
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: Add
#[since(version="1.0")]
to specify first version of a …
…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
1 parent
9760e15
commit 5776139
Showing
8 changed files
with
372 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.