├── .github └── workflows │ ├── build.yml │ ├── lints.yml │ └── tests.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── Readme.md ├── src ├── lib.rs └── util.rs └── tests ├── compilation_tests.rs ├── expansion ├── roxygen_only.expanded.rs ├── roxygen_only.rs ├── roxygen_with_args_section_and_generics.expanded.rs ├── roxygen_with_args_section_and_generics.rs ├── roxygen_with_argument_section.expanded.rs ├── roxygen_with_argument_section.rs ├── roxygen_with_parameters_section_from_readme.expanded.rs └── roxygen_with_parameters_section_from_readme.rs ├── fail ├── duplicate_arguments_section.rs ├── duplicate_arguments_section.stderr ├── duplicate_roxygen_attr.rs ├── duplicate_roxygen_attr.stderr ├── no_parameters_documented.rs ├── no_parameters_documented.stderr ├── wrong_arguments_section_placement.rs ├── wrong_arguments_section_placement.stderr ├── wrong_fully_qual_parameters_section_placement.rs └── wrong_fully_qual_parameters_section_placement.stderr ├── macro_expansions.rs └── smoketest.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | # Runs the workflow at 00:00 on the first day of every month 10 | - cron: '0 0 1 * *' 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | check: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Check formatting 21 | run: cargo check --workspace --all-targets --all-features 22 | -------------------------------------------------------------------------------- /.github/workflows/lints.yml: -------------------------------------------------------------------------------- 1 | name: lints 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | # Runs the workflow at 00:00 on the first day of every month 10 | - cron: '0 0 1 * *' 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | check-fmt: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Check formatting 21 | run: cargo fmt -- --check 22 | clippy: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Install clippy 27 | run: rustup component add clippy 28 | - name: Run clippy 29 | run: cargo clippy 30 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | # Runs the workflow at 00:00 on the first day of every month 10 | - cron: '0 0 1 * *' 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Install cargo-expand 21 | run: cargo install cargo-expand 22 | - name: Run tests 23 | run: cargo test --workspace --all-targets --all-features 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | # RustRover 17 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 18 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 19 | # and can be added to the global gitignore or merged into this file. For a more nuclear 20 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 21 | #.idea/ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "roxygen" 3 | version = "1.0.4" 4 | edition = "2021" 5 | authors = ["geo-ant"] 6 | license = "MIT" 7 | homepage = "https://github.com/geo-ant/roxygen" 8 | repository = "https://github.com/geo-ant/roxygen" 9 | description = "Seamlessly document function parameters with rustdoc" 10 | readme = "Readme.md" 11 | categories = ["rust-patterns","development-tools"] 12 | keywords = ["document", "function", "parameters", "arguments", "doxygen"] 13 | 14 | [badges] 15 | maintenance = { status = "passively-maintained" } 16 | 17 | [lib] 18 | proc-macro = true 19 | 20 | [dependencies] 21 | quote = "1" 22 | proc-macro2 = "1.0" 23 | syn = {version="2.0",features=["parsing","full"]} 24 | 25 | [dev-dependencies] 26 | trybuild = "1.0" 27 | macrotest = "1.0" 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Geo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # roxygen - documenting function parameters 2 | 3 | ![build](https://github.com/geo-ant/roxygen/actions/workflows/build.yml/badge.svg?branch=main) 4 | ![tests](https://github.com/geo-ant/roxygen/actions/workflows/tests.yml/badge.svg?branch=main) 5 | ![lints](https://github.com/geo-ant/roxygen/actions/workflows/lints.yml/badge.svg?branch=main) 6 | [![crates](https://img.shields.io/crates/v/roxygen)](https://crates.io/crates/roxygen) 7 | ![maintenance-status](https://img.shields.io/badge/maintenance-passively--maintained-yellowgreen.svg) 8 | [![crates](https://raw.githubusercontent.com/geo-ant/user-content/refs/heads/main/ko-fi-support.svg)](https://ko-fi.com/geoant) 9 | 10 | The `#[roxygen]` attribute allows you to add doc-comments to function 11 | parameters, which is a _compile error_ in current Rust. Generic lifetimes, 12 | types, and constants of the function [can also be documented](https://docs.rs/roxygen/latest/roxygen/). 13 | You can now write 14 | 15 | ```rust 16 | use roxygen::*; 17 | 18 | #[roxygen] 19 | /// sum the rows of an image 20 | fn sum_image_rows( 21 | /// the image data in row-major format 22 | image_data: &[f32], 23 | /// the number of rows in the image 24 | nrows: u32, 25 | /// the number of columns in the image 26 | ncols: u32, 27 | /// an out buffer into which the resulting 28 | /// sums are placed. Must have space 29 | /// for exactly `nrows` elements 30 | sums: &mut [f32]) -> Result<(),String> { 31 | todo!() 32 | } 33 | ``` 34 | 35 | You have to document at least one parameter (or generic), but you don't have 36 | to document all of them. The example above will produce documentation as 37 | if you had written a doc comment for the function like so: 38 | 39 | ```rust 40 | /// sum the rows of an image 41 | /// 42 | /// **Parameters**: 43 | /// 44 | /// * `image_data`: the image data in row-major format 45 | /// * `nrows`: the number of rows in the image 46 | /// * `ncols`: the number of columns in the image 47 | /// * `sums`: an out buffer into which the resulting 48 | /// sums are placed. Must have space 49 | /// for exactly `nrows` elements 50 | fn sum_image_rows( 51 | image_data: &[f32], 52 | nrows: u32, 53 | ncols: u32, 54 | sums: &mut [f32]) -> Result<(),String> { 55 | todo!() 56 | } 57 | ``` 58 | 59 | ⚠️ **Renaming** the macros exported from this crate (`use ... as ...`) or renaming the 60 | crate itself (in your `Cargo.toml`) will make all of this stop working properly. 61 | 62 | ## Placing the Parameters-Section 63 | 64 | By default, the section documenting the parameters will go at the end 65 | of the top-level function documentation. However, this crate allows to explicitly 66 | place the section by using a custom attribute like so: 67 | 68 | ```rust 69 | use roxygen::roxygen; 70 | 71 | #[roxygen] 72 | /// long documention 73 | /// ... 74 | #[roxygen::parameters_section] 75 | /// # Examples 76 | /// ... 77 | fn foo( 78 | /// some docs 79 | first: i32, 80 | second: f32 81 | ) 82 | {} 83 | ``` 84 | 85 | ## Considerations 86 | 87 | It's a [long standing issue](https://github.com/rust-lang/rust/issues/57525) 88 | whether and how to add this capability to `rustdoc`. Firstly, there's no 89 | general consensus on how exactly to document function parameters. However, 90 | I've seen the presented style used a lot, with minor variations. 91 | Secondly, the standard library [doesn't need this](https://github.com/rust-lang/rust/issues/57525#issuecomment-453633783) 92 | style of documentation at all. So before you stick this macro on every function, 93 | do consider 94 | 95 | * taking inspiration from how the standard library deals with function parameters, 96 | * using fewer function parameters, 97 | * using more descriptive parameters names, 98 | * using _types_ to communicate intent, 99 | * sticking function parameters in a `struct`. 100 | 101 | Here is [an elegant way](https://www.reddit.com/r/rust/comments/1gb782e/comment/ltpk16x/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button), 102 | how the example above can be reworked without using per parameter documentation: 103 | 104 | ```rust 105 | /// Sums the rows of an image. 106 | /// 107 | /// The rows of `image_data`, an `nrows` by `ncols` 108 | /// matrix in row-major ordering, are summed into `sums` 109 | /// which must have exactly `nrows` elements. 110 | fn sum_image_rows( 111 | image_data: &[f32], 112 | nrows: u32, 113 | ncols: u32, 114 | sums: &mut [f32]) -> Result<(),String> { 115 | todo!() 116 | } 117 | ``` 118 | 119 | All that being said, I've realized that sometimes I still want to document 120 | function parameters. 121 | 122 | ### Compile Times 123 | 124 | Macros will always increase your compile time to some degree, but I don't think 125 | this is a giant issue (after the roxygen dependency itself was compiled, that is): 126 | firstly, this macro is to be used _sparingly_. Secondly, this macro just does 127 | some light parsing and shuffling around of the documentation tokens. It 128 | introduces no additional code. Thus, it doesn't 129 | make your actual code more or less complex and should not affect compile 130 | times much (after this crate was compiled once), but I haven't 131 | measured it... so take it with a grain of sodium-chloride. 132 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc= include_str!("../Readme.md")] 2 | //! ## Documenting Generics 3 | //! Generic parameters can be documented with doc comments just as the arguments 4 | //! can be: 5 | //! ```rust 6 | //! use roxygen::roxygen; 7 | //! 8 | //! #[roxygen] 9 | //! fn frobnicate< 10 | //! /// some comment goes here 11 | //! S, 12 | //! T> ( 13 | //! /// the value being frobnicated 14 | //! frobnicator: T, 15 | //! /// the frobnicant 16 | //! frobnicant: S) -> T 17 | //! { 18 | //! todo!() 19 | //! } 20 | //! ``` 21 | //! 22 | //! This generates an additional section for the generic parameters right 23 | //! after the arguments section (if it exists). 24 | //! All types of generic arguments, including lifetimes and const-generics 25 | //! can be documented like this. 26 | use quote::{quote, ToTokens}; 27 | use syn::{parse_macro_input, Attribute, ItemFn}; 28 | use util::{ 29 | extract_documented_generics, extract_documented_parameters, extract_fn_doc_attrs, 30 | make_doc_block, 31 | }; 32 | mod util; 33 | 34 | /// parameter section macro name 35 | const PARAM_SECTION: &str = "parameters_section"; 36 | /// the name of this crate 37 | const ROXYGEN_CRATE: &str = "roxygen"; 38 | /// the name of the main macro in this crate 39 | const ROXYGEN_MACRO: &str = ROXYGEN_CRATE; 40 | 41 | // helper macro "try" on a syn::Error, so that we can return it as a token stream 42 | macro_rules! try2 { 43 | ($ex:expr) => { 44 | match $ex { 45 | Ok(val) => val, 46 | Err(err) => return err.into_compile_error().into(), 47 | } 48 | }; 49 | } 50 | 51 | #[proc_macro_attribute] 52 | /// the principal attribute inside this crate that lets us document function arguments 53 | pub fn roxygen( 54 | _attr: proc_macro::TokenStream, 55 | item: proc_macro::TokenStream, 56 | ) -> proc_macro::TokenStream { 57 | let mut function: ItemFn = parse_macro_input!(item as ItemFn); 58 | 59 | try2!(function.attrs.iter_mut().try_for_each(|attr| { 60 | if is_roxygen_main(attr) { 61 | Err(syn::Error::new_spanned( 62 | attr, 63 | "Duplicate attribute. This attribute must only appear once.", 64 | )) 65 | } else { 66 | Ok(()) 67 | } 68 | })); 69 | 70 | // extrac the doc attributes on the function itself 71 | let function_docs = try2!(extract_fn_doc_attrs(&mut function.attrs)); 72 | 73 | let documented_params = try2!(extract_documented_parameters( 74 | function.sig.inputs.iter_mut() 75 | )); 76 | 77 | let documented_generics = try2!(extract_documented_generics(&mut function.sig.generics)); 78 | 79 | let has_documented_params = !documented_params.is_empty(); 80 | let has_documented_generics = !documented_generics.is_empty(); 81 | 82 | if !has_documented_params && !has_documented_generics { 83 | return syn::Error::new_spanned( 84 | function.sig.ident, 85 | "Function has no documented parameters or generics.\nDocument at least one function parameter or generic.", 86 | ) 87 | .into_compile_error() 88 | .into(); 89 | } 90 | 91 | let parameter_doc_block = make_doc_block("Parameters", documented_params); 92 | let generics_doc_block = make_doc_block("Generics", documented_generics); 93 | 94 | let docs_before = function_docs.before_args_section; 95 | let docs_after = function_docs.after_args_section; 96 | let maybe_empty_doc_line = if !docs_after.is_empty() { 97 | Some(quote! {#[doc=""]}) 98 | } else { 99 | None 100 | }; 101 | 102 | quote! { 103 | #(#docs_before)* 104 | #parameter_doc_block 105 | #generics_doc_block 106 | #maybe_empty_doc_line 107 | #(#docs_after)* 108 | #function 109 | } 110 | .into() 111 | } 112 | 113 | // this is to expose the helper attribute #[arguments_section]. 114 | // The only logic about this attribute that this here function includes is 115 | // to make sure that this attribute is not placed before the #[roxygen] 116 | // attribute. All other logic is handled in the roxygen macro itself. 117 | /// a helper attribute that dictates the placement of the section documenting 118 | /// the function arguments 119 | #[proc_macro_attribute] 120 | pub fn parameters_section( 121 | _attr: proc_macro::TokenStream, 122 | item: proc_macro::TokenStream, 123 | ) -> proc_macro::TokenStream { 124 | let function: ItemFn = parse_macro_input!(item as ItemFn); 125 | 126 | // enforce that this macro comes after roxygen, which means it 127 | // cannot see the roxygen attribute 128 | let maybe_roxygen = function.attrs.iter().find(|attr| is_roxygen_main(attr)); 129 | if let Some(attr) = maybe_roxygen { 130 | syn::Error::new_spanned(attr,"The #[roxygen] attribute must come before the parameters_section attribute.\nPlace it before any of the doc comments for the function.").into_compile_error().into() 131 | } else { 132 | function.to_token_stream().into() 133 | } 134 | } 135 | 136 | /// check whether an attribute is the arguments section attribute. 137 | /// Stick this into it's own function so I can change the logic 138 | //@note(geo) this logic won't work if the crate is renamed 139 | #[inline(always)] 140 | fn is_parameters_section(attr: &Attribute) -> bool { 141 | let path = attr.path(); 142 | 143 | if path.is_ident(PARAM_SECTION) { 144 | true 145 | } else { 146 | // checks for (::)roxygen::param_section 147 | path.segments.len() == 2 148 | && path.segments[0].ident == ROXYGEN_CRATE 149 | && path.segments[1].ident == PARAM_SECTION 150 | } 151 | } 152 | 153 | /// check whether an attribute is the raw #[roxygen] main attribute. 154 | /// Stuck into this function, so I can refactor this logic 155 | //@note(geo) this logic won't work if the crate is renamed 156 | #[inline(always)] 157 | fn is_roxygen_main(attr: &Attribute) -> bool { 158 | let path = attr.path(); 159 | 160 | if path.is_ident(ROXYGEN_MACRO) { 161 | true 162 | } else { 163 | // checks for (::)roxygen::roxygen 164 | path.segments.len() == 2 165 | && path.segments[0].ident == ROXYGEN_CRATE 166 | && path.segments[1].ident == ROXYGEN_MACRO 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{Attribute, Expr, FnArg, Generics, Ident, LitStr, Meta, MetaNameValue, Pat}; 4 | 5 | use crate::is_parameters_section; 6 | 7 | /// Function to prepend a string to a `#[doc]` attribute. 8 | pub fn prepend_to_doc_attribute(prepend_text: &str, attr: &Attribute) -> proc_macro2::TokenStream { 9 | // Parse the attribute to see if it's a MetaNameValue (e.g., #[doc = "..."]) 10 | assert!( 11 | attr.path().is_ident("doc"), 12 | "function must only be called on doc attributes" 13 | ); 14 | if let Meta::NameValue(MetaNameValue { 15 | value: Expr::Lit(ref lit), 16 | .. 17 | }) = attr.meta 18 | { 19 | let syn::ExprLit { 20 | attrs: _, 21 | lit: syn::Lit::Str(doc_string), 22 | } = lit 23 | else { 24 | unreachable!("reached unexpected node while parsing"); 25 | }; 26 | // Prepend the text to the existing doc comment 27 | let new_doc = format!("{}{}", prepend_text, doc_string.value()); 28 | 29 | // Create a new string literal with the modified doc string 30 | let new_doc_lit = LitStr::new(&new_doc, doc_string.span()); 31 | 32 | // Create a new attribute with the modified doc string (enclosed in quotes) 33 | let new_attr = quote! { 34 | #[doc = #new_doc_lit] 35 | }; 36 | new_attr 37 | } else { 38 | unreachable!("reached unexpected node while parsing"); 39 | } 40 | } 41 | 42 | /// removes the #[doc...] attributes from `attrs` and returns them in 43 | /// it's own vector. 44 | pub fn extract_doc_attrs(attrs: &mut Vec) -> Vec { 45 | let doc_attrs = attrs 46 | .iter() 47 | .filter(|attr| attr.path().is_ident("doc")) 48 | .cloned() 49 | .collect(); 50 | attrs.retain(|attr| !attr.path().is_ident("doc")); 51 | doc_attrs 52 | } 53 | 54 | /// function doc attributes split by whether they occur above or below 55 | /// the attribute that indicates where the argument section should be placed. 56 | /// If no such attribute is present, we stick all the doc attributes in the 57 | /// before section 58 | pub struct FunctionDocs { 59 | pub before_args_section: Vec, 60 | pub after_args_section: Vec, 61 | } 62 | 63 | /// extract the documentation from the doc comments of the function and perform 64 | /// some additional logic 65 | pub fn extract_fn_doc_attrs(attrs: &mut Vec) -> Result { 66 | let mut before_args_section = Vec::with_capacity(attrs.len()); 67 | let mut after_args_section = Vec::with_capacity(attrs.len()); 68 | 69 | // I'm sure this could be done with iterators... 70 | // I'm just too dumb to do that now 71 | let mut idx = 0; 72 | // parse the arguments before the arguments-section attribute 73 | while idx < attrs.len() { 74 | let current_attr = attrs.get(idx).unwrap(); 75 | if is_parameters_section(current_attr) { 76 | idx += 1; 77 | break; 78 | } 79 | if current_attr.path().is_ident("doc") { 80 | before_args_section.push(current_attr.clone()); 81 | } 82 | idx += 1; 83 | } 84 | 85 | while idx < attrs.len() { 86 | let current_attr = attrs.get(idx).unwrap(); 87 | if is_parameters_section(current_attr) { 88 | return Err(syn::Error::new_spanned( 89 | current_attr, 90 | "Duplicate attribute not allowed.", 91 | )); 92 | } 93 | if current_attr.path().is_ident("doc") { 94 | after_args_section.push(current_attr.clone()); 95 | } 96 | idx += 1; 97 | } 98 | 99 | // delete all doc attributes from the function (and the arguments section attributes that I don't need anymore) 100 | attrs.retain(|attr| !attr.path().is_ident("doc")); 101 | Ok(FunctionDocs { 102 | before_args_section, 103 | after_args_section, 104 | }) 105 | } 106 | 107 | /// an identifier (such as a function parameter or a generic type) 108 | /// with doc attributes 109 | pub struct DocumentedIdent<'a> { 110 | pub ident: &'a Ident, 111 | /// the doc comments 112 | pub docs: Vec, 113 | } 114 | 115 | impl<'a> DocumentedIdent<'a> { 116 | pub fn new(ident: &'a Ident, docs: Vec) -> Self { 117 | Self { ident, docs } 118 | } 119 | } 120 | 121 | /// extract the parameter documentation from an iterator over function arguments. 122 | /// This will also remove all the doc comments from the collection of attributes, but 123 | /// will leave all the other attributes untouched. 124 | pub fn extract_documented_parameters<'a, I>(args: I) -> Result>, syn::Error> 125 | where 126 | I: Iterator, 127 | { 128 | // will contain the docs comments for each documented function parameter 129 | // together with the identifier of the function parameter. 130 | let (lower, upper) = args.size_hint(); 131 | let mut documented_params = Vec::::with_capacity(upper.unwrap_or(lower)); 132 | 133 | for arg in args { 134 | match arg { 135 | FnArg::Typed(pat_type) => { 136 | let Pat::Ident(pat_ident) = pat_type.pat.as_ref() else { 137 | unreachable!("unexpected node while parsing"); 138 | }; 139 | let ident = &pat_ident.ident; 140 | let docs = extract_doc_attrs(&mut pat_type.attrs); 141 | 142 | if !docs.is_empty() { 143 | documented_params.push(DocumentedIdent::new(ident, docs)); 144 | } 145 | } 146 | FnArg::Receiver(_) => {} 147 | } 148 | } 149 | Ok(documented_params) 150 | } 151 | 152 | /// extract the documented generic parameters from a collection of generics as 153 | /// a collection of documented identifiers 154 | /// This will also remove all the doc comments from the collection of generics, but 155 | /// will leave all the other attributes untouched. 156 | pub fn extract_documented_generics( 157 | generics: &'_ mut Generics, 158 | ) -> Result>, syn::Error> { 159 | let mut documented_generics = Vec::with_capacity(generics.params.len()); 160 | for param in generics.params.iter_mut() { 161 | let (ident, attrs) = match param { 162 | syn::GenericParam::Lifetime(lif) => (&lif.lifetime.ident, &mut lif.attrs), 163 | syn::GenericParam::Type(ty) => (&ty.ident, &mut ty.attrs), 164 | syn::GenericParam::Const(con) => (&con.ident, &mut con.attrs), 165 | }; 166 | let docs = extract_doc_attrs(attrs); 167 | if !docs.is_empty() { 168 | documented_generics.push(DocumentedIdent::new(ident, docs)) 169 | } 170 | } 171 | 172 | Ok(documented_generics) 173 | } 174 | 175 | /// make a documentation block, which is a markdown list of 176 | /// **** 177 | /// 178 | /// * `ident`: doc-comments 179 | /// * `ident2`: doc-comments 180 | /// * ... 181 | /// 182 | /// returns an empty token stream if the list of idents is empty 183 | pub fn make_doc_block( 184 | caption: S, 185 | documented_idents: Vec>, 186 | ) -> Option 187 | where 188 | S: AsRef, 189 | { 190 | let has_documented_idents = !documented_idents.is_empty(); 191 | 192 | let list = documented_idents.into_iter().map(|param| { 193 | let mut docs_iter = param.docs.iter(); 194 | // we always have at least one doc attribute because otherwise we 195 | // would not have inserted this pair into the parameter docs in the 196 | // first place 197 | let first = docs_iter 198 | .next() 199 | .expect("unexpectedly encountered empty doc list"); 200 | 201 | let first_line = prepend_to_doc_attribute(&format!(" * `{}`:", param.ident), first); 202 | 203 | // we just need to indent the other lines, if they exist 204 | let next_lines = docs_iter.map(|attr| prepend_to_doc_attribute(" ", attr)); 205 | quote! { 206 | #first_line 207 | #(#next_lines)* 208 | } 209 | }); 210 | 211 | let caption = format!(" **{}**:", caption.as_ref()); 212 | 213 | if has_documented_idents { 214 | Some(quote! { 215 | #[doc=""] 216 | #[doc=#caption] 217 | #[doc=""] 218 | #(#list)* 219 | }) 220 | } else { 221 | None 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /tests/compilation_tests.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn expected_compilation_errors_fail() { 3 | let t = trybuild::TestCases::new(); 4 | t.compile_fail("tests/fail/*.rs") 5 | } 6 | -------------------------------------------------------------------------------- /tests/expansion/roxygen_only.expanded.rs: -------------------------------------------------------------------------------- 1 | use roxygen::*; 2 | /// this is documentation 3 | /// and this is too 4 | /// 5 | /// **Parameters**: 6 | /// 7 | /// * `bar`: this has one line of docs 8 | /// * `baz`: this has 9 | /// two lines of docs 10 | fn foo(bar: u32, baz: String, _undocumented: i32) -> bool { 11 | baz.len() > bar as usize 12 | } 13 | -------------------------------------------------------------------------------- /tests/expansion/roxygen_only.rs: -------------------------------------------------------------------------------- 1 | use roxygen::*; 2 | 3 | #[roxygen] 4 | /// this is documentation 5 | /// and this is too 6 | // but this is not 7 | fn foo( 8 | /// this has one line of docs 9 | bar: u32, 10 | /// this has 11 | /// two lines of docs 12 | baz: String, 13 | _undocumented: i32, 14 | ) -> bool { 15 | baz.len() > bar as usize 16 | } 17 | -------------------------------------------------------------------------------- /tests/expansion/roxygen_with_args_section_and_generics.expanded.rs: -------------------------------------------------------------------------------- 1 | use roxygen::*; 2 | /// this is documentation 3 | /// and this is too 4 | /// 5 | /// **Parameters**: 6 | /// 7 | /// * `bar`: this has one line of docs 8 | /// * `baz`: this has 9 | /// two lines of docs 10 | /// 11 | /// **Generics**: 12 | /// 13 | /// * `a`: a lifetime 14 | /// * `T`: documentation for parameter T 15 | /// spans multiple lines 16 | /// * `N`: a const generic 17 | /// 18 | /// this goes after the arguments section 19 | fn foo<'a, S, T, const N: usize>(bar: u32, baz: String, _undocumented: i32) -> bool { 20 | baz.len() > bar as usize 21 | } 22 | -------------------------------------------------------------------------------- /tests/expansion/roxygen_with_args_section_and_generics.rs: -------------------------------------------------------------------------------- 1 | use roxygen::*; 2 | 3 | #[roxygen] 4 | /// this is documentation 5 | /// and this is too 6 | #[parameters_section] 7 | /// this goes after the arguments section 8 | fn foo< 9 | /// a lifetime 10 | 'a, 11 | S, 12 | /// documentation for parameter T 13 | /// spans multiple lines 14 | T, 15 | /// a const generic 16 | const N: usize, 17 | >( 18 | /// this has one line of docs 19 | bar: u32, 20 | /// this has 21 | /// two lines of docs 22 | baz: String, 23 | _undocumented: i32, 24 | ) -> bool { 25 | baz.len() > bar as usize 26 | } 27 | -------------------------------------------------------------------------------- /tests/expansion/roxygen_with_argument_section.expanded.rs: -------------------------------------------------------------------------------- 1 | use roxygen::*; 2 | /// this is documentation 3 | /// and this is too 4 | /// 5 | /// **Parameters**: 6 | /// 7 | /// * `bar`: this has one line of docs 8 | /// * `baz`: this has 9 | /// two lines of docs 10 | /// 11 | /// this goes after the arguments section 12 | fn foo(bar: u32, baz: String, _undocumented: i32) -> bool { 13 | baz.len() > bar as usize 14 | } 15 | -------------------------------------------------------------------------------- /tests/expansion/roxygen_with_argument_section.rs: -------------------------------------------------------------------------------- 1 | use roxygen::*; 2 | 3 | #[roxygen] 4 | /// this is documentation 5 | /// and this is too 6 | #[parameters_section] 7 | /// this goes after the arguments section 8 | fn foo( 9 | /// this has one line of docs 10 | bar: u32, 11 | /// this has 12 | /// two lines of docs 13 | baz: String, 14 | _undocumented: i32, 15 | ) -> bool { 16 | baz.len() > bar as usize 17 | } 18 | -------------------------------------------------------------------------------- /tests/expansion/roxygen_with_parameters_section_from_readme.expanded.rs: -------------------------------------------------------------------------------- 1 | use roxygen::roxygen; 2 | /// long documention 3 | /// ... 4 | /// 5 | /// **Parameters**: 6 | /// 7 | /// * `first`: some docs 8 | /// 9 | /// # Examples 10 | /// ... 11 | fn foo(first: i32, second: f32) {} 12 | -------------------------------------------------------------------------------- /tests/expansion/roxygen_with_parameters_section_from_readme.rs: -------------------------------------------------------------------------------- 1 | use roxygen::roxygen; 2 | 3 | #[roxygen] 4 | /// long documention 5 | /// ... 6 | #[roxygen::parameters_section] 7 | /// # Examples 8 | /// ... 9 | fn foo( 10 | /// some docs 11 | first: i32, 12 | second: f32, 13 | ) { 14 | } 15 | -------------------------------------------------------------------------------- /tests/fail/duplicate_arguments_section.rs: -------------------------------------------------------------------------------- 1 | use roxygen::*; 2 | 3 | #[roxygen] 4 | /// here are some comments 5 | /// and some more 6 | #[parameters_section] 7 | /// and some more 8 | /// but this next argument section should not be here 9 | #[parameters_section] 10 | pub fn add( 11 | /// some comments 12 | first: i32, 13 | second: i32, 14 | ) -> i32 { 15 | first + second 16 | } 17 | 18 | pub fn main() {} 19 | -------------------------------------------------------------------------------- /tests/fail/duplicate_arguments_section.stderr: -------------------------------------------------------------------------------- 1 | error: Duplicate attribute not allowed. 2 | --> tests/fail/duplicate_arguments_section.rs:9:1 3 | | 4 | 9 | #[parameters_section] 5 | | ^^^^^^^^^^^^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/fail/duplicate_roxygen_attr.rs: -------------------------------------------------------------------------------- 1 | use roxygen::*; 2 | 3 | #[roxygen] 4 | /// here are some comments 5 | #[roxygen] 6 | /// and some more 7 | #[parameters_section] 8 | /// and some more 9 | /// but this next argument section should not be here 10 | #[parameters_section] 11 | pub fn add( 12 | /// some comments 13 | first: i32, 14 | second: i32, 15 | ) -> i32 { 16 | first + second 17 | } 18 | 19 | pub fn main() {} 20 | -------------------------------------------------------------------------------- /tests/fail/duplicate_roxygen_attr.stderr: -------------------------------------------------------------------------------- 1 | error: Duplicate attribute. This attribute must only appear once. 2 | --> tests/fail/duplicate_roxygen_attr.rs:5:1 3 | | 4 | 5 | #[roxygen] 5 | | ^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/fail/no_parameters_documented.rs: -------------------------------------------------------------------------------- 1 | use roxygen::*; 2 | 3 | #[roxygen] 4 | /// no function parameters documented 5 | /// and some more 6 | pub fn add(first: i32, second: i32) -> i32 { 7 | first + second 8 | } 9 | 10 | pub fn main() {} 11 | -------------------------------------------------------------------------------- /tests/fail/no_parameters_documented.stderr: -------------------------------------------------------------------------------- 1 | error: Function has no documented parameters or generics. 2 | Document at least one function parameter or generic. 3 | --> tests/fail/no_parameters_documented.rs:6:8 4 | | 5 | 6 | pub fn add(first: i32, second: i32) -> i32 { 6 | | ^^^ 7 | -------------------------------------------------------------------------------- /tests/fail/wrong_arguments_section_placement.rs: -------------------------------------------------------------------------------- 1 | use roxygen::*; 2 | 3 | /// here are some comments 4 | /// this arguments section should not be here 5 | #[parameters_section] 6 | /// and some more 7 | #[roxygen] 8 | pub fn add( 9 | /// some comments 10 | first: i32, 11 | second: i32, 12 | ) -> i32 { 13 | first + second 14 | } 15 | 16 | pub fn main() {} 17 | -------------------------------------------------------------------------------- /tests/fail/wrong_arguments_section_placement.stderr: -------------------------------------------------------------------------------- 1 | error: The #[roxygen] attribute must come before the parameters_section attribute. 2 | Place it before any of the doc comments for the function. 3 | --> tests/fail/wrong_arguments_section_placement.rs:7:1 4 | | 5 | 7 | #[roxygen] 6 | | ^^^^^^^^^^ 7 | -------------------------------------------------------------------------------- /tests/fail/wrong_fully_qual_parameters_section_placement.rs: -------------------------------------------------------------------------------- 1 | /// here are some comments 2 | /// this arguments section should not be here 3 | #[roxygen::parameters_section] 4 | /// and some more 5 | #[roxygen::roxygen] 6 | pub fn add( 7 | /// some comments 8 | first: i32, 9 | second: i32, 10 | ) -> i32 { 11 | first + second 12 | } 13 | 14 | pub fn main() {} 15 | -------------------------------------------------------------------------------- /tests/fail/wrong_fully_qual_parameters_section_placement.stderr: -------------------------------------------------------------------------------- 1 | error: The #[roxygen] attribute must come before the parameters_section attribute. 2 | Place it before any of the doc comments for the function. 3 | --> tests/fail/wrong_fully_qual_parameters_section_placement.rs:5:1 4 | | 5 | 5 | #[roxygen::roxygen] 6 | | ^^^^^^^^^^^^^^^^^^^ 7 | -------------------------------------------------------------------------------- /tests/macro_expansions.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn macro_expansion() { 3 | macrotest::expand("tests/expansion/*.rs"); 4 | } 5 | -------------------------------------------------------------------------------- /tests/smoketest.rs: -------------------------------------------------------------------------------- 1 | use roxygen::*; 2 | 3 | // just a smoke test that the proc macro can indeed be used like this. 4 | // the real tests are in the macro expansion tests. 5 | #[roxygen] 6 | /// hello 7 | #[parameters_section] 8 | /// this 9 | /// is doc 10 | fn foo( 11 | /// some comments 12 | /// more comments 13 | first: i32, 14 | second: f32, 15 | ) -> f32 { 16 | first as f32 - second 17 | } 18 | 19 | #[test] 20 | fn test_foo() { 21 | assert_eq!(foo(1, 3.), -2.); 22 | } 23 | --------------------------------------------------------------------------------