#, "?style=flat-square&logo=rust)](https://crates.io/crates/", env!("CARGO_PKG_NAME"), ")")]
#, "?style=flat-square&logo=docs.rs)](https://docs.rs/", env!("CARGO_PKG_NAME"), ")")]
#"]
#, "-blue?style=flat-square&logo=rust)")]
#![doc = concat!(env!("CARGO_PKG_NAME"), " = ", "\"", env!("CARGO_PKG_VERSION_MAJOR"), ".", env!("CARGO_PKG_VERSION_MINOR"), "\"")]
use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
#[proc_macro]
pub fn docstr(input: TokenStream) -> TokenStream {
let mut input = input.into_iter().peekable();
let mut compile_errors = TokenStream::new();
let mut compile_error = |span: Span, message: &str| {
compile_errors.extend(CompileError::new(span, message));
};
let macro_path = match input.peek() {
Some(TokenTree::Punct(punct)) if *punct == '#' => None,
None => {
return CompileError::new(
Span::call_site(),
"requires at least a documentation comment argument: `/// ...`",
)
.into()
}
Some(_) => {
match extract_macro_path(&mut input) {
Ok(macro_path) => macro_path,
Err(compile_error) => return compile_error.into(),
}
}
};
let mut tokens_before_doc_comments = TokenStream::new();
let mut doc_comments = Vec::new();
let mut tokens_after_doc_comments = TokenStream::new();
#[derive(Eq, PartialEq, PartialOrd, Ord)]
enum DocCommentProgress {
NotReached,
Inside,
Finished,
}
let mut doc_comment_progress = DocCommentProgress::NotReached;
while let Some(tt) = input.next() {
let doc_comment_start_span = match tt {
tt if doc_comment_progress == DocCommentProgress::Finished => {
tokens_after_doc_comments.extend([tt]);
continue;
}
TokenTree::Punct(punct) if punct == '#' => {
match doc_comment_progress {
DocCommentProgress::NotReached => {
doc_comment_progress = DocCommentProgress::Inside;
}
DocCommentProgress::Inside => {
}
DocCommentProgress::Finished => {
unreachable!("if it's finished we would `continue` in an earlier arm")
}
}
match input.peek() {
Some(TokenTree::Punct(punct)) if *punct == '!' => {
compile_error(
punct.span(),
"Inner doc comments `//! ...` are not supported. Please use `/// ...`",
);
input.next();
}
_ => (),
}
punct.span()
}
tt if doc_comment_progress == DocCommentProgress::NotReached => {
let insert_comma = match input.peek() {
Some(TokenTree::Punct(next)) => match &tt {
TokenTree::Punct(current) if *current == ',' && *next == '#' => false,
_ if *next == '#' => true,
_ => false,
},
_ => false,
};
tokens_before_doc_comments.extend([tt]);
if insert_comma {
tokens_before_doc_comments
.extend([TokenTree::Punct(Punct::new(',', Spacing::Joint))]);
}
continue;
}
_ => {
unreachable!("when the next token is not `#` progress is `Finished`")
}
};
let doc_comment_square_brackets = match input.next() {
Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Bracket => group,
Some(tt) => {
compile_error(tt.span(), "expected `[...]`");
continue;
}
None => {
compile_error(
doc_comment_start_span,
"expected `#` to be followed by `[...]`",
);
continue;
}
};
match input.peek() {
Some(TokenTree::Punct(punct)) if *punct == '#' => {
}
_ => {
doc_comment_progress = DocCommentProgress::Finished;
}
}
let mut doc_comment_attribute_inner = doc_comment_square_brackets.stream().into_iter();
let kw_doc_span = match doc_comment_attribute_inner.next() {
Some(TokenTree::Ident(kw_doc)) if kw_doc.to_string() == "doc" => kw_doc.span(),
Some(tt) => {
compile_error(tt.span(), "expected `doc`");
continue;
}
None => {
compile_error(
doc_comment_square_brackets.span_open(),
"expected `doc` after `[`",
);
continue;
}
};
let punct_eq_span = match doc_comment_attribute_inner.next() {
Some(TokenTree::Punct(eq)) if eq == '=' => eq.span(),
Some(tt) => {
compile_error(tt.span(), "expected `=`");
continue;
}
None => {
compile_error(kw_doc_span, "expected `=` after `doc`");
continue;
}
};
let next = doc_comment_attribute_inner.next();
let Some(tt) = next else {
compile_error(punct_eq_span, "expected string literal after `=`");
continue;
};
let span = tt.span();
let Ok(litrs::Literal::String(literal)) = litrs::Literal::try_from(tt) else {
compile_error(
span,
"only string \"...\" or r\"...\" literals are supported",
);
continue;
};
let literal = literal.value();
let literal = literal.strip_prefix(' ').unwrap_or(literal);
doc_comments.push(literal.to_string());
}
if doc_comments.is_empty() {
compile_error(
Span::call_site(),
"requires at least a documentation comment argument: `/// ...`",
);
}
let string = doc_comments
.into_iter()
.reduce(|mut acc, s| {
acc.push('\n');
acc.push_str(&s);
acc
})
.unwrap_or_default();
let Some(macro_) = macro_path else {
if !tokens_before_doc_comments.is_empty() || !tokens_after_doc_comments.is_empty() {
compile_error(
Span::call_site(),
concat!(
"expected macro input to only contain doc comments: `/// ...`, ",
"because you haven't supplied a macro path as the 1st argument"
),
);
}
if !compile_errors.is_empty() {
return compile_errors;
}
return TokenTree::Literal(Literal::string(&string)).into();
};
if !compile_errors.is_empty() {
return compile_errors;
}
TokenStream::from_iter(
macro_.into_iter().chain([TokenTree::Group(Group::new(
Delimiter::Parenthesis,
TokenStream::from_iter(
tokens_before_doc_comments
.into_iter()
.chain([
TokenTree::Literal(Literal::string(&string)),
TokenTree::Punct(Punct::new(',', Spacing::Joint)),
])
.chain(tokens_after_doc_comments),
),
))]),
)
}
fn extract_macro_path(
input: &mut std::iter::Peekable<proc_macro::token_stream::IntoIter>,
) -> Result<Option<TokenStream>, CompileError> {
let mut macro_path = TokenStream::new();
enum PreviousMacroPathToken {
PathSeparator,
Ident,
}
let mut previous_macro_path_token = None;
macro_rules! invalid_macro_path {
() => {
CompileError::new(
macro_path
.into_iter()
.next()
.map(|tt| tt.span())
.unwrap_or_else(Span::call_site),
"invalid macro path",
)
};
}
loop {
let tt = input.next();
match tt {
Some(TokenTree::Punct(exclamation)) if exclamation == '!' => {
macro_path.extend([TokenTree::Punct(exclamation)]);
break;
}
Some(TokenTree::Punct(colon)) if colon == ':' => {
match previous_macro_path_token {
Some(PreviousMacroPathToken::Ident) | None => {
previous_macro_path_token = Some(PreviousMacroPathToken::PathSeparator);
}
Some(PreviousMacroPathToken::PathSeparator) => {
return Err(invalid_macro_path!());
}
}
macro_path.extend([TokenTree::Punct(colon)]);
match input.next() {
Some(TokenTree::Punct(colon)) if colon == ':' => {
macro_path.extend([TokenTree::Punct(colon)]);
}
_ => {
return Err(invalid_macro_path!());
}
}
}
Some(TokenTree::Ident(ident)) => match previous_macro_path_token {
Some(PreviousMacroPathToken::PathSeparator) | None => {
macro_path.extend([TokenTree::Ident(ident)]);
previous_macro_path_token = Some(PreviousMacroPathToken::Ident);
}
Some(PreviousMacroPathToken::Ident) => {
return Err(invalid_macro_path!());
}
},
_ if !macro_path.is_empty() => {
let macro_path_display = macro_path.to_string();
let last_token = macro_path.into_iter().last().expect("!.is_empty()");
return Err(CompileError::new(
last_token.span(),
format!("macro path must be followed by `!`, try: `{macro_path_display}!`"),
));
}
_ => {
return Err(CompileError::new(
tt.map(|tt| tt.span()).unwrap_or_else(Span::call_site),
"unexpected token",
));
}
}
}
Ok(Some(macro_path))
}
struct CompileError {
pub span: Span,
pub message: String,
}
impl From<CompileError> for TokenStream {
fn from(value: CompileError) -> Self {
value.into_iter().collect()
}
}
impl CompileError {
pub fn new(span: Span, message: impl AsRef<str>) -> Self {
Self {
span,
message: message.as_ref().to_string(),
}
}
}
impl IntoIterator for CompileError {
type Item = TokenTree;
type IntoIter = std::array::IntoIter<Self::Item, 3>;
fn into_iter(self) -> Self::IntoIter {
[
TokenTree::Ident(Ident::new("compile_error", self.span)),
TokenTree::Punct({
let mut punct = Punct::new('!', Spacing::Alone);
punct.set_span(self.span);
punct
}),
TokenTree::Group({
let mut group = Group::new(Delimiter::Brace, {
TokenStream::from_iter(vec![TokenTree::Literal({
let mut string = Literal::string(&self.message);
string.set_span(self.span);
string
})])
});
group.set_span(self.span);
group
}),
]
.into_iter()
}
}