diff --git a/Cargo.toml b/Cargo.toml index f16e29b..15b4dbb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "roxygen" -version = "0.1.2" +version = "0.2.0" edition = "2021" authors = ["geo-ant"] license = "MIT" diff --git a/Readme.md b/Readme.md index a624052..7909587 100644 --- a/Readme.md +++ b/Readme.md @@ -7,7 +7,9 @@ ![maintenance-status](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg) The `#[roxygen]` attribute allows you to add doc-comments to function -parameters, which is a _compile error_ in current Rust. You can now write +parameters, which is a _compile error_ in current Rust. Generic lifetimes, +types, and constants of the function [can also be documented](https://docs.rs/roxygen/latest/roxygen/). +You can now write ```rust use roxygen::*; @@ -29,7 +31,7 @@ fn sum_image_rows( } ``` -You have to document at least one parameter, but you don't have +You have to document at least one parameter (or generic), but you don't have to document all of them. The example above will produce documentation as if you had written a doc comment for the function like so: @@ -120,5 +122,5 @@ this is a giant issue here for two reasons: firstly, this macro is to be used _s Secondly, this macro just does some light parsing and shuffling around of the documentation tokens. It introduces no additional code. Thus, it doesn't make your actual code more or less complex and should not affect compile -times much (that is after this crate was compiled once), but I haven't +times much (after this crate was compiled once), but I haven't measured it... so take it with a grain of sodium-chloride. diff --git a/src/lib.rs b/src/lib.rs index 751da42..7f55aaf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,34 @@ #![doc= include_str!("../Readme.md")] +//! ## Documenting Generics +//! Generic parameters can be documented with doc comments just as the arguments +//! can be: +//! ```rust +//! use roxygen::roxygen; +//! +//! #[roxygen] +//! fn frobnicate< +//! /// some comment goes here +//! S, +//! T> ( +//! /// the value being frobnicated +//! frobnicator: T, +//! /// the frobnicant +//! frobnicant: S) -> T +//! { +//! todo!() +//! } +//! ``` +//! This generates an additional section for the generic parameters right +//! after the arguments section (if it exists). +//! All types of generic arguments, including lifetimes and const-generics +//! can be documented like this. +//! use quote::{quote, ToTokens}; -use syn::{parse_macro_input, Attribute, FnArg, Ident, ItemFn, Pat}; -use util::{extract_doc_attrs, extract_fn_doc_attrs, prepend_to_doc_attribute}; +use syn::{parse_macro_input, Attribute, ItemFn}; +use util::{ + extract_documented_generics, extract_documented_parameters, extract_fn_doc_attrs, + make_doc_block, +}; mod util; // helper macro "try" on a syn::Error, so that we can return it as a token stream @@ -33,59 +60,29 @@ pub fn roxygen( } })); - // will contain the docs comments for each documented function parameter - // together with the identifier of the function parameter. - let mut parameter_docs = - Vec::<(&Ident, Vec)>::with_capacity(function.sig.inputs.len()); - // extrac the doc attributes on the function itself let function_docs = try2!(extract_fn_doc_attrs(&mut function.attrs)); - // extract the doc attributes on the parameters - for arg in function.sig.inputs.iter_mut() { - match arg { - FnArg::Typed(pat_type) => { - let Pat::Ident(pat_ident) = pat_type.pat.as_ref() else { - unreachable!("unexpected node while parsing"); - }; - let ident = &pat_ident.ident; - let docs = extract_doc_attrs(&mut pat_type.attrs); + let documented_params = try2!(extract_documented_parameters( + function.sig.inputs.iter_mut() + )); - if !docs.is_empty() { - parameter_docs.push((ident, docs)); - } - } - FnArg::Receiver(_) => {} - } - } + let documented_generics = try2!(extract_documented_generics(&mut function.sig.generics)); + + let has_documented_params = !documented_params.is_empty(); + let has_documented_generics = !documented_generics.is_empty(); - if parameter_docs.is_empty() { + if !has_documented_params && !has_documented_generics { return syn::Error::new_spanned( function.sig.ident, - "Function has no documented arguments.\nDocument at least one function argument.", + "Function has no documented parameters or generics.\nDocument at least one function parameter or generic.", ) .into_compile_error() .into(); } - let parameter_doc_blocks = parameter_docs.into_iter().map(|(ident, docs)| { - let mut docs_iter = docs.iter(); - // we always have at least one doc attribute because otherwise we - // would not have inserted this pair into the parameter docs in the - // first place - let first = docs_iter - .next() - .expect("unexpectedly encountered empty doc list"); - - let first_line = prepend_to_doc_attribute(&format!(" * `{}`:", ident), first); - - // we just need to indent the other lines, if they exist - let next_lines = docs_iter.map(|attr| prepend_to_doc_attribute(" ", attr)); - quote! { - #first_line - #(#next_lines)* - } - }); + let parameter_doc_block = make_doc_block("Arguments", documented_params); + let generics_doc_block = make_doc_block("Generics", documented_generics); let docs_before = function_docs.before_args_section; let docs_after = function_docs.after_args_section; @@ -97,10 +94,8 @@ pub fn roxygen( quote! { #(#docs_before)* - #[doc=""] - #[doc=" **Arguments**: "] - #[doc=""] - #(#parameter_doc_blocks)* + #parameter_doc_block + #generics_doc_block #maybe_empty_doc_line #(#docs_after)* #function diff --git a/src/util.rs b/src/util.rs index 6331875..989127c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,5 +1,6 @@ +use proc_macro2::TokenStream; use quote::quote; -use syn::{Attribute, Expr, LitStr, Meta, MetaNameValue}; +use syn::{Attribute, Expr, FnArg, Generics, Ident, LitStr, Meta, MetaNameValue, Pat}; use crate::is_arguments_section; @@ -59,6 +60,8 @@ pub struct FunctionDocs { pub after_args_section: Vec, } +/// extract the documentation from the doc comments of the function and perform +/// some additional logic pub fn extract_fn_doc_attrs(attrs: &mut Vec) -> Result { let mut before_args_section = Vec::with_capacity(attrs.len()); let mut after_args_section = Vec::with_capacity(attrs.len()); @@ -100,3 +103,118 @@ pub fn extract_fn_doc_attrs(attrs: &mut Vec) -> Result { + pub ident: &'a Ident, + /// the doc comments + pub docs: Vec, +} + +impl<'a> DocumentedIdent<'a> { + pub fn new(ident: &'a Ident, docs: Vec) -> Self { + Self { ident, docs } + } +} + +/// extract the parameter documentation from an iterator over function arguments. +/// This will also remove all the doc comments from the collection of attributes, but +/// will leave all the other attributes untouched. +pub fn extract_documented_parameters<'a, I>(args: I) -> Result>, syn::Error> +where + I: Iterator, +{ + // will contain the docs comments for each documented function parameter + // together with the identifier of the function parameter. + let (lower, upper) = args.size_hint(); + let mut documented_params = Vec::::with_capacity(upper.unwrap_or(lower)); + + for arg in args { + match arg { + FnArg::Typed(pat_type) => { + let Pat::Ident(pat_ident) = pat_type.pat.as_ref() else { + unreachable!("unexpected node while parsing"); + }; + let ident = &pat_ident.ident; + let docs = extract_doc_attrs(&mut pat_type.attrs); + + if !docs.is_empty() { + documented_params.push(DocumentedIdent::new(ident, docs)); + } + } + FnArg::Receiver(_) => {} + } + } + Ok(documented_params) +} + +/// same as extracting documentatio from parameters, but for generic types +pub fn extract_documented_generics( + generics: &'_ mut Generics, +) -> Result>, syn::Error> { + let mut documented_generics = Vec::with_capacity(generics.params.len()); + for param in generics.params.iter_mut() { + let (ident, attrs) = match param { + syn::GenericParam::Lifetime(lif) => (&lif.lifetime.ident, &mut lif.attrs), + syn::GenericParam::Type(ty) => (&ty.ident, &mut ty.attrs), + syn::GenericParam::Const(con) => (&con.ident, &mut con.attrs), + }; + let docs = extract_doc_attrs(attrs); + if !docs.is_empty() { + documented_generics.push(DocumentedIdent::new(ident, docs)) + } + } + + Ok(documented_generics) +} + +/// make a documentation block, which is a markdown list of +/// **** +/// +/// * `ident`: doc-comments +/// * `ident2`: doc-comments +/// * ... +/// +/// returns an empty token stream if the list of idents is empty +pub fn make_doc_block( + caption: S, + documented_idents: Vec>, +) -> Option +where + S: AsRef, +{ + let has_documented_idents = !documented_idents.is_empty(); + + let list = documented_idents.into_iter().map(|param| { + let mut docs_iter = param.docs.iter(); + // we always have at least one doc attribute because otherwise we + // would not have inserted this pair into the parameter docs in the + // first place + let first = docs_iter + .next() + .expect("unexpectedly encountered empty doc list"); + + let first_line = prepend_to_doc_attribute(&format!(" * `{}`:", param.ident), first); + + // we just need to indent the other lines, if they exist + let next_lines = docs_iter.map(|attr| prepend_to_doc_attribute(" ", attr)); + quote! { + #first_line + #(#next_lines)* + } + }); + + let caption = format!(" **{}**:", caption.as_ref()); + + if has_documented_idents { + Some(quote! { + #[doc=""] + #[doc=#caption] + #[doc=""] + #(#list)* + }) + } else { + None + } +} diff --git a/tests/expansion/roxygen_with_args_section_and_generics.expanded.rs b/tests/expansion/roxygen_with_args_section_and_generics.expanded.rs new file mode 100644 index 0000000..5fa537f --- /dev/null +++ b/tests/expansion/roxygen_with_args_section_and_generics.expanded.rs @@ -0,0 +1,21 @@ +use roxygen::*; +/// this is documentation +/// and this is too +/// +/// **Arguments**: +/// +/// * `bar`: this has one line of docs +/// * `baz`: this has +/// two lines of docs +/// +/// **Generics**: +/// +/// * `a`: a lifetime +/// * `T`: documentation for parameter T +/// spans multiple lines +/// * `N`: a const generic +/// +/// this goes after the arguments section +fn foo<'a, S, T, const N: usize>(bar: u32, baz: String, _undocumented: i32) -> bool { + baz.len() > bar as usize +} diff --git a/tests/expansion/roxygen_with_args_section_and_generics.rs b/tests/expansion/roxygen_with_args_section_and_generics.rs new file mode 100644 index 0000000..babed29 --- /dev/null +++ b/tests/expansion/roxygen_with_args_section_and_generics.rs @@ -0,0 +1,26 @@ +use roxygen::*; + +#[roxygen] +/// this is documentation +/// and this is too +#[arguments_section] +/// this goes after the arguments section +fn foo< + /// a lifetime + 'a, + S, + /// documentation for parameter T + /// spans multiple lines + T, + /// a const generic + const N: usize, +>( + /// this has one line of docs + bar: u32, + /// this has + /// two lines of docs + baz: String, + _undocumented: i32, +) -> bool { + baz.len() > bar as usize +} diff --git a/tests/fail/no_parameters_documented.stderr b/tests/fail/no_parameters_documented.stderr index dd69fb4..f0e5cb2 100644 --- a/tests/fail/no_parameters_documented.stderr +++ b/tests/fail/no_parameters_documented.stderr @@ -1,5 +1,5 @@ -error: Function has no documented arguments. - Document at least one function argument. +error: Function has no documented parameters or generics. + Document at least one function parameter or generic. --> tests/fail/no_parameters_documented.rs:6:8 | 6 | pub fn add(first: i32, second: i32) -> i32 {