use {
super::generation::generate_documentation,
crate::{
analysis::{
get_all_parameters,
impl_trait_lint::find_impl_trait_candidates,
},
core::{
Result as OurResult,
WarningEmitter,
config::Config,
constants::{
attributes::{
ALLOW_NAMED_GENERICS,
DOCUMENT_ATTR_ORDER,
DOCUMENT_EXAMPLES,
DOCUMENT_MODULE,
DOCUMENT_PARAMETERS,
DOCUMENT_RETURNS,
DOCUMENT_SIGNATURE,
DOCUMENT_TYPE_PARAMETERS,
NO_VALIDATION,
},
markers::KIND_PREFIX,
},
error_handling::ErrorCollector,
},
resolution::get_context,
support::{
attributes::{
attr_matches,
has_attribute,
},
method_utils::{
impl_has_receiver_methods,
sig_has_non_receiver_parameters,
trait_has_receiver_methods,
},
parsing::{
parse_many,
parse_non_empty,
parse_with_dispatch,
},
},
},
proc_macro2::TokenStream,
quote::quote,
syn::{
Item,
ItemMod,
TraitItem,
TypeParamBound,
parse::{
Parse,
ParseStream,
},
spanned::Spanned,
visit_mut::{
self,
VisitMut,
},
},
};
pub struct DocumentModuleInput {
pub items: Vec<Item>,
}
impl Parse for DocumentModuleInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let items = parse_many(input)?;
let items = parse_non_empty(items, "Module documentation must contain at least one item")?;
Ok(DocumentModuleInput {
items,
})
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum ValidationMode {
#[default]
On,
Off,
}
fn parse_validation_mode(attr: TokenStream) -> syn::Result<ValidationMode> {
if attr.is_empty() {
return Ok(ValidationMode::default());
}
let attr_str = attr.to_string();
match attr_str.trim() {
NO_VALIDATION => Ok(ValidationMode::Off),
_ => Err(syn::Error::new(
attr.span(),
format!("Unknown validation mode '{attr_str}'. Valid option: '{NO_VALIDATION}'"),
)),
}
}
enum ParsedInput {
ModuleWrapper(ItemMod, syn::token::Brace, Vec<Item>),
DirectItems(Vec<Item>),
}
fn try_parse_module_wrapper(item: TokenStream) -> Option<ParsedInput> {
if let Ok(mut item_mod) = syn::parse2::<ItemMod>(item) {
if let Some((brace, mod_items)) = item_mod.content.take() {
return Some(ParsedInput::ModuleWrapper(item_mod, brace, mod_items));
} else {
return None;
}
}
None
}
fn try_parse_direct_items(item: TokenStream) -> Option<ParsedInput> {
if let Ok(input) = syn::parse2::<DocumentModuleInput>(item) {
return Some(ParsedInput::DirectItems(input.items));
}
None
}
fn try_parse_const_block(item: TokenStream) -> Result<ParsedInput, syn::Error> {
let item_const = syn::parse2::<syn::ItemConst>(item)?;
if let syn::Expr::Block(expr_block) = *item_const.expr {
let items: Vec<Item> = expr_block
.block
.stmts
.into_iter()
.filter_map(|stmt| match stmt {
syn::Stmt::Item(item) => Some(item),
_ => None,
})
.collect();
Ok(ParsedInput::DirectItems(items))
} else {
Err(syn::Error::new(
item_const.span(),
format!(
"{DOCUMENT_MODULE} on a const item requires a block expression: const _: () = {{ ... }};"
),
))
}
}
fn parse_document_module_input(item: TokenStream) -> Result<ParsedInput, syn::Error> {
parse_with_dispatch(
item,
vec![
Box::new(|tokens| {
try_parse_module_wrapper(tokens).ok_or_else(|| {
syn::Error::new(proc_macro2::Span::call_site(), "Not a module wrapper")
})
}),
Box::new(|tokens| {
try_parse_direct_items(tokens).ok_or_else(|| {
syn::Error::new(proc_macro2::Span::call_site(), "Not direct items")
})
}),
Box::new(try_parse_const_block),
],
&format!(
"{DOCUMENT_MODULE} must be applied to a module or a const block (e.g., const _: () = {{ ... }})."
),
)
}
pub fn document_module_worker(
attr: TokenStream,
item: TokenStream,
) -> OurResult<TokenStream> {
let parsed_input = parse_document_module_input(item)?;
let (module_wrapper, mut items) = match parsed_input {
ParsedInput::ModuleWrapper(module, brace, items) => (Some((module, brace)), items),
ParsedInput::DirectItems(items) => (None, items),
};
let validation_mode = parse_validation_mode(attr)?;
let mut config = Config::default();
get_context(&items, &mut config)?;
apply_to_nested_modules(&mut items, get_context, &mut config)?;
let dispatch_info = crate::analysis::dispatch::analyze_dispatch_traits(&items);
config.dispatch_traits.extend(dispatch_info);
let warning_tokens: Vec<TokenStream> = if validation_mode != ValidationMode::Off {
let mut emitter = WarningEmitter::new();
validate_documentation(&items, &mut emitter);
validate_nested_modules(&items, &mut emitter);
lint_impl_trait(&items, &mut emitter);
lint_impl_trait_nested(&items, &mut emitter);
emitter.into_tokens()
} else {
Vec::new()
};
let mut doc_errors: Vec<proc_macro2::TokenStream> = Vec::new();
if let Err(e) = generate_documentation(&mut items, &config) {
doc_errors.push(e.to_compile_error());
}
if let Err(e) = apply_to_nested_modules_immut(&mut items, generate_documentation, &config) {
doc_errors.push(e.to_compile_error());
}
let items_output = if let Some((mut module, brace)) = module_wrapper {
module.content = Some((brace, items));
quote!(#module)
} else {
quote!(#(#items)*)
};
let output = if !warning_tokens.is_empty() || !doc_errors.is_empty() {
quote! {
#(#warning_tokens)*
#(#doc_errors)*
#items_output
}
} else {
items_output
};
Ok(output)
}
fn apply_to_nested_modules<F>(
items: &mut [Item],
operation: F,
config: &mut Config,
) -> syn::Result<()>
where
F: Fn(&[Item], &mut Config) -> syn::Result<()> + Copy, {
let mut errors = ErrorCollector::new();
let mut visitor = ModuleVisitor {
operation,
config,
errors: &mut errors,
};
for item in items {
visitor.visit_item_mut(item);
}
errors.finish()
}
fn apply_to_nested_modules_immut<F>(
items: &mut [Item],
operation: F,
config: &Config,
) -> syn::Result<()>
where
F: Fn(&mut [Item], &Config) -> syn::Result<()> + Copy, {
let mut errors = ErrorCollector::new();
let mut visitor = ModuleVisitorImmut {
operation,
config,
errors: &mut errors,
};
for item in items {
visitor.visit_item_mut(item);
}
errors.finish()
}
struct ModuleVisitor<'a, F>
where
F: Fn(&[Item], &mut Config) -> syn::Result<()>, {
operation: F,
config: &'a mut Config,
errors: &'a mut ErrorCollector,
}
impl<'a, F> VisitMut for ModuleVisitor<'a, F>
where
F: Fn(&[Item], &mut Config) -> syn::Result<()>,
{
fn visit_item_mod_mut(
&mut self,
module: &mut ItemMod,
) {
module.attrs.retain(|attr| !attr_matches(attr, DOCUMENT_MODULE));
if let Some((_, ref items)) = module.content {
if let Err(e) = (self.operation)(items, self.config) {
self.errors.push(e);
}
visit_mut::visit_item_mod_mut(self, module);
}
}
}
struct ModuleVisitorImmut<'a, F>
where
F: Fn(&mut [Item], &Config) -> syn::Result<()>, {
operation: F,
config: &'a Config,
errors: &'a mut ErrorCollector,
}
impl<'a, F> VisitMut for ModuleVisitorImmut<'a, F>
where
F: Fn(&mut [Item], &Config) -> syn::Result<()>,
{
fn visit_item_mod_mut(
&mut self,
module: &mut ItemMod,
) {
if let Some((_, ref mut items)) = module.content {
if let Err(e) = (self.operation)(items, self.config) {
self.errors.push(e);
}
visit_mut::visit_item_mod_mut(self, module);
}
}
}
fn validate_no_duplicate_doc_attrs(
attrs: &[syn::Attribute],
item_span: proc_macro2::Span,
item_label: &str,
warnings: &mut WarningEmitter,
) {
for name in DOCUMENT_ATTR_ORDER {
let count = attrs.iter().filter(|a| a.path().is_ident(name)).count();
if count > 1 {
warnings.warn(
item_span,
format!(
"{item_label} has `#[{name}]` applied {count} times; it may only appear once",
),
);
}
}
}
#[expect(
clippy::indexing_slicing,
clippy::unwrap_used,
reason = "Indices bounded by DOCUMENT_ATTR_ORDER"
)]
fn validate_doc_attr_order(
attrs: &[syn::Attribute],
item_span: proc_macro2::Span,
item_label: &str,
warnings: &mut WarningEmitter,
) {
let positions: Vec<Option<usize>> = DOCUMENT_ATTR_ORDER
.iter()
.map(|name| attrs.iter().position(|a| a.path().is_ident(name)))
.collect();
for i in 0 .. DOCUMENT_ATTR_ORDER.len() {
for j in (i + 1) .. DOCUMENT_ATTR_ORDER.len() {
if let (Some(pos_i), Some(pos_j)) = (positions[i], positions[j])
&& pos_i > pos_j
{
warnings.warn(
item_span,
format!(
"{item_label} has `#[{}]` before `#[{}]`, but the required order is: {}",
DOCUMENT_ATTR_ORDER[j],
DOCUMENT_ATTR_ORDER[i],
DOCUMENT_ATTR_ORDER
.iter()
.filter(|name| positions
[DOCUMENT_ATTR_ORDER.iter().position(|n| n == *name).unwrap()]
.is_some())
.copied()
.map(|n| format!("`#[{n}]`"))
.collect::<Vec<_>>()
.join(" -> "),
),
);
return;
}
}
}
}
fn validate_method_documentation_core(
attrs: &[syn::Attribute],
sig: &syn::Signature,
span: proc_macro2::Span,
warnings: &mut WarningEmitter,
) {
let method_name = &sig.ident;
let label = format!("Method `{method_name}`");
validate_no_duplicate_doc_attrs(attrs, span, &label, warnings);
validate_doc_attr_order(attrs, span, &label, warnings);
if !has_attribute(attrs, DOCUMENT_SIGNATURE) {
warnings.warn(
span,
format!("Method `{method_name}` should have #[{DOCUMENT_SIGNATURE}] attribute"),
);
}
let has_type_params = !sig.generics.params.is_empty();
let has_doc_type_params = has_attribute(attrs, DOCUMENT_TYPE_PARAMETERS);
if has_type_params && !has_doc_type_params {
let type_param_names: Vec<String> = get_all_parameters(&sig.generics);
warnings.warn(
span,
format!(
"Method `{method_name}` has type parameters <{}> but no #[{DOCUMENT_TYPE_PARAMETERS}] attribute",
type_param_names.join(", "),
),
);
}
if sig_has_non_receiver_parameters(sig) && !has_attribute(attrs, DOCUMENT_PARAMETERS) {
warnings.warn(
span,
format!(
"Method `{method_name}` has parameters but no #[{DOCUMENT_PARAMETERS}] attribute",
),
);
}
if let syn::ReturnType::Type(..) = sig.output
&& !has_attribute(attrs, DOCUMENT_RETURNS)
{
warnings.warn(
span,
format!(
"Method `{method_name}` has a return type but no #[{DOCUMENT_RETURNS}] attribute",
),
);
}
if !has_attribute(attrs, DOCUMENT_EXAMPLES) {
warnings.warn(
span,
format!(
"Method `{method_name}` should have a #[{DOCUMENT_EXAMPLES}] attribute with example code in doc comments using fenced code blocks",
),
);
}
}
fn validate_method_documentation(
method: &syn::ImplItemFn,
warnings: &mut WarningEmitter,
) {
validate_method_documentation_core(&method.attrs, &method.sig, method.span(), warnings);
}
fn validate_container_documentation(
attrs: &[syn::Attribute],
generics: &syn::Generics,
has_receiver_methods: bool,
span: proc_macro2::Span,
container_label: &str,
warnings: &mut WarningEmitter,
) {
validate_no_duplicate_doc_attrs(attrs, span, container_label, warnings);
validate_doc_attr_order(attrs, span, container_label, warnings);
let has_type_params = !generics.params.is_empty();
let has_doc_type_params = has_attribute(attrs, DOCUMENT_TYPE_PARAMETERS);
if has_type_params && !has_doc_type_params {
let type_param_names: Vec<String> = get_all_parameters(generics);
warnings.warn(
span,
format!(
"{container_label} has type parameters <{}> but no #[{DOCUMENT_TYPE_PARAMETERS}] attribute",
type_param_names.join(", "),
),
);
}
if has_receiver_methods && !has_attribute(attrs, DOCUMENT_PARAMETERS) {
warnings.warn(
span,
format!(
"{container_label} contains methods with receiver parameters but no #[{DOCUMENT_PARAMETERS}] attribute",
),
);
}
}
fn validate_impl_documentation(
item_impl: &syn::ItemImpl,
warnings: &mut WarningEmitter,
) {
validate_container_documentation(
&item_impl.attrs,
&item_impl.generics,
impl_has_receiver_methods(item_impl),
item_impl.span(),
"Impl block",
warnings,
);
for impl_item in &item_impl.items {
if let syn::ImplItem::Fn(method) = impl_item {
validate_method_documentation(method, warnings);
}
}
}
fn validate_trait_documentation(
item_trait: &syn::ItemTrait,
warnings: &mut WarningEmitter,
) {
for bound in &item_trait.supertraits {
if let TypeParamBound::Trait(trait_bound) = bound
&& let Some(segment) = trait_bound.path.segments.last()
&& segment.ident.to_string().starts_with(KIND_PREFIX)
{
warnings.warn(
segment.ident.span(),
format!(
"Trait `{}` uses raw `{}` supertrait. Use `#[kind(...)]` attribute instead",
item_trait.ident, segment.ident
),
);
}
}
let label = format!("Trait `{}`", item_trait.ident);
validate_container_documentation(
&item_trait.attrs,
&item_trait.generics,
trait_has_receiver_methods(item_trait),
item_trait.span(),
&label,
warnings,
);
for item in &item_trait.items {
if let TraitItem::Fn(method) = item {
validate_method_documentation_core(&method.attrs, &method.sig, method.span(), warnings);
}
}
}
fn validate_fn_documentation(
item_fn: &syn::ItemFn,
warnings: &mut WarningEmitter,
) {
let fn_name = &item_fn.sig.ident;
if !has_attribute(&item_fn.attrs, DOCUMENT_EXAMPLES) {
warnings.warn(
item_fn.span(),
format!(
"Function `{fn_name}` should have a #[{DOCUMENT_EXAMPLES}] attribute with example code in doc comments using fenced code blocks",
),
);
}
}
fn validate_documentation(
items: &[Item],
emitter: &mut WarningEmitter,
) {
for item in items {
match item {
Item::Impl(item_impl) => validate_impl_documentation(item_impl, emitter),
Item::Trait(item_trait) => validate_trait_documentation(item_trait, emitter),
Item::Fn(item_fn) => validate_fn_documentation(item_fn, emitter),
_ => {}
}
}
}
fn validate_nested_modules(
items: &[Item],
emitter: &mut WarningEmitter,
) {
for item in items {
if let Item::Mod(module) = item
&& let Some((_, ref nested_items)) = module.content
{
validate_documentation(nested_items, emitter);
validate_nested_modules(nested_items, emitter);
}
}
}
fn lint_sig_impl_trait(
attrs: &[syn::Attribute],
sig: &syn::Signature,
emitter: &mut WarningEmitter,
) {
if has_attribute(attrs, ALLOW_NAMED_GENERICS) {
return;
}
for candidate in find_impl_trait_candidates(sig) {
emitter.warn(
candidate.param_span,
format!(
"Type parameter `{}` could use `impl {}` instead of a named generic",
candidate.param_name, candidate.bounds_display,
),
);
}
}
fn lint_impl_trait(
items: &[Item],
emitter: &mut WarningEmitter,
) {
for item in items {
match item {
Item::Impl(item_impl) => {
if item_impl.trait_.is_none() {
for impl_item in &item_impl.items {
if let syn::ImplItem::Fn(method) = impl_item {
lint_sig_impl_trait(&method.attrs, &method.sig, emitter);
}
}
}
}
Item::Trait(item_trait) =>
for trait_item in &item_trait.items {
if let TraitItem::Fn(method) = trait_item {
lint_sig_impl_trait(&method.attrs, &method.sig, emitter);
}
},
Item::Fn(item_fn) => {
lint_sig_impl_trait(&item_fn.attrs, &item_fn.sig, emitter);
}
_ => {}
}
}
}
fn lint_impl_trait_nested(
items: &[Item],
emitter: &mut WarningEmitter,
) {
for item in items {
if let Item::Mod(module) = item
&& let Some((_, ref nested_items)) = module.content
{
lint_impl_trait(nested_items, emitter);
lint_impl_trait_nested(nested_items, emitter);
}
}
}