diff --git a/crates/teloxide-macros/src/attr.rs b/crates/teloxide-macros/src/attr.rs
index 535b508f..a44cb2d4 100644
--- a/crates/teloxide-macros/src/attr.rs
+++ b/crates/teloxide-macros/src/attr.rs
@@ -4,7 +4,7 @@ use proc_macro2::Span;
use syn::{
parse::{Parse, ParseBuffer, ParseStream},
spanned::Spanned,
- Attribute, Ident, Lit, Path, Token,
+ Attribute, Ident, Lit, LitStr, Path, Token,
};
pub(crate) fn fold_attrs(
@@ -18,14 +18,26 @@ pub(crate) fn fold_attrs(
.filter(filter)
.flat_map(|attribute| {
// FIXME: don't allocate here
- let attrs = match attribute.parse_args_with(|input: &ParseBuffer| {
- input.parse_terminated::<_, Token![,]>(Attr::parse)
- }) {
- Ok(ok) => ok,
- Err(err) => return vec![Err(err.into())],
- };
-
- attrs.into_iter().map(&parse).collect()
+ if crate::command_attr::is_doc_comment(&attribute) {
+ vec![parse(Attr {
+ key: Ident::new("description", Span::call_site()),
+ value: AttrValue::Lit(
+ LitStr::new(
+ &crate::command_attr::parse_doc_comment(&attribute)
+ .expect("it is doc comment"),
+ Span::call_site(),
+ )
+ .into(),
+ ),
+ })]
+ } else {
+ match attribute.parse_args_with(|input: &ParseBuffer| {
+ input.parse_terminated::<_, Token![,]>(Attr::parse)
+ }) {
+ Ok(ok) => ok.into_iter().map(&parse).collect(),
+ Err(err) => vec![Err(err.into())],
+ }
+ }
})
.try_fold(init, |acc, r| r.and_then(|r| f(acc, r)))
}
diff --git a/crates/teloxide-macros/src/command_attr.rs b/crates/teloxide-macros/src/command_attr.rs
index 3adf71d7..a074d08e 100644
--- a/crates/teloxide-macros/src/command_attr.rs
+++ b/crates/teloxide-macros/src/command_attr.rs
@@ -6,9 +6,8 @@ use crate::{
Result,
};
-use proc_macro2::{Span, TokenTree};
-use quote::quote_spanned;
-use syn::{parse::Parser, spanned::Spanned, Attribute};
+use proc_macro2::Span;
+use syn::Attribute;
/// All attributes that can be used for `derive(BotCommands)`
pub(crate) struct CommandAttrs {
@@ -50,31 +49,9 @@ impl CommandAttrs {
pub fn from_attributes(attributes: &[Attribute]) -> Result {
use CommandAttrKind::*;
- let docs = attributes.iter().filter_map(|attr| {
- parse_doc_comment(attr)
- .or_else(|| parse_command_description(attr))
- .map(|doc| (doc, attr.span()))
- });
-
- let mut attributes = attributes.to_vec();
- if docs.clone().count() != 0 {
- // Remove all command description attributes, to avoid duplication
- attributes.retain(|attr| !is_command_description(attr));
- let description = docs.clone().map(|(doc, _)| doc).collect::>().join("\n");
- let sp = docs
- .map(|(_, sp)| sp)
- .reduce(|acc, sp| acc.join(sp).expect("The spans are in the same file"))
- .expect("There is at least one doc comment");
- // Insert a new command description attribute, with all descriptions and doc
- // comments
- let attr = Attribute::parse_outer
- .parse2(quote_spanned! { sp => #[command(description = #description)] })?;
- attributes.push(attr.into_iter().next().unwrap());
- }
-
fold_attrs(
- attributes.into_iter(),
- is_command_attribute,
+ attributes.iter().cloned(),
+ |attr| is_command_attribute(attr) || is_doc_comment(attr),
CommandAttr::parse,
Self {
prefix: None,
@@ -96,9 +73,26 @@ impl CommandAttrs {
}
}
+ fn join_string(
+ opt: &mut Option<(String, Span)>,
+ new_str: String,
+ sp: Span,
+ ) -> Result<()> {
+ match opt {
+ slot @ None => {
+ *slot = Some((new_str, sp));
+ Ok(())
+ }
+ Some((old_str, _)) => {
+ *old_str = format!("{old_str}\n{new_str}");
+ Ok(())
+ }
+ }
+ }
+
match attr.kind {
Prefix(p) => insert(&mut this.prefix, p, attr.sp),
- Description(d) => insert(&mut this.description, d, attr.sp),
+ Description(d) => join_string(&mut this.description, d, attr.sp),
RenameRule(r) => insert(&mut this.rename_rule, r, attr.sp),
Rename(r) => insert(&mut this.rename, r, attr.sp),
ParseWith(p) => insert(&mut this.parser, p, attr.sp),
@@ -148,51 +142,18 @@ fn is_command_attribute(a: &Attribute) -> bool {
}
}
-fn is_command_description(attr: &Attribute) -> bool {
- for token in attr.tokens.clone() {
- if let TokenTree::Group(group) = token {
- for token in group.stream() {
- if let TokenTree::Ident(ident) = token {
- if ident == "description" {
- return true;
- }
- }
- }
- }
+pub(crate) fn is_doc_comment(a: &Attribute) -> bool {
+ match a.path.get_ident() {
+ Some(ident) => ident == "doc",
+ _ => false,
}
- false
}
-fn parse_command_description(attr: &Attribute) -> Option {
- if is_command_attribute(attr) {
- for token in attr.tokens.clone() {
- if let TokenTree::Group(group) = token {
- for token in group.stream() {
- if let TokenTree::Ident(ident) = token {
- if ident == "description" {
- for token in group.stream() {
- if let TokenTree::Literal(lit) = token {
- let description = lit.to_string();
- return Some(
- lit.to_string().trim()[1..description.len() - 1]
- .replace(r"\n", "\n"),
- );
- }
- }
- }
- }
- }
- }
- }
- }
-
- None
-}
-
-fn parse_doc_comment(attr: &Attribute) -> Option {
- #[allow(clippy::collapsible_match)]
- if let syn::Meta::NameValue(syn::MetaNameValue { lit, .. }) = attr.parse_meta().ok()? {
- if let syn::Lit::Str(s) = lit {
+pub(crate) fn parse_doc_comment(attr: &Attribute) -> Option {
+ if is_doc_comment(attr) {
+ if let syn::Meta::NameValue(syn::MetaNameValue { lit: syn::Lit::Str(s), .. }) =
+ attr.parse_meta().ok()?
+ {
return Some(s.value().trim().replace(r"\n", "\n"));
}
}