Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions documented-macros/src/derive_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -50,7 +50,7 @@ impl DocType {
#[allow(clippy::type_complexity)]
fn docs_handler_opt<S>(
&self,
) -> Box<dyn Fn(Option<String>, Option<Expr>, S) -> syn::Result<TokenStream>>
) -> Box<dyn Fn(Option<DocContent>, Option<Expr>, S) -> syn::Result<TokenStream>>
where
S: ToTokens,
{
Expand Down
4 changes: 2 additions & 2 deletions documented-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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);
Expand Down
112 changes: 85 additions & 27 deletions documented-macros/src/util.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -45,38 +47,94 @@ pub fn get_vis_name_attrs(item: &Item) -> syn::Result<(Visibility, String, &[Att
}
}

pub fn get_docs(attrs: &[Attribute], trim: bool) -> syn::Result<Option<String>> {
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<DocValue<'a>>);
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<Option<DocContent<'_>>> {
let content = attrs
.iter()
.filter_map(|attr| match attr.meta {
Meta::NameValue(ref name_value) if name_value.path.is_ident("doc") => {
Some(&name_value.value)
}
_ => 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::<Result<Vec<_>, _>>()?;

if string_literals.is_empty() {
return Ok(None);
}
.try_fold(None, |docs: Option<Vec<_>>, 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::<Vec<_>>()
.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::<Vec<_>>())
.map(|line| line.trim().to_string())
.collect::<Vec<_>>()
.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))
}
11 changes: 11 additions & 0 deletions documented-test/src/derive/documented.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions lib/src/_caveats.rs
Original file line number Diff line number Diff line change
@@ -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");
//! ```
2 changes: 2 additions & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Loading