diff --git a/documented-macros/src/derive_impl.rs b/documented-macros/src/derive_impl.rs index fb9e01d..aa3e963 100644 --- a/documented-macros/src/derive_impl.rs +++ b/documented-macros/src/derive_impl.rs @@ -18,7 +18,7 @@ use crate::{ derive::DeriveConfig, derive_fields::{DeriveFieldsConfig, RenameMode}, }, - util::{crate_module_path, get_docs}, + util::{crate_module_path, get_docs, DocContent}, }; /// The type of the doc comment. @@ -50,7 +50,7 @@ impl DocType { #[allow(clippy::type_complexity)] fn docs_handler_opt( &self, - ) -> Box, Option, S) -> syn::Result> + ) -> Box, Option, S) -> syn::Result> where S: ToTokens, { diff --git a/documented-macros/src/lib.rs b/documented-macros/src/lib.rs index 8d2bc80..eb05cb0 100644 --- a/documented-macros/src/lib.rs +++ b/documented-macros/src/lib.rs @@ -23,7 +23,7 @@ use crate::{ /// /// /// /** Multi-line doc comments are supported too. /// Each line of the multi-line block is individually trimmed by default. -/// Note the lack of spaces in front of this line. +/// Note the lack of spaces in front of this line. (See `_caveats` module.) /// */ /// #[doc = "Attribute-style documentation is supported too."] /// #[derive(Documented)] @@ -34,7 +34,7 @@ use crate::{ /// /// Multi-line doc comments are supported too. /// Each line of the multi-line block is individually trimmed by default. -/// Note the lack of spaces in front of this line. +/// Note the lack of spaces in front of this line. (See `_caveats` module.) /// /// Attribute-style documentation is supported too."; /// assert_eq!(BornIn69::DOCS, doc_str); diff --git a/documented-macros/src/util.rs b/documented-macros/src/util.rs index 2006a3d..f8a0a41 100644 --- a/documented-macros/src/util.rs +++ b/documented-macros/src/util.rs @@ -1,6 +1,8 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ - parse_quote, spanned::Spanned, Attribute, Error, Expr, ExprLit, Item, Lit, Meta, Path, - Visibility, + parse_quote, spanned::Spanned, Attribute, Error, Expr, ExprLit, ExprMacro, Item, Lit, Macro, + Meta, Path, Visibility, }; pub fn crate_module_path() -> Path { @@ -45,8 +47,45 @@ pub fn get_vis_name_attrs(item: &Item) -> syn::Result<(Visibility, String, &[Att } } -pub fn get_docs(attrs: &[Attribute], trim: bool) -> syn::Result> { - let string_literals = attrs +/// The processed value(s) of `#[doc = VAL]` attribute(s). +#[derive(Clone, Debug)] +enum DocValue<'a> { + /// At least one consecutive `/// foo` or `#{doc = "foo"]`. + /// + /// - Each literal value is trimmed if requested. + /// - Consecutive literal values are folded into one. + Lit(String), + /// `#[doc = include_str!("path")]`. + /// + /// No processing on this form because we don't have the expansion. + Macro(&'a Macro), +} +impl ToTokens for DocValue<'_> { + fn to_tokens(&self, ts: &mut TokenStream) { + let tokens = match self { + Self::Lit(lit) => quote! { #lit }, + Self::Macro(mac) => quote! { #mac }, + }; + ts.append_all(tokens); + } +} + +/// The processed and aggregated values of `#[doc = VAL]` attribute(s). +#[derive(Clone, Debug)] +pub struct DocContent<'a>(Vec>); +impl ToTokens for DocContent<'_> { + fn to_tokens(&self, ts: &mut TokenStream) { + let tokens = match self.0.as_slice() { + [] => unreachable!("0-length DocContent should not be produced"), + [single] => quote! { #single }, + [head, tail @ ..] => quote! { concat!(#head, #("\n", #tail),*) }, + }; + ts.append_all([tokens]); + } +} + +pub fn get_docs(attrs: &[Attribute], trim: bool) -> syn::Result>> { + let content = attrs .iter() .filter_map(|attr| match attr.meta { Meta::NameValue(ref name_value) if name_value.path.is_ident("doc") => { @@ -54,29 +93,48 @@ pub fn get_docs(attrs: &[Attribute], trim: bool) -> syn::Result> } _ => None, }) - .map(|expr| match expr { - Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) => Ok(s.value()), - other => Err(Error::new( - other.span(), - "Doc comment is not a string literal", - )), - }) - .collect::, _>>()?; - - if string_literals.is_empty() { - return Ok(None); - } + .try_fold(None, |docs: Option>, expr| -> syn::Result<_> { + let val = match expr { + Expr::Lit(ExprLit { lit: Lit::Str(lit), .. }) => { + let maybe_trimmed = if trim { + lit.value() + .split('\n') + .map(|line| line.trim().to_string()) + .collect::>() + .join("\n") + } else { + lit.value() + }; + DocValue::Lit(maybe_trimmed) + } + Expr::Macro(ExprMacro { mac, .. }) => DocValue::Macro(mac), + other => Err(Error::new( + other.span(), + "Doc comment is neither a string literal nor a macro invocation", + ))?, + }; - let docs = if trim { - string_literals - .iter() - .flat_map(|lit| lit.split('\n').collect::>()) - .map(|line| line.trim().to_string()) - .collect::>() - .join("\n") - } else { - string_literals.join("\n") - }; + let mut docs = docs.unwrap_or_default(); + match docs.as_mut_slice() { + // always push first element + [] => { + docs.push(val); + } + // try to fold subsequent elements + [.., tail] => match (tail, &val) { + // fold consecutive literals + (DocValue::Lit(tail), DocValue::Lit(lit)) => { + tail.push('\n'); + tail.push_str(lit); + } + // simple push otherwise + (_, _) => { + docs.push(val); + } + }, + } - Ok(Some(docs)) + Ok(Some(docs)) + })?; + Ok(content.map(DocContent)) } diff --git a/documented-test/src/derive/documented.rs b/documented-test/src/derive/documented.rs index c658ec7..06f6902 100644 --- a/documented-test/src/derive/documented.rs +++ b/documented-test/src/derive/documented.rs @@ -36,6 +36,17 @@ mod test_use { assert_eq!(Nicer::DOCS, docs); } + #[test] + fn macro_form_works() { + /// Famous saying: + #[doc = concat!("something something", " death and taxes")] + #[derive(Documented)] + struct FactOfLife; + + let docs = "Famous saying:\nsomething something death and taxes"; + assert_eq!(FactOfLife::DOCS, docs) + } + #[test] fn generic_type_works() { /// Wow diff --git a/lib/src/_caveats.rs b/lib/src/_caveats.rs new file mode 100644 index 0000000..7d6365b --- /dev/null +++ b/lib/src/_caveats.rs @@ -0,0 +1,36 @@ +//! # Caveats +//! +//! ## No line-trimming for macro-inserted doc comments +//! +//! The line-trimming feature does not work for doc comments inserted by macros: +//! +//! ``` +//! #[doc = concat!(" line 1\n", " line 2")] +//! #[derive(documented::Documented)] +//! struct Terrible; +//! +//! # use documented::Documented; +//! // trim is enabled (by default here) but does not work +//! assert_eq!(Terrible::DOCS, " line 1\n line 2"); +//! ``` +//! +//! This is because the expansion of your macro invocation +//! (e.g. `concat!`, `include_str!`, etc.) is not visible from the perspective +//! of the procedural macros of `documented`. Therefore it is not possible +//! (or rather, not practical) to do any post-processing on the text contents. +//! +//! Note that If an item has multiple `#[doc = ...]` attributes and only +//! a subset of them use the macro instead of literal form, line-trimming +//! will still work for the literal form attributes: +//! +//! ``` +//! /// line 1 +//! #[doc = concat!(" line 2\n", " line 3")] +//! #[doc = "line 4 "] +//! #[derive(documented::Documented)] +//! struct Terrible; +//! +//! # use documented::Documented; +//! // trim does not work for line 2 & 3 +//! assert_eq!(Terrible::DOCS, "line 1\n line 2\n line 3\nline 4"); +//! ``` diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 58a9372..8916a7d 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,5 +1,7 @@ #![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))] +pub mod _caveats; + pub use documented_macros::{ docs_const, Documented, DocumentedFields, DocumentedFieldsOpt, DocumentedOpt, DocumentedVariants, DocumentedVariantsOpt,