use {
crate::{
core::{
Error,
Result,
constants::attributes::DOCUMENT_PARAMETERS,
},
support::documentation_parameters::DocumentationParameter,
},
proc_macro2::{
Span,
TokenStream,
},
syn::{
GenericParam,
Generics,
parse::{
Parse,
ParseStream,
},
},
};
pub fn parse_many<T: Parse>(input: ParseStream) -> syn::Result<Vec<T>> {
let mut items = Vec::new();
while !input.is_empty() {
items.push(input.parse()?);
}
Ok(items)
}
pub fn parse_non_empty<T>(
items: Vec<T>,
message: &str,
) -> Result<Vec<T>> {
if items.is_empty() {
return Err(Error::validation(Span::call_site(), message));
}
Ok(items)
}
pub fn parse_with_dispatch<T>(
tokens: TokenStream,
alternatives: Vec<Box<dyn Fn(TokenStream) -> syn::Result<T>>>,
error_msg: &str,
) -> syn::Result<T> {
for attempt in alternatives {
if let Ok(result) = attempt(tokens.clone()) {
return Ok(result);
}
}
Err(syn::Error::new(Span::call_site(), error_msg))
}
pub fn parse_generics(generics: &Generics) -> Result<&Generics> {
for param in &generics.params {
if let GenericParam::Const(const_param) = param {
return Err(Error::validation(
const_param.ident.span(),
"Const generic parameters are not supported in Kind definitions",
)
.with_suggestion("Remove const parameters or use a different approach"));
}
}
Ok(generics)
}
pub fn parse_entry_count(
expected: usize,
provided: usize,
span: Span,
context: &str,
) -> Result<(usize, usize)> {
if expected != provided {
return Err(Error::Parse(syn::Error::new(
span,
format!(
"Expected exactly {expected} description arguments (one for each {context}), found {provided}. All {context}s must be documented."
),
)));
}
Ok((expected, provided))
}
pub fn parse_non_zero_count<F>(
count: usize,
span: Span,
error_fn: F,
) -> Result<usize>
where
F: FnOnce() -> String, {
if count == 0 {
return Err(Error::Parse(syn::Error::new(span, error_fn())));
}
Ok(count)
}
pub fn parse_has_documentable_items(
count: usize,
span: Span,
attr_name: &str,
item_description: &str,
) -> Result<usize> {
parse_non_zero_count(count, span, || format!("Cannot use #[{attr_name}] on {item_description}"))
}
pub fn parse_parameter_documentation_pairs(
targets: Vec<String>,
entries: Vec<DocumentationParameter>,
span: Span,
) -> Result<Vec<(String, DocumentationParameter)>> {
let expected = targets.len();
let found = entries.len();
parse_has_documentable_items(
expected,
span,
DOCUMENT_PARAMETERS,
r#"functions with no parameters to document.
Note: `self` parameters (including `&self` and `&mut self`) are not considered documentable parameters. Remove this attribute or add parameters to the function."#,
)?;
parse_entry_count(expected, found, span, "parameter")?;
Ok(targets.into_iter().zip(entries).collect())
}
#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "Tests use panicking operations for brevity and clarity")]
mod tests {
use super::*;
#[test]
fn test_parse_many() {
use syn::parse::Parser;
let input = "u32 i32 f64";
let parser = |input: ParseStream| parse_many::<syn::Type>(input);
let result = parser.parse_str(input);
assert!(result.is_ok());
let items = result.unwrap();
assert_eq!(items.len(), 3);
}
#[test]
fn test_parse_non_empty() {
let items = vec![1, 2, 3];
let result = parse_non_empty(items, "Error");
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 3);
let empty: Vec<i32> = vec![];
let result = parse_non_empty(empty, "Should not be empty");
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "Validation error: Should not be empty");
}
#[test]
fn test_parse_generics_with_const() {
use syn::parse_quote;
let generics: Generics = parse_quote!(<const N: usize>);
let result = parse_generics(&generics);
assert!(result.is_err());
}
#[test]
fn test_parse_generics_without_const() {
use syn::parse_quote;
let generics: Generics = parse_quote!(<T, U>);
let result = parse_generics(&generics);
assert!(result.is_ok());
let returned_generics = result.unwrap();
assert_eq!(returned_generics.params.len(), 2);
}
#[test]
fn test_parse_entry_count_matches() {
let result = parse_entry_count(3, 3, Span::call_site(), "field");
assert!(result.is_ok());
assert_eq!(result.unwrap(), (3, 3));
}
#[test]
fn test_parse_entry_count_mismatch() {
let result = parse_entry_count(3, 2, Span::call_site(), "field");
assert!(result.is_err());
let error = result.unwrap_err().to_string();
assert!(error.contains("Expected exactly 3"));
assert!(error.contains("found 2"));
}
#[test]
fn test_parse_has_documentable_items_ok() {
let result = parse_has_documentable_items(
1,
Span::call_site(),
"document_parameters",
"functions with no parameters",
);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
}
#[test]
fn test_parse_has_documentable_items_zero() {
let result = parse_has_documentable_items(
0,
Span::call_site(),
"document_parameters",
"functions with no parameters",
);
assert!(result.is_err());
let error = result.unwrap_err().to_string();
assert!(error.contains("Cannot use #[document_parameters]"));
assert!(error.contains("functions with no parameters"));
}
#[test]
fn test_parse_non_zero_count_ok() {
let result =
parse_non_zero_count(5, Span::call_site(), || "Should not see this".to_string());
assert!(result.is_ok());
assert_eq!(result.unwrap(), 5);
}
#[test]
fn test_parse_non_zero_count_zero() {
let result =
parse_non_zero_count(0, Span::call_site(), || "Count cannot be zero".to_string());
assert!(result.is_err());
let error = result.unwrap_err().to_string();
assert!(error.contains("Count cannot be zero"));
}
}