Skip to content

Commit

Permalink
Merge pull request #9 from geo-ant/feature/documenting-generics
Browse files Browse the repository at this point in the history
Feature/documenting generics
  • Loading branch information
geo-ant authored Oct 26, 2024
2 parents 599bddf + 0f16e5b commit 63e2557
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 54 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "roxygen"
version = "0.1.2"
version = "0.2.0"
edition = "2021"
authors = ["geo-ant"]
license = "MIT"
Expand Down
8 changes: 5 additions & 3 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -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:

Expand Down Expand Up @@ -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.
89 changes: 42 additions & 47 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<Attribute>)>::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;
Expand All @@ -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
Expand Down
120 changes: 119 additions & 1 deletion src/util.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -59,6 +60,8 @@ pub struct FunctionDocs {
pub after_args_section: Vec<Attribute>,
}

/// extract the documentation from the doc comments of the function and perform
/// some additional logic
pub fn extract_fn_doc_attrs(attrs: &mut Vec<Attribute>) -> Result<FunctionDocs, syn::Error> {
let mut before_args_section = Vec::with_capacity(attrs.len());
let mut after_args_section = Vec::with_capacity(attrs.len());
Expand Down Expand Up @@ -100,3 +103,118 @@ pub fn extract_fn_doc_attrs(attrs: &mut Vec<Attribute>) -> Result<FunctionDocs,
after_args_section,
})
}

/// an identifier (such as a function parameter or a generic type)
/// with doc attributes
pub struct DocumentedIdent<'a> {
pub ident: &'a Ident,
/// the doc comments
pub docs: Vec<Attribute>,
}

impl<'a> DocumentedIdent<'a> {
pub fn new(ident: &'a Ident, docs: Vec<Attribute>) -> 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<Vec<DocumentedIdent<'a>>, syn::Error>
where
I: Iterator<Item = &'a mut FnArg>,
{
// 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::<DocumentedIdent>::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<Vec<DocumentedIdent<'_>>, 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
/// **<caption>**
///
/// * `ident`: doc-comments
/// * `ident2`: doc-comments
/// * ...
///
/// returns an empty token stream if the list of idents is empty
pub fn make_doc_block<S>(
caption: S,
documented_idents: Vec<DocumentedIdent<'_>>,
) -> Option<TokenStream>
where
S: AsRef<str>,
{
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
}
}
21 changes: 21 additions & 0 deletions tests/expansion/roxygen_with_args_section_and_generics.expanded.rs
Original file line number Diff line number Diff line change
@@ -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
}
26 changes: 26 additions & 0 deletions tests/expansion/roxygen_with_args_section_and_generics.rs
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 2 additions & 2 deletions tests/fail/no_parameters_documented.stderr
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down

0 comments on commit 63e2557

Please sign in to comment.