clap_noun_verb_macros/lib.rs
1// Copyright (c) 2024 Sean Chatman
2// SPDX-License-Identifier: MIT OR Apache-2.0
3#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
4#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used, clippy::panic))]
5
6//! Procedural macros for clap-noun-verb
7//!
8//! This crate provides attribute macros `#[noun]` and `#[verb]` for
9//! declarative CLI command registration.
10//!
11//! # I/O Support (v4.0)
12//!
13//! The #[verb] macro now auto-detects clio::Input and clio::Output types
14//! in function parameters and automatically wires them with appropriate
15//! clap configuration.
16//!
17//! # Compile-Time Error Proofing (Poka-Yoke)
18//!
19//! The macro system includes advanced compile-time validation:
20//! - Gap 1: Forgotten #[verb] detection
21//! - Gap 2: Duplicate verb detection
22//! - Gap 3: Return type must implement Serialize
23//! - Gap 4: Enhanced attribute syntax validation
24
25mod io_detection;
26mod rdf_generation;
27mod telemetry_validation;
28mod validation;
29
30// Frontier: Meta-Framework for self-introspection
31// Note: proc-macro crates cannot export modules, only proc_macro functions
32mod meta_framework;
33
34// Frontier: Fractal Pattern macros
35mod macros;
36
37use proc_macro::TokenStream;
38use quote::quote;
39use syn::{parse::Parser, parse_macro_input, ItemFn};
40
41// Note: #[arg(...)] attributes on function parameters cannot be a real proc_macro_attribute
42// because Rust doesn't allow proc_macro_attribute on parameters - only on items.
43// The #[verb] macro parses #[arg(...)] attributes directly from pat_type.attrs.
44//
45// This proc_macro_attribute exists solely so Rust accepts #[arg] on parameters
46// without triggering "unknown attribute" errors. When applied to an item (function,
47// struct, etc.), it emits a compile error — #[arg] should ONLY appear on parameters
48// within #[verb] functions, where it is parsed as raw tokens by the verb macro.
49
50/// Attribute macro for #[arg(...)] on function parameters within #[verb] functions.
51///
52/// **DO NOT apply this to items (functions, structs, etc.)** — it will produce a
53/// compile error. Use it only on function parameters inside a `#[verb]` function.
54///
55/// # Correct usage (on parameters)
56///
57/// ```rust,ignore
58/// #[verb("set")]
59/// fn set_config(
60/// #[arg(env = "SERVER_PORT", default_value = "8080")]
61/// port: u16,
62/// ) -> Result<()> {}
63/// ```
64///
65/// # Incorrect usage (on item) — will error
66///
67/// ```rust,ignore
68/// #[arg(something)] // ERROR: #[arg] cannot be applied to items
69/// fn my_function() {}
70/// ```
71#[proc_macro_attribute]
72pub fn arg(_args: TokenStream, _input: TokenStream) -> TokenStream {
73 // This macro fires when #[arg] is applied to an item (function, struct, etc.).
74 // On parameters inside #[verb] functions, it never executes — #[verb] reads
75 // the raw attribute tokens directly. So if we're here, it's misuse.
76 syn::Error::new(
77 proc_macro2::Span::call_site(),
78 "#[arg(...)] cannot be applied to items directly. \
79 It should only be used on function parameters within a #[verb] function.\n\
80 \n\
81 Correct:\n #[verb(\"set\")]\n fn set(#[arg(default_value = \"80\")] port: u16) {{}}\n\
82 \n\
83 For argument metadata, you can also use doc comment tags:\n /// [default: 80]\n /// [env: PORT]\n port: u16"
84 )
85 .to_compile_error()
86 .into()
87}
88
89/// Attribute macro for generating self-introspecting meta-framework capabilities
90///
91/// Generates RDF introspection methods, optimization queries, capability discovery,
92/// and type-safe wrappers for structs.
93///
94/// # Example
95///
96/// ```rust,ignore
97/// use clap_noun_verb_macros::meta_aware;
98///
99/// #[meta_aware]
100/// struct AgentCapabilities {
101/// name: String,
102/// max_concurrency: usize,
103/// }
104///
105/// let caps = AgentCapabilities { name: "worker".to_string(), max_concurrency: 10 };
106/// let rdf = caps.introspect_capabilities(); // RDF triples
107/// let opts = caps.query_optimizations(); // Optimization hints
108/// ```
109#[proc_macro_attribute]
110pub fn meta_aware(_args: TokenStream, input: TokenStream) -> TokenStream {
111 let input_parsed = parse_macro_input!(input as syn::DeriveInput);
112
113 match meta_framework::generate_meta_aware(input_parsed) {
114 Ok(tokens) => tokens.into(),
115 Err(e) => e.to_compile_error().into(),
116 }
117}
118/// Declare a telemetry span for compile-time validation
119///
120/// This macro creates a span constant and registers it in the distributed slice
121/// for compile-time validation. If the span is never used in a `span!` macro,
122/// compilation will fail.
123///
124/// # Example
125///
126/// ```rust,ignore
127/// use clap_noun_verb_macros::declare_span;
128///
129/// // Declare span
130/// declare_span!(PROCESS_REQUEST, "process_request");
131///
132/// // Use it (required or compilation fails)
133/// fn process() {
134/// span!(PROCESS_REQUEST, {
135/// // ... work ...
136/// })
137/// }
138/// ```
139#[proc_macro]
140pub fn declare_span(input: TokenStream) -> TokenStream {
141 let input = proc_macro2::TokenStream::from(input);
142
143 // Parse: IDENT, "name"
144 let parser = syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated;
145 let args = match Parser::parse2(parser, input) {
146 Ok(args) => args,
147 Err(e) => return e.to_compile_error().into(),
148 };
149
150 if args.len() != 2 {
151 return syn::Error::new_spanned(
152 quote::quote! { #args },
153 "declare_span! requires exactly 2 arguments: IDENT, \"name\"",
154 )
155 .to_compile_error()
156 .into();
157 }
158
159 let ident = match &args[0] {
160 syn::Expr::Path(path) => {
161 if let Some(ident) = path.path.get_ident() {
162 ident.clone()
163 } else {
164 return syn::Error::new_spanned(
165 &args[0],
166 "First argument must be an identifier (e.g., PROCESS_REQUEST)",
167 )
168 .to_compile_error()
169 .into();
170 }
171 }
172 _ => {
173 return syn::Error::new_spanned(
174 &args[0],
175 "First argument must be an identifier (e.g., PROCESS_REQUEST)",
176 )
177 .to_compile_error()
178 .into()
179 }
180 };
181
182 let name = match &args[1] {
183 syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) => s.value(),
184 _ => {
185 return syn::Error::new_spanned(
186 &args[1],
187 "Second argument must be a string literal (e.g., \"process_request\")",
188 )
189 .to_compile_error()
190 .into()
191 }
192 };
193
194 let output = telemetry_validation::generate_span_declaration(&ident, &name);
195 output.into()
196}
197
198/// Instrument a code block with a telemetry span
199///
200/// This macro wraps a block of code with span instrumentation and registers
201/// usage for compile-time validation.
202///
203/// # Example
204///
205/// ```rust,ignore
206/// use clap_noun_verb_macros::{declare_span, span};
207///
208/// declare_span!(PROCESS_DATA, "process_data");
209///
210/// fn process() -> Result<Data> {
211/// span!(PROCESS_DATA, {
212/// // Work happens here
213/// Ok(Data::new())
214/// })
215/// }
216/// ```
217#[proc_macro]
218pub fn span(input: TokenStream) -> TokenStream {
219 let input = proc_macro2::TokenStream::from(input);
220
221 // Parse: IDENT, { block }
222 let parser = syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated;
223 let args = match Parser::parse2(parser, input.clone()) {
224 Ok(args) => args,
225 Err(e) => return e.to_compile_error().into(),
226 };
227
228 if args.len() != 2 {
229 return syn::Error::new_spanned(
230 quote::quote! { #input },
231 "span! requires exactly 2 arguments: SPAN_CONST, { block }",
232 )
233 .to_compile_error()
234 .into();
235 }
236
237 let span_ident = match &args[0] {
238 syn::Expr::Path(path) => {
239 if let Some(ident) = path.path.get_ident() {
240 ident.clone()
241 } else {
242 return syn::Error::new_spanned(
243 &args[0],
244 "First argument must be a span constant (e.g., PROCESS_REQUEST)",
245 )
246 .to_compile_error()
247 .into();
248 }
249 }
250 _ => {
251 return syn::Error::new_spanned(
252 &args[0],
253 "First argument must be a span constant (e.g., PROCESS_REQUEST)",
254 )
255 .to_compile_error()
256 .into()
257 }
258 };
259
260 let block = &args[1];
261
262 // Register usage (telemetry removed — no-op)
263 let _usage = &span_ident;
264
265 // Generate instrumented code
266 let expanded = quote::quote! {
267 {
268 // Execute block
269 let _result = #block;
270
271 _result
272 }
273 };
274
275 expanded.into()
276}
277
278/// Attribute macro for registering a noun command
279///
280/// Usage:
281/// ```rust,ignore
282/// #[noun("services", "Manage services")]
283/// fn my_function() {}
284/// ```
285#[proc_macro_attribute]
286pub fn noun(_args: TokenStream, input: TokenStream) -> TokenStream {
287 let input_fn = parse_macro_input!(input as ItemFn);
288
289 // DEPRECATED: #[noun] is no longer needed. Nouns are auto-detected from:
290 // - Filename (e.g., papers.rs -> noun "papers")
291 // - Module doc comments (//! ...) for the noun description
292 //
293 // This macro is now a no-op. Remove #[noun(...)] from your code.
294 // The noun name and about arguments are ignored.
295
296 // Remove #[noun] attribute from output (it's been processed)
297 let mut output_fn = input_fn.clone();
298 output_fn.attrs.retain(|attr| {
299 !attr.path().is_ident("noun")
300 && attr.path().segments.last().map(|seg| seg.ident != "noun").unwrap_or(true)
301 });
302
303 // Emit deprecation warning
304 let expanded = quote! {
305 #[deprecated(
306 since = "5.6.0",
307 note = "#[noun] is no longer needed — nouns are auto-detected from filename and module doc comments (//!). Remove this attribute."
308 )]
309 #output_fn
310 };
311
312 expanded.into()
313}
314
315/// Attribute macro for registering a verb command
316///
317/// Usage:
318/// ```rust,ignore
319/// #[verb("status")]
320/// fn show_status() -> Result<Status> {}
321/// ```
322///
323/// # Compile-Time Validation
324///
325/// This macro performs extensive compile-time validation:
326/// - Return type must exist and implement Serialize
327/// - Attribute syntax must be correct (helpful error messages)
328/// - Duplicate verb registration detection
329/// - Parameter attributes (#[arg]) must be valid
330#[proc_macro_attribute]
331pub fn verb(args: TokenStream, input: TokenStream) -> TokenStream {
332 let input_fn = parse_macro_input!(input as ItemFn);
333 let args_tokens = proc_macro2::TokenStream::from(args.clone());
334
335 // GAP 3: Validate return type implements Serialize
336 if let Err(e) = validation::validate_return_type(&input_fn.sig.output, &input_fn.sig.ident) {
337 return e.to_compile_error().into();
338 }
339
340 // GAP 4: Validate attribute syntax with helpful errors
341 if let Err(e) = validation::validate_verb_attribute_syntax(&args_tokens, &input_fn) {
342 return e.to_compile_error().into();
343 }
344
345 // 🛡️ POKA-YOKE FM-1.1: Validate verb function complexity (CLI layer purity)
346 // Prevents business logic from leaking into verb functions
347 if let Err(e) = validation::validate_verb_complexity(&input_fn) {
348 return e.to_compile_error().into();
349 }
350
351 // 🛡️ POKA-YOKE FM-1.2: Validate no CLI types in parameters (domain independence)
352 // Prevents domain functions from depending on CLI types
353 if let Err(e) = validation::validate_no_cli_types_in_params(&input_fn.sig) {
354 return e.to_compile_error().into();
355 }
356
357 // Validate #[arg] attributes on parameters
358 for input in &input_fn.sig.inputs {
359 if let syn::FnArg::Typed(pat_type) = input {
360 if let Err(e) = validation::validate_arg_attribute_syntax(&pat_type.attrs) {
361 return e.to_compile_error().into();
362 }
363 }
364 }
365
366 // Parse verb name from args
367 let parser = syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated;
368 let args_vec: syn::punctuated::Punctuated<_, _> =
369 match Parser::parse2(parser, args_tokens.clone()) {
370 Ok(args) => args,
371 Err(_) => {
372 // If parsing fails, extract verb name from function name
373 let verb_name = extract_verb_name_from_fn_name(&input_fn);
374 let docstring = extract_docstring(&input_fn);
375 let arg_relationships = parse_argument_descriptions_with_relationships(&docstring);
376 return generate_verb_registration(
377 input_fn,
378 verb_name,
379 None,
380 None,
381 arg_relationships,
382 );
383 }
384 };
385
386 let verb_name = if args_vec.is_empty() {
387 extract_verb_name_from_fn_name(&input_fn)
388 } else {
389 match &args_vec[0] {
390 syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) => s.value(),
391 _ => {
392 return syn::Error::new_spanned(
393 &args_vec[0],
394 "First argument must be a string literal",
395 )
396 .to_compile_error()
397 .into()
398 }
399 }
400 };
401
402 // Extract noun name if provided as second arg, or auto-detect from #[noun] attribute or file context
403 let noun_name = if args_vec.len() > 1 {
404 match &args_vec[1] {
405 syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) => Some(s.value()),
406 _ => None,
407 }
408 } else {
409 // Try to auto-detect noun name:
410 // 1. First check for #[noun] attribute on same function
411 // 2. Then try to infer from filename using file!() macro
412 extract_noun_name_from_attributes(&input_fn)
413 .or_else(|| extract_noun_name_from_file_context(&input_fn))
414 };
415
416 // If verb name was auto-inferred and noun name was auto-detected,
417 // strip the noun name from the verb name if it appears in the function name
418 // Example: show_collector_status() with noun="collector" -> verb="status" (not "collector_status")
419 let verb_name = if args_vec.is_empty() {
420 if let Some(noun) = noun_name.as_ref() {
421 // Check if verb_name starts with noun_name (e.g., "collector_status" starts with "collector")
422 if verb_name.starts_with(noun) && verb_name.len() > noun.len() {
423 // Check if there's a separator (underscore) after the noun
424 if verb_name.as_bytes()[noun.len()] == b'_' {
425 // Strip noun_ prefix (e.g., "collector_status" -> "status")
426 verb_name[noun.len() + 1..].to_string()
427 } else {
428 verb_name
429 }
430 } else {
431 verb_name
432 }
433 } else {
434 verb_name
435 }
436 } else {
437 verb_name
438 };
439
440 // Extract docstring for help text
441 let docstring = extract_docstring(&input_fn);
442
443 // Parse argument descriptions with relationship metadata from docstring
444 let arg_relationships = parse_argument_descriptions_with_relationships(&docstring);
445
446 // Clean docstring for about - remove # Arguments section and relationship tags
447 let clean_about = clean_docstring_for_about(&docstring);
448 generate_verb_registration(input_fn, verb_name, noun_name, Some(clean_about), arg_relationships)
449}
450
451/// Extract verb name from function name (remove common prefixes)
452fn extract_verb_name_from_fn_name(input_fn: &ItemFn) -> String {
453 let fn_name = input_fn.sig.ident.to_string();
454
455 // List of prefixes to strip in order of priority
456 let prefixes = [
457 "show_", "get_", "list_", "create_", "delete_", "update_", "fetch_", "display_", "print_",
458 "run_", "execute_", "check_", "verify_", "start_", "stop_", "restart_", "add_", "remove_",
459 "set_", "unset_",
460 ];
461
462 // Try each prefix
463 for prefix in &prefixes {
464 if let Some(stripped) = fn_name.strip_prefix(prefix) {
465 return stripped.to_string();
466 }
467 }
468
469 // If no prefix matches, return the function name as-is
470 fn_name
471}
472
473/// Extract noun name from filename using file!() macro
474///
475/// Core team approach: Infer noun name from source filename.
476/// Example: `services.rs` -> `"services"`, `user_management.rs` -> `"user_management"`
477fn extract_noun_name_from_file_context(_input_fn: &ItemFn) -> Option<String> {
478 // Core team approach: Infer noun name from filename
479 // We can't access filename at compile time in stable Rust, so we'll extract it
480 // at runtime using file!() macro in the generated code.
481 // Return None here to signal that we should extract from file!() at runtime.
482 None // Will be extracted at runtime using file!() in generated code
483}
484
485// Note: Module doc extraction from `//!` comments is complex in proc macros
486// because we need to parse the entire file. For now, we use function doc as fallback.
487// Future enhancement: Use span information or file parsing to extract module docs.
488
489/// Extract noun name from attributes on same function
490///
491/// Core team approach: Check for helper doc comment first (emitted by #[noun] when #[verb] is present),
492/// then fall back to original #[noun] attribute. This works regardless of macro processing order.
493///
494/// The helper doc comment `#[doc = "__noun_name_internal:name"]` is emitted by #[noun] when it detects
495/// #[verb] is also present, ensuring reliable noun name detection without Rust trying to process it.
496fn extract_noun_name_from_attributes(input_fn: &ItemFn) -> Option<String> {
497 // First, check for helper doc comment emitted by #[noun]
498 // Format: #[doc = "__noun_name_internal:name"]
499 for attr in &input_fn.attrs {
500 if attr.path().is_ident("doc") {
501 if let syn::Meta::NameValue(nv) = &attr.meta {
502 if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) = &nv.value {
503 let doc_value = s.value();
504 if let Some(noun_name) = doc_value.strip_prefix("__noun_name_internal:") {
505 return Some(noun_name.to_string());
506 }
507 }
508 }
509 }
510 }
511
512 // Fallback: Check for original #[noun] attribute (when #[verb] processes first)
513 for attr in &input_fn.attrs {
514 let is_noun_attr = {
515 if attr.path().is_ident("noun") {
516 true
517 } else {
518 let segments: Vec<_> = attr.path().segments.iter().collect();
519 segments.last().map(|seg| seg.ident == "noun").unwrap_or(false)
520 }
521 };
522
523 if is_noun_attr {
524 if let syn::Meta::List(meta_list) = &attr.meta {
525 let parser =
526 syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated;
527 if let Ok(args_vec) = parser.parse2(meta_list.tokens.clone()) {
528 if !args_vec.is_empty() {
529 if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) =
530 &args_vec[0]
531 {
532 return Some(s.value());
533 }
534 }
535 }
536 }
537 }
538 }
539
540 None
541}
542
543/// Extract docstring from function attributes
544///
545/// Doc comments in syn are stored as Meta::List with "doc" as the path.
546/// Each doc comment line is a separate attribute.
547fn extract_docstring(input_fn: &ItemFn) -> String {
548 input_fn
549 .attrs
550 .iter()
551 .filter_map(|attr| {
552 if attr.path().is_ident("doc") {
553 // Doc comments in syn 2.0 are stored as Meta::NameValue
554 // Format: #[doc = "text"]
555 let meta = &attr.meta;
556 match meta {
557 syn::Meta::NameValue(nv) => {
558 if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) =
559 &nv.value
560 {
561 Some(s.value().trim().to_string())
562 } else {
563 None
564 }
565 }
566 // Some doc comments might be in List format
567 syn::Meta::List(list) => {
568 // Extract tokens from list
569 let tokens = list.tokens.to_string();
570 // Remove quotes and extra formatting
571 Some(tokens.trim_matches('"').trim().to_string())
572 }
573 _ => None,
574 }
575 } else {
576 None
577 }
578 })
579 .collect::<Vec<_>>()
580 .join("\n")
581 .trim()
582 .to_string()
583}
584
585/// Argument relationship metadata extracted from doc comments
586///
587/// Typer-like syntax for relationships in doc comments:
588/// - `[group: name]` - Argument belongs to exclusive group "name"
589/// - `[requires: arg]` - Argument requires "arg" to be present
590/// - `[conflicts: arg]` - Argument conflicts with "arg"
591///
592/// Example:
593/// ```text
594/// # Arguments
595/// * `json` - Export as JSON [group: format]
596/// * `yaml` - Export as YAML [group: format]
597/// * `filename` - Output filename [requires: format]
598/// * `raw` - Raw output mode [conflicts: format]
599/// * `config` - Config file [env: APP_CONFIG] [default: config.toml]
600/// * `verbose` - Verbose output [hide]
601/// * `format` - Output format [value_hint: file_path]
602/// * `debug` - Debug mode [global]
603/// * `force` - Force operation [exclusive]
604/// * `output` - Output options [help_heading: Output]
605/// ```
606#[derive(Default, Clone)]
607struct DocArgRelationships {
608 /// Group name (for exclusive groups)
609 group: Option<String>,
610 /// Arguments this one requires
611 requires: Vec<String>,
612 /// Arguments this one conflicts with
613 conflicts_with: Vec<String>,
614 /// Environment variable to read value from
615 env: Option<String>,
616 /// Whether this argument should be hidden from help
617 hide: bool,
618 /// Default value for the argument
619 default_value: Option<String>,
620 /// Value hint for shell completion (file_path, dir_path, url, etc.)
621 value_hint: Option<String>,
622 /// Whether this argument is global (propagates to subcommands)
623 global: bool,
624 /// Whether this argument is exclusive (cannot be used with any other args)
625 exclusive: bool,
626 /// Help heading to group this argument under
627 help_heading: Option<String>,
628 /// Clean description without relationship tags
629 description: String,
630}
631
632/// Remove markdown code fences from text
633///
634/// Removes triple-backtick code blocks (```rust, ```bash, ``` etc.) while preserving
635/// the text content. This prevents code examples from appearing literally in help output.
636///
637/// Examples:
638/// - Input: "Example:\n```rust\nlet x = 5;\n```\nDone."
639/// - Output: "Example:\nlet x = 5;\nDone."
640fn strip_markdown_code_fences(text: &str) -> String {
641 let mut result = String::new();
642 let lines: Vec<&str> = text.lines().collect();
643 let mut i = 0;
644
645 while i < lines.len() {
646 let line = lines[i];
647 let trimmed = line.trim();
648
649 // Check if this line starts a code fence
650 if trimmed.starts_with("```") {
651 // Skip the opening fence line
652 i += 1;
653
654 // Skip lines until we find the closing fence
655 while i < lines.len() {
656 let closing_line = lines[i].trim();
657 if closing_line.starts_with("```") {
658 // Found closing fence, skip it and move past
659 i += 1;
660 break;
661 } else {
662 // Include content lines (but only if they're meaningful)
663 let content = lines[i];
664 if !content.trim().is_empty() {
665 if !result.is_empty() && !result.ends_with('\n') {
666 result.push('\n');
667 }
668 result.push_str(content);
669 }
670 i += 1;
671 }
672 }
673 } else {
674 // Regular line, include it
675 if !result.is_empty() && !result.ends_with('\n') {
676 result.push('\n');
677 }
678 result.push_str(line);
679 i += 1;
680 }
681 }
682
683 result.trim().to_string()
684}
685
686/// Clean docstring for command about text
687///
688/// Removes the `# Arguments` section entirely (clap generates its own help for arguments)
689/// and strips any relationship tags from the remaining text.
690fn clean_docstring_for_about(docstring: &str) -> String {
691 let mut result_lines = Vec::new();
692 let mut in_arguments_section = false;
693
694 for line in docstring.lines() {
695 let trimmed = line.trim();
696
697 // Check if we've entered the Arguments section
698 if trimmed == "# Arguments" || trimmed.starts_with("# Arguments") {
699 in_arguments_section = true;
700 continue;
701 }
702
703 // If we hit another section heading, we're done with Arguments
704 if in_arguments_section && trimmed.starts_with('#') && !trimmed.starts_with("# Arguments") {
705 in_arguments_section = false;
706 }
707
708 // Skip lines in the Arguments section
709 if in_arguments_section {
710 continue;
711 }
712
713 // Strip any relationship tags from the line
714 let clean_line = strip_relationship_tags(line);
715 if !clean_line.is_empty() || line.is_empty() {
716 result_lines.push(clean_line);
717 }
718 }
719
720 // Trim trailing empty lines
721 while result_lines.last().map(|s| s.trim().is_empty()).unwrap_or(false) {
722 result_lines.pop();
723 }
724
725 let joined = result_lines.join("\n").trim().to_string();
726
727 // Remove markdown code fences from the final result
728 strip_markdown_code_fences(&joined)
729}
730
731/// Strip relationship tags from a line of text
732///
733/// Removes [group: xxx], [requires: xxx], [conflicts: xxx], [env: xxx], [hide],
734/// [default: xxx], [value_hint: xxx], [global], [exclusive], [help_heading: xxx] tags.
735fn strip_relationship_tags(text: &str) -> String {
736 let mut result = String::new();
737 let mut remaining = text;
738
739 while !remaining.is_empty() {
740 if let Some(tag_start) = remaining.find('[') {
741 // Add text before the tag
742 result.push_str(&remaining[..tag_start]);
743
744 // Find the end of the tag
745 if let Some(tag_end) = remaining[tag_start..].find(']') {
746 let tag_content = &remaining[tag_start + 1..tag_start + tag_end];
747
748 // Check if this is a relationship tag we should strip
749 let is_relationship_tag = if let Some(colon_pos) = tag_content.find(':') {
750 let key = tag_content[..colon_pos].trim().to_lowercase();
751 matches!(
752 key.as_str(),
753 "group"
754 | "requires"
755 | "require"
756 | "conflicts"
757 | "conflicts_with"
758 | "conflict"
759 | "env"
760 | "default"
761 | "value_hint"
762 | "help_heading"
763 )
764 } else {
765 // Tags without colon
766 let key = tag_content.trim().to_lowercase();
767 matches!(key.as_str(), "hide" | "global" | "exclusive")
768 };
769
770 if !is_relationship_tag {
771 // Keep unknown tags
772 result.push('[');
773 result.push_str(tag_content);
774 result.push(']');
775 }
776
777 // Move past the tag
778 remaining = &remaining[tag_start + tag_end + 1..];
779 } else {
780 // No closing bracket, add rest as-is
781 result.push_str(remaining);
782 break;
783 }
784 } else {
785 // No more tags, add the rest
786 result.push_str(remaining);
787 break;
788 }
789 }
790
791 result
792}
793
794/// Parse relationship metadata from a description string
795///
796/// Extracts all Typer-like tags from argument descriptions:
797/// - `[group: name]` - Argument belongs to exclusive group
798/// - `[requires: arg]` - Argument requires another argument
799/// - `[conflicts: arg]` - Argument conflicts with another argument
800/// - `[env: VAR]` - Read value from environment variable
801/// - `[hide]` - Hide this argument from help
802/// - `[default: value]` - Default value for the argument
803/// - `[value_hint: type]` - Value hint for shell completion
804/// - `[global]` - Propagate to subcommands
805/// - `[exclusive]` - Cannot be used with any other args
806/// - `[help_heading: name]` - Group under help heading
807fn parse_doc_relationships(description: &str) -> DocArgRelationships {
808 let mut result = DocArgRelationships::default();
809 let mut clean_parts = Vec::new();
810
811 // Parse description, extracting relationship tags
812 let mut remaining = description;
813
814 while !remaining.is_empty() {
815 // Look for a tag
816 if let Some(tag_start) = remaining.find('[') {
817 // Add text before the tag
818 let before = remaining[..tag_start].trim();
819 if !before.is_empty() {
820 clean_parts.push(before.to_string());
821 }
822
823 // Find the end of the tag
824 if let Some(tag_end) = remaining[tag_start..].find(']') {
825 let tag_content = &remaining[tag_start + 1..tag_start + tag_end];
826
827 // Parse tag content: "key: value" or "key" (for boolean tags)
828 if let Some(colon_pos) = tag_content.find(':') {
829 let key = tag_content[..colon_pos].trim().to_lowercase();
830 let value = tag_content[colon_pos + 1..].trim().to_string();
831
832 match key.as_str() {
833 "group" => result.group = Some(value),
834 "requires" | "require" => result.requires.push(value),
835 "conflicts" | "conflicts_with" | "conflict" => {
836 result.conflicts_with.push(value)
837 }
838 "env" => result.env = Some(value),
839 "default" | "default_value" => result.default_value = Some(value),
840 "value_hint" | "hint" => result.value_hint = Some(value),
841 "help_heading" | "heading" => result.help_heading = Some(value),
842 _ => {
843 // Unknown tag, keep it in the description
844 clean_parts.push(format!("[{}]", tag_content));
845 }
846 }
847 } else {
848 // Boolean tags without colon (e.g., [hide], [global], [exclusive])
849 let key = tag_content.trim().to_lowercase();
850 match key.as_str() {
851 "hide" | "hidden" => result.hide = true,
852 "global" => result.global = true,
853 "exclusive" => result.exclusive = true,
854 _ => {
855 // Unknown tag, keep it in the description
856 clean_parts.push(format!("[{}]", tag_content));
857 }
858 }
859 }
860
861 // Move past the tag
862 remaining = &remaining[tag_start + tag_end + 1..];
863 } else {
864 // No closing bracket, add rest as-is
865 clean_parts.push(remaining.to_string());
866 break;
867 }
868 } else {
869 // No more tags, add the rest
870 let rest = remaining.trim();
871 if !rest.is_empty() {
872 clean_parts.push(rest.to_string());
873 }
874 break;
875 }
876 }
877
878 let description = clean_parts.join(" ").trim().to_string();
879
880 // Remove markdown code fences from the description
881 result.description = strip_markdown_code_fences(&description);
882 result
883}
884
885/// Parse argument descriptions WITH relationship metadata from docstring
886///
887/// Similar to parse_argument_descriptions but also extracts Typer-like
888/// relationship tags from the description text.
889fn parse_argument_descriptions_with_relationships(
890 docstring: &str,
891) -> std::collections::HashMap<String, DocArgRelationships> {
892 let mut results = std::collections::HashMap::new();
893
894 // Split docstring into lines
895 let lines: Vec<&str> = docstring.lines().collect();
896 let mut in_arguments_section = false;
897
898 for line in lines {
899 let trimmed = line.trim();
900
901 // Check if we've entered the Arguments section
902 if trimmed == "# Arguments" || trimmed.starts_with("# Arguments") {
903 in_arguments_section = true;
904 continue;
905 }
906
907 // If we hit another section heading, stop parsing
908 if in_arguments_section && trimmed.starts_with('#') && !trimmed.starts_with("# Arguments") {
909 break;
910 }
911
912 // Parse argument description line
913 if in_arguments_section && trimmed.starts_with('*') {
914 let rest = trimmed[1..].trim();
915
916 if let Some(dash_pos) = rest.find('-') {
917 let before_dash = rest[..dash_pos].trim();
918 let description = rest[dash_pos + 1..].trim();
919
920 // Extract argument name (remove backticks and asterisks)
921 let arg_name =
922 before_dash.trim_start_matches('*').trim().trim_matches('`').trim().to_string();
923
924 if !arg_name.is_empty() && !description.is_empty() {
925 // Parse relationships from description
926 let relationships = parse_doc_relationships(description);
927 results.insert(arg_name, relationships);
928 }
929 }
930 }
931 }
932
933 results
934}
935
936/// Generate verb registration code with full type inference
937fn generate_verb_registration(
938 input_fn: ItemFn,
939 verb_name: String,
940 noun_name: Option<String>,
941 about: Option<String>,
942 arg_relationships: std::collections::HashMap<String, DocArgRelationships>,
943) -> TokenStream {
944 let fn_name = &input_fn.sig.ident;
945 let wrapper_name = quote::format_ident!("__{}_wrapper", fn_name);
946 let init_fn_name = quote::format_ident!("__init_{}", fn_name);
947
948 // Analyze function signature for arguments
949 let mut arg_extractions = Vec::new();
950 let mut arg_calls = Vec::new();
951
952 for input in &input_fn.sig.inputs {
953 if let syn::FnArg::Typed(pat_type) = input {
954 let arg_name = match &*pat_type.pat {
955 syn::Pat::Ident(ident) => &ident.ident,
956 _ => continue,
957 };
958
959 let arg_name_str = arg_name.to_string();
960
961 // Determine if optional (Option<T>) or required
962 let is_option = is_option_type(&pat_type.ty);
963 let inner_type = extract_inner_type(&pat_type.ty);
964 let is_flag = is_bool_type(&pat_type.ty);
965 let is_usize_type = if let syn::Type::Path(type_path) = &*pat_type.ty {
966 type_path.path.segments.last().map(|s| s.ident == "usize").unwrap_or(false)
967 } else {
968 false
969 };
970 let is_vec = is_vec_type(&pat_type.ty);
971 let vec_inner_type =
972 if is_vec { extract_inner_type(&pat_type.ty) } else { (*pat_type.ty).clone() };
973
974 // Parse arg config to check for Count action
975 let arg_config = parse_arg_attributes(&pat_type.attrs);
976 let is_count_action = if let Some(config) = &arg_config {
977 config.action.as_ref().map(|a| a == "count").unwrap_or(false)
978 || (is_usize_type && !is_option)
979 } else {
980 is_usize_type && !is_option
981 };
982
983 let has_set_false_action = if let Some(config) = &arg_config {
984 config.action.as_ref().map(|a| a == "set_false").unwrap_or(false)
985 } else {
986 false
987 };
988
989 if is_count_action {
990 // Count action - extract count from __handler_input
991 arg_extractions.push(quote! {
992 let #arg_name: usize = __handler_input.args.get(#arg_name_str)
993 .and_then(|v| v.parse::<usize>().ok())
994 .unwrap_or(0);
995 });
996 arg_calls.push(quote! { #arg_name });
997 } else if is_flag {
998 // Boolean flags — the registry stores SetTrue flags in `args`
999 // (always as "true" when present), never in `opts`. Read from
1000 // `args` first, fall back to `opts` for compatibility.
1001 let default_val = if has_set_false_action {
1002 quote! { true }
1003 } else {
1004 quote! { false }
1005 };
1006 arg_extractions.push(quote! {
1007 let #arg_name = __handler_input.args.get(#arg_name_str)
1008 .or_else(|| __handler_input.opts.get(#arg_name_str))
1009 .map(|v| v.parse::<bool>().unwrap_or(#default_val))
1010 .unwrap_or(#default_val);
1011 });
1012 arg_calls.push(quote! { #arg_name });
1013 } else if is_vec {
1014 // Vec<T> types - extract from __handler_input.args as comma-separated string, then parse
1015 // The registry extracts multiple values and joins them
1016 arg_extractions.push(quote! {
1017 let #arg_name: #pat_type.ty = if let Some(value_str) = __handler_input.args.get(#arg_name_str) {
1018 // Parse comma-separated values
1019 value_str.split(',')
1020 .map(|s| s.trim().parse::<#vec_inner_type>())
1021 .collect::<Result<Vec<_>, _>>()
1022 .map_err(|_| ::clap_noun_verb::error::NounVerbError::argument_error(
1023 format!("Invalid value for argument '{}'", #arg_name_str)
1024 ))?
1025 } else {
1026 Vec::new()
1027 };
1028 });
1029 arg_calls.push(quote! { #arg_name });
1030 } else if is_option {
1031 // Optional arguments
1032 arg_extractions.push(quote! {
1033 let #arg_name = __handler_input.args.get(#arg_name_str)
1034 .and_then(|v| v.parse::<#inner_type>().ok());
1035 });
1036 arg_calls.push(quote! { #arg_name });
1037 } else {
1038 // Required arguments
1039 arg_extractions.push(quote! {
1040 let #arg_name = __handler_input.args.get(#arg_name_str)
1041 .ok_or_else(|| ::clap_noun_verb::error::NounVerbError::missing_argument(#arg_name_str))?
1042 .parse::<#inner_type>()
1043 .map_err(|_| ::clap_noun_verb::error::NounVerbError::argument_error(
1044 format!("Invalid value for argument '{}'", #arg_name_str)
1045 ))?;
1046 });
1047 arg_calls.push(quote! { #arg_name });
1048 }
1049 }
1050 }
1051
1052 // Generate argument metadata for registration
1053 let mut arg_metadata = Vec::new();
1054 for input in &input_fn.sig.inputs {
1055 if let syn::FnArg::Typed(pat_type) = input {
1056 let arg_name = match &*pat_type.pat {
1057 syn::Pat::Ident(ident) => ident.ident.to_string(),
1058 _ => continue,
1059 };
1060
1061 let is_option = is_option_type(&pat_type.ty);
1062 let is_flag = is_bool_type(&pat_type.ty);
1063
1064 // Check if it's usize (can be used with Count action for flags like -v, -vv, -vvv)
1065 let is_usize_type = if let syn::Type::Path(type_path) = &*pat_type.ty {
1066 type_path.path.segments.last().map(|s| s.ident == "usize").unwrap_or(false)
1067 } else {
1068 false
1069 };
1070
1071 // Parse arg config to check for explicit Count action
1072 let arg_config = parse_arg_attributes(&pat_type.attrs);
1073 let has_count_action = if let Some(config) = &arg_config {
1074 config.action.as_ref().map(|a| a == "count").unwrap_or(false)
1075 } else {
1076 false
1077 };
1078
1079 // usize type without Option is treated as a flag (Count action)
1080 let is_flag_type = is_flag || (is_usize_type && !is_option) || has_count_action;
1081
1082 // Get inner type for validation (unwrap Option if needed)
1083 let inner_ty =
1084 if is_option { extract_inner_type(&pat_type.ty) } else { (*pat_type.ty).clone() };
1085
1086 // Auto-infer validation from type
1087 let (mut min_val, mut max_val, mut min_len, mut max_len) =
1088 get_type_validation(&inner_ty);
1089
1090 // Parse validation attributes from parameter (e.g., #[validate(min = 0, max = 100)])
1091 if let Some(validation) = parse_validation_attributes(&pat_type.attrs) {
1092 // Override type-inferred validation with explicit attributes
1093 if let Some(min) = validation.min_value {
1094 min_val = Some(min);
1095 }
1096 if let Some(max) = validation.max_value {
1097 max_val = Some(max);
1098 }
1099 if let Some(min) = validation.min_length {
1100 min_len = Some(min);
1101 }
1102 if let Some(max) = validation.max_length {
1103 max_len = Some(max);
1104 }
1105 }
1106
1107 // Parse argument attributes (e.g., #[arg(short = 'v', default_value = "50")])
1108 // Note: arg_config already parsed above for is_flag_type check
1109
1110 // Auto-detect multiple values from Vec<T> type
1111 let is_vec_type = is_vec_type(&inner_ty);
1112 let multiple_values =
1113 arg_config.as_ref().map(|c| c.multiple).unwrap_or(false) || is_vec_type;
1114
1115 // Auto-infer action: usize type → Count (for flags like -v, -vv, -vvv),
1116 // bool flags → SetTrue (unless overridden)
1117 let inferred_action = if (is_usize_type && !is_option) || has_count_action {
1118 // usize type without Option is inferred as Count (for -v, -vv, -vvv patterns)
1119 Some("count".to_string())
1120 } else if is_flag && arg_config.as_ref().and_then(|c| c.action.as_ref()).is_none() {
1121 Some("set_true".to_string()) // Default for bool flags
1122 } else {
1123 None
1124 };
1125
1126 let min_value_token = if let Some(min) = min_val {
1127 quote! { Some(#min.to_string()) }
1128 } else {
1129 quote! { None }
1130 };
1131
1132 let max_value_token = if let Some(max) = max_val {
1133 quote! { Some(#max.to_string()) }
1134 } else {
1135 quote! { None }
1136 };
1137
1138 let min_length_token = if let Some(min) = min_len {
1139 quote! { Some(#min) }
1140 } else {
1141 quote! { None }
1142 };
1143
1144 let max_length_token = if let Some(max) = max_len {
1145 quote! { Some(#max) }
1146 } else {
1147 quote! { None }
1148 };
1149
1150 // Get help text - priority: #[arg(help = "...")] > docstring > default
1151 // Note: For docstring, use the clean description (without relationship tags)
1152 let help_token = if let Some(config) = &arg_config {
1153 if let Some(ref explicit_help) = config.help {
1154 quote! { Some(#explicit_help.to_string()) }
1155 } else if let Some(rel) = arg_relationships.get(&arg_name) {
1156 let help = &rel.description;
1157 if !help.is_empty() {
1158 quote! { Some(#help.to_string()) }
1159 } else {
1160 quote! { None }
1161 }
1162 } else {
1163 quote! { None }
1164 }
1165 } else if let Some(rel) = arg_relationships.get(&arg_name) {
1166 let help = &rel.description;
1167 if !help.is_empty() {
1168 quote! { Some(#help.to_string()) }
1169 } else {
1170 quote! { None }
1171 }
1172 } else {
1173 quote! { None }
1174 };
1175
1176 // Get long_help from #[arg(long_help = "...")]
1177 let long_help_token = if let Some(config) = &arg_config {
1178 if let Some(ref lh) = config.long_help {
1179 quote! { Some(#lh.to_string()) }
1180 } else {
1181 quote! { None }
1182 }
1183 } else {
1184 quote! { None }
1185 };
1186
1187 // Generate tokens for argument attributes
1188 let short_token = if let Some(config) = &arg_config {
1189 if let Some(s) = config.short {
1190 quote! { Some(#s) }
1191 } else {
1192 quote! { None }
1193 }
1194 } else {
1195 quote! { None }
1196 };
1197
1198 let default_value_token = if let Some(config) = &arg_config {
1199 if let Some(ref dv) = config.default_value {
1200 quote! { Some(#dv.to_string()) }
1201 } else {
1202 quote! { None }
1203 }
1204 } else {
1205 quote! { None }
1206 };
1207
1208 let env_token = if let Some(config) = &arg_config {
1209 if let Some(ref e) = config.env {
1210 quote! { Some(#e.to_string()) }
1211 } else {
1212 quote! { None }
1213 }
1214 } else {
1215 quote! { None }
1216 };
1217
1218 let value_name_token = if let Some(config) = &arg_config {
1219 if let Some(ref vn) = config.value_name {
1220 quote! { Some(#vn.to_string()) }
1221 } else {
1222 quote! { None }
1223 }
1224 } else {
1225 quote! { None }
1226 };
1227
1228 let aliases_token = if let Some(config) = &arg_config {
1229 if !config.aliases.is_empty() {
1230 let aliases_vec = &config.aliases;
1231 quote! { vec![#(#aliases_vec.to_string()),*] }
1232 } else {
1233 quote! { vec![] }
1234 }
1235 } else {
1236 quote! { vec![] }
1237 };
1238
1239 let positional_token = if let Some(config) = &arg_config {
1240 if let Some(pos) = config.positional {
1241 quote! { Some(#pos) }
1242 } else {
1243 quote! { None }
1244 }
1245 } else {
1246 quote! { None }
1247 };
1248
1249 // Generate action token - use explicit action if provided, otherwise use inferred
1250 let action_token = if let Some(config) = &arg_config {
1251 if let Some(ref act) = config.action {
1252 // Parse action string to ArgAction
1253 match act.as_str() {
1254 "count" => quote! { Some(::clap_noun_verb::ArgAction::Count) },
1255 "set" => quote! { Some(::clap_noun_verb::ArgAction::Set) },
1256 "set_false" => quote! { Some(::clap_noun_verb::ArgAction::SetFalse) },
1257 "set_true" => quote! { Some(::clap_noun_verb::ArgAction::SetTrue) },
1258 "append" => quote! { Some(::clap_noun_verb::ArgAction::Append) },
1259 _ => quote! { None },
1260 }
1261 } else if let Some(ref inferred) = inferred_action {
1262 match inferred.as_str() {
1263 "count" => quote! { Some(::clap_noun_verb::ArgAction::Count) },
1264 "set_true" => quote! { Some(::clap_noun_verb::ArgAction::SetTrue) },
1265 _ => quote! { None },
1266 }
1267 } else {
1268 quote! { None }
1269 }
1270 } else if let Some(ref inferred) = inferred_action {
1271 match inferred.as_str() {
1272 "count" => quote! { Some(::clap_noun_verb::ArgAction::Count) },
1273 "set_true" => quote! { Some(::clap_noun_verb::ArgAction::SetTrue) },
1274 _ => quote! { None },
1275 }
1276 } else {
1277 quote! { None }
1278 };
1279
1280 // Get group from doc comment or #[arg] attribute
1281 // Priority: #[arg(group = "...")] > doc comment [group: ...]
1282 let doc_rel = arg_relationships.get(&arg_name);
1283 let group_token = if let Some(config) = &arg_config {
1284 if let Some(ref g) = config.group {
1285 quote! { Some(#g.to_string()) }
1286 } else if let Some(rel) = doc_rel {
1287 if let Some(ref g) = rel.group {
1288 quote! { Some(#g.to_string()) }
1289 } else {
1290 quote! { None }
1291 }
1292 } else {
1293 quote! { None }
1294 }
1295 } else if let Some(rel) = doc_rel {
1296 if let Some(ref g) = rel.group {
1297 quote! { Some(#g.to_string()) }
1298 } else {
1299 quote! { None }
1300 }
1301 } else {
1302 quote! { None }
1303 };
1304
1305 // Get requires from doc comment or #[arg] attribute
1306 // Merges both sources if present
1307 let requires_token = {
1308 let mut requires_all = Vec::new();
1309
1310 // Add from #[arg(requires = "...")]
1311 if let Some(config) = &arg_config {
1312 requires_all.extend(config.requires.iter().cloned());
1313 }
1314
1315 // Add from doc comment [requires: ...]
1316 if let Some(rel) = doc_rel {
1317 for req in &rel.requires {
1318 if !requires_all.contains(req) {
1319 requires_all.push(req.clone());
1320 }
1321 }
1322 }
1323
1324 if requires_all.is_empty() {
1325 quote! { vec![] }
1326 } else {
1327 quote! { vec![#(#requires_all.to_string()),*] }
1328 }
1329 };
1330
1331 // Get conflicts_with from doc comment or #[arg] attribute
1332 // Merges both sources if present
1333 let conflicts_with_token = {
1334 let mut conflicts_all = Vec::new();
1335
1336 // Add from #[arg(conflicts_with = "...")]
1337 if let Some(config) = &arg_config {
1338 conflicts_all.extend(config.conflicts_with.iter().cloned());
1339 }
1340
1341 // Add from doc comment [conflicts: ...]
1342 if let Some(rel) = doc_rel {
1343 for conf in &rel.conflicts_with {
1344 if !conflicts_all.contains(conf) {
1345 conflicts_all.push(conf.clone());
1346 }
1347 }
1348 }
1349
1350 if conflicts_all.is_empty() {
1351 quote! { vec![] }
1352 } else {
1353 quote! { vec![#(#conflicts_all.to_string()),*] }
1354 }
1355 };
1356
1357 // Handle value_parser
1358 // Extract TokenStream string representation if explicit value_parser was specified
1359 let value_parser_token = if let Some(config) = &arg_config {
1360 if let Some(ref vp_ts) = config.value_parser {
1361 // Explicit value_parser specified - extract string representation
1362 // The TokenStream contains a string literal with the expression
1363 let ts_str = vp_ts.to_string();
1364 // Extract the actual expression string from the literal
1365 let expr_str = if ts_str.starts_with('"') && ts_str.ends_with('"') {
1366 ts_str.trim_matches('"').to_string()
1367 } else {
1368 ts_str
1369 };
1370 quote! { Some(#expr_str.to_string()) }
1371 } else {
1372 // Try auto-inference
1373 let inferred_parser = infer_type_parser(&inner_ty);
1374 if let Some(ref parser) = inferred_parser {
1375 quote! { Some(#parser.to_string()) }
1376 } else {
1377 quote! { None }
1378 }
1379 }
1380 } else {
1381 // Try auto-inference
1382 let inferred_parser = infer_type_parser(&inner_ty);
1383 if let Some(ref parser) = inferred_parser {
1384 quote! { Some(#parser.to_string()) }
1385 } else {
1386 quote! { None }
1387 }
1388 };
1389
1390 let next_line_help_token = if let Some(config) = &arg_config {
1391 let value = config.next_line_help;
1392 quote! { #value }
1393 } else {
1394 quote! { false }
1395 };
1396
1397 let display_order_token = if let Some(config) = &arg_config {
1398 if let Some(order) = config.display_order {
1399 quote! { Some(#order) }
1400 } else {
1401 quote! { None }
1402 }
1403 } else {
1404 quote! { None }
1405 };
1406
1407 let exclusive_token = if let Some(config) = &arg_config {
1408 if let Some(excl) = config.exclusive {
1409 quote! { Some(#excl) }
1410 } else {
1411 quote! { None }
1412 }
1413 } else {
1414 quote! { None }
1415 };
1416
1417 let trailing_vararg_token = if let Some(config) = &arg_config {
1418 let value = config.trailing_vararg;
1419 quote! { #value }
1420 } else {
1421 quote! { false }
1422 };
1423
1424 let allow_negative_numbers_token = if let Some(config) = &arg_config {
1425 let value = config.allow_negative_numbers;
1426 quote! { #value }
1427 } else {
1428 quote! { false }
1429 };
1430
1431 // New tokens for extended doc comment tags
1432 // Priority: #[arg] attribute > doc comment tag
1433
1434 // Get env from doc comment (already have env_token from #[arg])
1435 let doc_env_token = if let Some(rel) = doc_rel {
1436 if let Some(ref env_var) = rel.env {
1437 quote! { Some(#env_var.to_string()) }
1438 } else {
1439 quote! { None }
1440 }
1441 } else {
1442 quote! { None }
1443 };
1444 // Merge: prefer #[arg(env)] if present, else use doc comment
1445 let final_env_token = if let Some(config) = &arg_config {
1446 if config.env.is_some() {
1447 env_token.clone()
1448 } else {
1449 doc_env_token
1450 }
1451 } else {
1452 doc_env_token
1453 };
1454
1455 // Get hide from doc comment
1456 let hide_token = if let Some(rel) = doc_rel {
1457 let hide_val = rel.hide;
1458 quote! { #hide_val }
1459 } else {
1460 quote! { false }
1461 };
1462
1463 // Get default_value from doc comment (already have default_value_token from #[arg])
1464 let doc_default_token = if let Some(rel) = doc_rel {
1465 if let Some(ref dv) = rel.default_value {
1466 quote! { Some(#dv.to_string()) }
1467 } else {
1468 quote! { None }
1469 }
1470 } else {
1471 quote! { None }
1472 };
1473 // Merge: prefer #[arg(default_value)] if present, else use doc comment
1474 let final_default_token = if let Some(config) = &arg_config {
1475 if config.default_value.is_some() {
1476 default_value_token.clone()
1477 } else {
1478 doc_default_token
1479 }
1480 } else {
1481 doc_default_token
1482 };
1483
1484 // Get value_hint from doc comment
1485 let value_hint_token = if let Some(rel) = doc_rel {
1486 if let Some(ref vh) = rel.value_hint {
1487 quote! { Some(#vh.to_string()) }
1488 } else {
1489 quote! { None }
1490 }
1491 } else {
1492 quote! { None }
1493 };
1494
1495 // Get global from doc comment
1496 let global_token = if let Some(rel) = doc_rel {
1497 let global_val = rel.global;
1498 quote! { #global_val }
1499 } else {
1500 quote! { false }
1501 };
1502
1503 // Get exclusive from doc comment (merge with #[arg(exclusive)])
1504 let doc_exclusive_token = if let Some(rel) = doc_rel {
1505 if rel.exclusive {
1506 quote! { Some(true) }
1507 } else {
1508 quote! { None }
1509 }
1510 } else {
1511 quote! { None }
1512 };
1513 let final_exclusive_token = if let Some(config) = &arg_config {
1514 if config.exclusive.is_some() {
1515 exclusive_token.clone()
1516 } else {
1517 doc_exclusive_token
1518 }
1519 } else {
1520 doc_exclusive_token
1521 };
1522
1523 // Get help_heading from doc comment
1524 let help_heading_token = if let Some(rel) = doc_rel {
1525 if let Some(ref hh) = rel.help_heading {
1526 quote! { Some(#hh.to_string()) }
1527 } else {
1528 quote! { None }
1529 }
1530 } else {
1531 quote! { None }
1532 };
1533
1534 arg_metadata.push(quote! {
1535 ::clap_noun_verb::cli::registry::ArgMetadata {
1536 name: #arg_name.to_string(),
1537 required: !#is_option,
1538 is_flag: #is_flag_type,
1539 help: #help_token,
1540 min_value: #min_value_token,
1541 max_value: #max_value_token,
1542 min_length: #min_length_token,
1543 max_length: #max_length_token,
1544 short: #short_token,
1545 default_value: #final_default_token,
1546 env: #final_env_token,
1547 multiple: #multiple_values,
1548 value_name: #value_name_token,
1549 aliases: #aliases_token,
1550 positional: #positional_token,
1551 action: #action_token,
1552 group: #group_token,
1553 requires: #requires_token,
1554 conflicts_with: #conflicts_with_token,
1555 value_parser: #value_parser_token,
1556 hide: #hide_token,
1557 next_help_heading: #help_heading_token,
1558 long_help: #long_help_token,
1559 next_line_help: #next_line_help_token,
1560 display_order: #display_order_token,
1561 exclusive: #final_exclusive_token,
1562 trailing_vararg: #trailing_vararg_token,
1563 allow_negative_numbers: #allow_negative_numbers_token,
1564 value_hint: #value_hint_token,
1565 global: #global_token,
1566 }
1567 });
1568 }
1569 }
1570
1571 // GAP 2: Generate duplicate verb detection
1572 let noun_name_for_check = noun_name.as_deref().unwrap_or("__auto__");
1573 let duplicate_check =
1574 validation::generate_duplicate_detection(&verb_name, noun_name_for_check, fn_name);
1575
1576 // Telemetry instrumentation removed (no-op)
1577 let _telemetry_instrumentation = ();
1578
1579 // Generate wrapper function
1580 // Empty string "" means root-level verb (no noun)
1581 // "__auto__" means auto-infer from filename
1582 let noun_name_str = noun_name.as_deref().unwrap_or("__auto__");
1583 let about_str = about.as_deref().unwrap_or("");
1584
1585 let mut output_fn = input_fn.clone();
1586 output_fn.attrs.retain(|attr| {
1587 let is_noun = attr.path().is_ident("noun")
1588 || attr.path().segments.last().map(|seg| seg.ident == "noun").unwrap_or(false);
1589 !is_noun
1590 });
1591
1592 // Strip #[arg] and #[validate] attributes from parameters in output_fn
1593 for input in &mut output_fn.sig.inputs {
1594 if let syn::FnArg::Typed(pat_type) = input {
1595 pat_type.attrs.retain(|attr| {
1596 let is_arg = attr.path().is_ident("arg")
1597 || attr.path().segments.last().map(|seg| seg.ident == "arg").unwrap_or(false);
1598 let is_validate = attr.path().is_ident("validate")
1599 || attr
1600 .path()
1601 .segments
1602 .last()
1603 .map(|seg| seg.ident == "validate")
1604 .unwrap_or(false);
1605 !is_arg && !is_validate
1606 });
1607 }
1608 }
1609
1610 let expanded = quote! {
1611 #output_fn
1612
1613 // GAP 2: Compile-time duplicate verb detection
1614 #duplicate_check
1615
1616 // Wrapper function that adapts HandlerInput to function signature
1617 // NOTE: Use __handler_input to avoid shadowing if user has an arg named "input"
1618 fn #wrapper_name(__handler_input: ::clap_noun_verb::logic::HandlerInput) -> ::clap_noun_verb::error::Result<::clap_noun_verb::logic::HandlerOutput> {
1619 // Execute handler with argument extraction
1620 #(#arg_extractions)*
1621 let result = #fn_name(#(#arg_calls),*)?;
1622
1623 ::clap_noun_verb::logic::HandlerOutput::from_data(result)
1624 }
1625
1626 // Auto-generated registration
1627 // CRITICAL FIX: Use named function instead of closure to satisfy fn() type requirement
1628 #[allow(non_upper_case_globals)]
1629 #[linkme::distributed_slice(::clap_noun_verb::cli::registry::__VERB_REGISTRY)]
1630 static #init_fn_name: fn() = {
1631 fn __register_impl() {
1632 // Core team approach: Auto-infer noun name from filename if not explicitly provided
1633 // Special case: "root" means root-level verb (no noun)
1634 let (noun_name_static, noun_about_static, verb_name_final) = if #noun_name_str == "root" {
1635 // Root-level verb: no noun, verb appears directly under CLI binary
1636 // Pass empty string to registry to signal root verb
1637 ("", "", #verb_name)
1638 } else if #noun_name_str == "__auto__" {
1639 // Extract noun name from filename using file!() macro
1640 let file_path = file!();
1641 let inferred_name = ::std::path::Path::new(file_path)
1642 .file_stem()
1643 .and_then(|s| s.to_str())
1644 .unwrap_or("unknown")
1645 .to_string();
1646
1647 // If verb name was auto-inferred, strip noun name from verb name if it appears
1648 // Example: show_collector_status() -> verb_name="collector_status", noun="collector" -> verb="status"
1649 let mut final_verb_name = #verb_name.to_string();
1650 if final_verb_name.starts_with(&inferred_name) && final_verb_name.len() > inferred_name.len() {
1651 if final_verb_name.as_bytes()[inferred_name.len()] == b'_' {
1652 // Strip noun_ prefix (e.g., "collector_status" -> "status")
1653 final_verb_name = final_verb_name[inferred_name.len() + 1..].to_string();
1654 }
1655 }
1656
1657 // Extract module doc comments (//! ...) from source file
1658 // The noun description comes from the file's module-level docs,
1659 // not from the verb's function docs.
1660 let file_path = file!();
1661 let noun_about = ::std::fs::read_to_string(file_path)
1662 .ok()
1663 .and_then(|content| {
1664 let lines: Vec<&str> = content
1665 .lines()
1666 .take(20)
1667 .filter(|line| line.trim_start().starts_with("//!"))
1668 .map(|line| line.trim_start_matches("//!").trim())
1669 .collect();
1670 if lines.is_empty() { None } else { Some(lines.join("\n")) }
1671 })
1672 .unwrap_or_default();
1673
1674 // Leak strings to get static lifetime for registration (acceptable for CLI construction)
1675 let name_static: &'static str = Box::leak(inferred_name.into_boxed_str());
1676 let about_static: &'static str = Box::leak(noun_about.into_boxed_str());
1677 let verb_static: &'static str = Box::leak(final_verb_name.into_boxed_str());
1678
1679 // Auto-register noun with inferred name and doc
1680 ::clap_noun_verb::cli::registry::CommandRegistry::register_noun(
1681 name_static,
1682 about_static,
1683 );
1684
1685 (name_static, about_static, verb_static)
1686 } else {
1687 // Leak explicit noun name and about to get static lifetime
1688 let name_static: &'static str = Box::leak(#noun_name_str.to_string().into_boxed_str());
1689 let about_static: &'static str = Box::leak(String::new().into_boxed_str());
1690 let verb_static: &'static str = #verb_name;
1691
1692 // BUGFIX: Auto-register noun even with explicit noun name
1693 // This ensures the noun exists even if no #[noun] attribute was used
1694 ::clap_noun_verb::cli::registry::CommandRegistry::register_noun(
1695 name_static,
1696 about_static,
1697 );
1698
1699 (name_static, about_static, verb_static)
1700 };
1701
1702 let args = vec![#(#arg_metadata),*];
1703 ::clap_noun_verb::cli::registry::CommandRegistry::register_verb_with_args::<_>(
1704 noun_name_static,
1705 verb_name_final,
1706 #about_str,
1707 args,
1708 #wrapper_name,
1709 );
1710 }
1711 __register_impl // Return function pointer (not a call!)
1712 };
1713 };
1714
1715 expanded.into()
1716}
1717
1718/// Check if type is Option<T>
1719fn is_option_type(ty: &syn::Type) -> bool {
1720 if let syn::Type::Path(type_path) = ty {
1721 if let Some(segment) = type_path.path.segments.last() {
1722 segment.ident == "Option"
1723 } else {
1724 false
1725 }
1726 } else {
1727 false
1728 }
1729}
1730
1731/// Check if type is bool
1732fn is_bool_type(ty: &syn::Type) -> bool {
1733 if let syn::Type::Path(type_path) = ty {
1734 if let Some(segment) = type_path.path.segments.last() {
1735 segment.ident == "bool"
1736 } else {
1737 false
1738 }
1739 } else {
1740 false
1741 }
1742}
1743
1744/// Check if type is Vec<T>
1745fn is_vec_type(ty: &syn::Type) -> bool {
1746 if let syn::Type::Path(type_path) = ty {
1747 if let Some(segment) = type_path.path.segments.last() {
1748 segment.ident == "Vec"
1749 } else {
1750 false
1751 }
1752 } else {
1753 false
1754 }
1755}
1756
1757/// Validation constraints parsed from attributes
1758struct ValidationConstraints {
1759 min_value: Option<String>,
1760 max_value: Option<String>,
1761 min_length: Option<usize>,
1762 max_length: Option<usize>,
1763}
1764
1765/// Argument configuration parsed from #[arg(...)] attributes
1766struct ArgConfig {
1767 short: Option<char>,
1768 default_value: Option<String>,
1769 env: Option<String>,
1770 multiple: bool,
1771 value_name: Option<String>,
1772 aliases: Vec<String>,
1773 positional: Option<usize>,
1774 action: Option<String>, // Store as string: "count", "set", "set_false", "set_true", "append"
1775 group: Option<String>,
1776 requires: Vec<String>,
1777 conflicts_with: Vec<String>,
1778 value_parser: Option<proc_macro2::TokenStream>, // Store TokenStream for compile-time expansion
1779 help: Option<String>, // Override docstring help
1780 long_help: Option<String>, // Long help text
1781 next_line_help: bool, // Next line help formatting
1782 display_order: Option<usize>, // Display order in help
1783 exclusive: Option<bool>, // Exclusive group flag
1784 trailing_vararg: bool, // Trailing varargs support
1785 allow_negative_numbers: bool, // Allow negative numbers
1786}
1787
1788/// Parse argument attributes from parameter attributes
1789///
1790/// Parses `#[arg(short = 'v', default_value = "50", env = "PORT", multiple, value_name = "FILE")]` attributes
1791fn parse_arg_attributes(attrs: &[syn::Attribute]) -> Option<ArgConfig> {
1792 for attr in attrs {
1793 if attr.path().is_ident("arg") {
1794 if let syn::Meta::List(list) = &attr.meta {
1795 // Parse tokens manually to handle both flags (just names) and key-value pairs
1796 let mut config = ArgConfig {
1797 short: None,
1798 default_value: None,
1799 env: None,
1800 multiple: false,
1801 value_name: None,
1802 aliases: Vec::new(),
1803 positional: None,
1804 action: None,
1805 group: None,
1806 requires: Vec::new(),
1807 conflicts_with: Vec::new(),
1808 value_parser: None,
1809 help: None,
1810 long_help: None,
1811 next_line_help: false,
1812 display_order: None,
1813 exclusive: None,
1814 trailing_vararg: false,
1815 allow_negative_numbers: false,
1816 };
1817
1818 // Try parsing as MetaList first (handles key=value pairs)
1819 let parser =
1820 syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated;
1821 if let Ok(meta_list) = parser.parse2(list.tokens.clone()) {
1822 for meta in meta_list {
1823 match &meta {
1824 syn::Meta::NameValue(nv) => {
1825 let ident = nv.path.get_ident()?.to_string();
1826 match ident.as_str() {
1827 "short" => {
1828 // Parse short = 'v' or short = "v"
1829 if let syn::Expr::Lit(syn::ExprLit {
1830 lit: syn::Lit::Char(c),
1831 ..
1832 }) = &nv.value
1833 {
1834 config.short = Some(c.value());
1835 } else if let syn::Expr::Lit(syn::ExprLit {
1836 lit: syn::Lit::Str(s),
1837 ..
1838 }) = &nv.value
1839 {
1840 let s_val = s.value();
1841 if s_val.len() == 1 {
1842 config.short = s_val.chars().next();
1843 }
1844 }
1845 }
1846 "default_value" => {
1847 if let syn::Expr::Lit(syn::ExprLit {
1848 lit: syn::Lit::Str(s),
1849 ..
1850 }) = &nv.value
1851 {
1852 config.default_value = Some(s.value());
1853 }
1854 }
1855 "env" => {
1856 if let syn::Expr::Lit(syn::ExprLit {
1857 lit: syn::Lit::Str(s),
1858 ..
1859 }) = &nv.value
1860 {
1861 config.env = Some(s.value());
1862 }
1863 }
1864 "value_name" => {
1865 if let syn::Expr::Lit(syn::ExprLit {
1866 lit: syn::Lit::Str(s),
1867 ..
1868 }) = &nv.value
1869 {
1870 config.value_name = Some(s.value());
1871 }
1872 }
1873 "aliases" => {
1874 // Parse aliases = ["verbose", "v"]
1875 if let syn::Expr::Array(arr) = &nv.value {
1876 for expr in &arr.elems {
1877 if let syn::Expr::Lit(syn::ExprLit {
1878 lit: syn::Lit::Str(s),
1879 ..
1880 }) = expr
1881 {
1882 config.aliases.push(s.value());
1883 }
1884 }
1885 }
1886 }
1887 "alias" => {
1888 // Parse alias = "verbose" (single alias)
1889 if let syn::Expr::Lit(syn::ExprLit {
1890 lit: syn::Lit::Str(s),
1891 ..
1892 }) = &nv.value
1893 {
1894 config.aliases.push(s.value());
1895 }
1896 }
1897 "index" => {
1898 // Parse index = 0 (positional argument index)
1899 if let syn::Expr::Lit(syn::ExprLit {
1900 lit: syn::Lit::Int(i),
1901 ..
1902 }) = &nv.value
1903 {
1904 if let Ok(index) = i.base10_parse::<usize>() {
1905 config.positional = Some(index);
1906 }
1907 }
1908 }
1909 "action" => {
1910 // Parse action = "count", action = "set_false", etc.
1911 if let syn::Expr::Lit(syn::ExprLit {
1912 lit: syn::Lit::Str(s),
1913 ..
1914 }) = &nv.value
1915 {
1916 config.action = Some(s.value());
1917 }
1918 }
1919 "group" => {
1920 // Parse group = "filter"
1921 if let syn::Expr::Lit(syn::ExprLit {
1922 lit: syn::Lit::Str(s),
1923 ..
1924 }) = &nv.value
1925 {
1926 config.group = Some(s.value());
1927 }
1928 }
1929 "requires" => {
1930 // Parse requires = ["arg1", "arg2"] or requires = "arg1"
1931 if let syn::Expr::Array(arr) = &nv.value {
1932 for expr in &arr.elems {
1933 if let syn::Expr::Lit(syn::ExprLit {
1934 lit: syn::Lit::Str(s),
1935 ..
1936 }) = expr
1937 {
1938 config.requires.push(s.value());
1939 }
1940 }
1941 } else if let syn::Expr::Lit(syn::ExprLit {
1942 lit: syn::Lit::Str(s),
1943 ..
1944 }) = &nv.value
1945 {
1946 config.requires.push(s.value());
1947 }
1948 }
1949 "conflicts_with" => {
1950 // Parse conflicts_with = ["arg1", "arg2"] or conflicts_with = "arg1"
1951 if let syn::Expr::Array(arr) = &nv.value {
1952 for expr in &arr.elems {
1953 if let syn::Expr::Lit(syn::ExprLit {
1954 lit: syn::Lit::Str(s),
1955 ..
1956 }) = expr
1957 {
1958 config.conflicts_with.push(s.value());
1959 }
1960 }
1961 } else if let syn::Expr::Lit(syn::ExprLit {
1962 lit: syn::Lit::Str(s),
1963 ..
1964 }) = &nv.value
1965 {
1966 config.conflicts_with.push(s.value());
1967 }
1968 }
1969 "value_parser" => {
1970 // Parse value_parser = ...
1971 // Workaround: Convert TokenStream to string and match common patterns
1972 // This allows us to support common expressions like:
1973 // - clap_noun_verb::value_parser!(u16).range(1..=65535)
1974 // - clap_noun_verb::value_parser!(u32).range(1..)
1975 // - clap_noun_verb::value_parser!(PathBuf)
1976 // etc.
1977 // Convert syn::Expr to TokenStream, then to string
1978 let ts = quote::quote! { #nv.value };
1979 let ts_string = ts.to_string();
1980 config.value_parser =
1981 Some(proc_macro2::TokenStream::from_iter(
1982 std::iter::once(proc_macro2::TokenTree::Literal(
1983 proc_macro2::Literal::string(&ts_string),
1984 )),
1985 ));
1986 }
1987 "help" => {
1988 // Parse help = "..." to override docstring
1989 if let syn::Expr::Lit(syn::ExprLit {
1990 lit: syn::Lit::Str(s),
1991 ..
1992 }) = &nv.value
1993 {
1994 config.help = Some(s.value());
1995 }
1996 }
1997 "long_help" => {
1998 // Parse long_help = "..." for detailed help
1999 if let syn::Expr::Lit(syn::ExprLit {
2000 lit: syn::Lit::Str(s),
2001 ..
2002 }) = &nv.value
2003 {
2004 config.long_help = Some(s.value());
2005 }
2006 }
2007 "display_order" => {
2008 // Parse display_order = N
2009 if let syn::Expr::Lit(syn::ExprLit {
2010 lit: syn::Lit::Int(i),
2011 ..
2012 }) = &nv.value
2013 {
2014 if let Ok(order) = i.base10_parse::<usize>() {
2015 config.display_order = Some(order);
2016 }
2017 }
2018 }
2019 "exclusive" => {
2020 // Parse exclusive = true/false
2021 if let syn::Expr::Lit(syn::ExprLit {
2022 lit: syn::Lit::Bool(b),
2023 ..
2024 }) = &nv.value
2025 {
2026 config.exclusive = Some(b.value);
2027 }
2028 }
2029 "trailing_vararg" => {
2030 // Parse trailing_vararg = true
2031 if let syn::Expr::Lit(syn::ExprLit {
2032 lit: syn::Lit::Bool(b),
2033 ..
2034 }) = &nv.value
2035 {
2036 config.trailing_vararg = b.value;
2037 }
2038 }
2039 "allow_negative_numbers" => {
2040 // Parse allow_negative_numbers = true
2041 if let syn::Expr::Lit(syn::ExprLit {
2042 lit: syn::Lit::Bool(b),
2043 ..
2044 }) = &nv.value
2045 {
2046 config.allow_negative_numbers = b.value;
2047 }
2048 }
2049 _ => {}
2050 }
2051 }
2052 syn::Meta::Path(path) => {
2053 // Handle flag attributes like `multiple`, `next_line_help`, `trailing_vararg`
2054 if let Some(ident) = path.get_ident() {
2055 match ident.to_string().as_str() {
2056 "multiple" => config.multiple = true,
2057 "next_line_help" => config.next_line_help = true,
2058 "trailing_vararg" => config.trailing_vararg = true,
2059 "allow_negative_numbers" => {
2060 config.allow_negative_numbers = true
2061 }
2062 _ => {}
2063 }
2064 }
2065 }
2066 _ => {}
2067 }
2068 }
2069
2070 return Some(config);
2071 }
2072 }
2073 }
2074 }
2075 None
2076}
2077
2078/// Parse validation attributes from parameter attributes
2079///
2080/// Parses `#[validate(min = 0, max = 100, min_length = 1, max_length = 50)]` attributes
2081fn parse_validation_attributes(attrs: &[syn::Attribute]) -> Option<ValidationConstraints> {
2082 for attr in attrs {
2083 if attr.path().is_ident("validate") {
2084 if let syn::Meta::List(list) = &attr.meta {
2085 let parser = syn::punctuated::Punctuated::<syn::MetaNameValue, syn::Token![,]>::parse_terminated;
2086 if let Ok(meta_list) = parser.parse2(list.tokens.clone()) {
2087 let mut constraints = ValidationConstraints {
2088 min_value: None,
2089 max_value: None,
2090 min_length: None,
2091 max_length: None,
2092 };
2093
2094 for meta in meta_list {
2095 let ident = meta.path.get_ident()?.to_string();
2096 let value = match &meta.value {
2097 syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(i), .. }) => {
2098 if ident == "min" || ident == "min_value" {
2099 constraints.min_value = Some(i.base10_digits().to_string());
2100 } else if ident == "max" || ident == "max_value" {
2101 constraints.max_value = Some(i.base10_digits().to_string());
2102 } else if ident == "min_length" {
2103 if let Ok(v) = i.base10_parse::<usize>() {
2104 constraints.min_length = Some(v);
2105 }
2106 } else if ident == "max_length" {
2107 if let Ok(v) = i.base10_parse::<usize>() {
2108 constraints.max_length = Some(v);
2109 }
2110 }
2111 None
2112 }
2113 syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) => {
2114 if ident == "min"
2115 || ident == "min_value"
2116 || ident == "max"
2117 || ident == "max_value"
2118 {
2119 Some(s.value())
2120 } else {
2121 None
2122 }
2123 }
2124 _ => None,
2125 };
2126
2127 if let Some(val) = value {
2128 if ident == "min" || ident == "min_value" {
2129 constraints.min_value = Some(val);
2130 } else if ident == "max" || ident == "max_value" {
2131 constraints.max_value = Some(val);
2132 }
2133 }
2134 }
2135
2136 return Some(constraints);
2137 }
2138 }
2139 }
2140 }
2141 None
2142}
2143
2144/// Get auto-validation constraints for a type
2145///
2146/// Returns validation constraints that can be inferred from the type:
2147/// - `u32`, `u64`, `usize` = min_value = "0"
2148/// - `u8`, `u16` = min_value = "0", max_value inferred from type max
2149/// - `i32`, `i64`, `isize` = no auto validation (can be negative)
2150/// - `String` = no auto validation (but could add min_length/max_length later)
2151fn get_type_validation(
2152 ty: &syn::Type,
2153) -> (Option<String>, Option<String>, Option<usize>, Option<usize>) {
2154 if let syn::Type::Path(type_path) = ty {
2155 let type_name =
2156 type_path.path.segments.last().map(|s| s.ident.to_string()).unwrap_or_default();
2157
2158 match type_name.as_str() {
2159 // Unsigned integers: min = 0
2160 "u8" => (Some("0".to_string()), Some("255".to_string()), None, None),
2161 "u16" => (Some("0".to_string()), Some("65535".to_string()), None, None),
2162 "u32" | "u64" | "usize" => (Some("0".to_string()), None, None, None),
2163 // Signed integers: no auto validation (can be negative)
2164 "i8" => (Some("-128".to_string()), Some("127".to_string()), None, None),
2165 "i16" => (Some("-32768".to_string()), Some("32767".to_string()), None, None),
2166 "i32" | "i64" | "isize" => (None, None, None, None),
2167 // String: no auto validation (can add min_length/max_length from attributes later)
2168 "String" => (None, None, None, None),
2169 _ => (None, None, None, None),
2170 }
2171 } else {
2172 (None, None, None, None)
2173 }
2174}
2175
2176/// Extract inner type from Option<T>, Vec<T>, or return original
2177fn extract_inner_type(ty: &syn::Type) -> syn::Type {
2178 if let syn::Type::Path(type_path) = ty {
2179 if let Some(segment) = type_path.path.segments.last() {
2180 if segment.ident == "Option" || segment.ident == "Vec" {
2181 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
2182 if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
2183 return inner_ty.clone();
2184 }
2185 }
2186 }
2187 }
2188 }
2189 ty.clone()
2190}
2191
2192/// Infer type parser for common types
2193///
2194/// Returns a string representation of the parser expression for auto-inferred types:
2195/// - `PathBuf` → `clap_noun_verb::value_parser!(PathBuf)`
2196/// - `IpAddr` → `clap_noun_verb::value_parser!(IpAddr)`
2197/// - `Ipv4Addr` → `clap_noun_verb::value_parser!(Ipv4Addr)`
2198/// - `Ipv6Addr` → `clap_noun_verb::value_parser!(Ipv6Addr)`
2199/// - `Url` → `clap_noun_verb::value_parser!(Url)` (if url feature available)
2200/// - Numeric types already handled by validation constraints
2201fn infer_type_parser(ty: &syn::Type) -> Option<String> {
2202 if let syn::Type::Path(type_path) = ty {
2203 let type_name =
2204 type_path.path.segments.last().map(|s| s.ident.to_string()).unwrap_or_default();
2205
2206 match type_name.as_str() {
2207 "PathBuf" => Some("clap_noun_verb::value_parser!(::std::path::PathBuf)".to_string()),
2208 "IpAddr" => Some("clap_noun_verb::value_parser!(::std::net::IpAddr)".to_string()),
2209 "Ipv4Addr" => Some("clap_noun_verb::value_parser!(::std::net::Ipv4Addr)".to_string()),
2210 "Ipv6Addr" => Some("clap_noun_verb::value_parser!(::std::net::Ipv6Addr)".to_string()),
2211 // Url requires url crate - check if available at compile time
2212 // For now, we'll include it and let compilation fail if url feature isn't enabled
2213 "Url" => Some("clap_noun_verb::value_parser!(::url::Url)".to_string()),
2214 // Duration requires custom parser - defer to explicit specification
2215 _ => None,
2216 }
2217 } else {
2218 None
2219 }
2220}
2221
2222/// Mark a CLI as participatory in the federated network
2223///
2224/// This macro generates federation initialization code for CLI discovery,
2225/// capability advertisement, and peer authentication.
2226///
2227/// # Example
2228///
2229/// ```rust,ignore
2230/// #[federated(
2231/// discovery_url = "https://cli-federation.example.com",
2232/// identity = "my-cli-v1.0",
2233/// trust_anchor = "./certs/root.pem"
2234/// )]
2235/// struct MyCli;
2236/// ```
2237#[proc_macro_attribute]
2238pub fn federated(args: TokenStream, input: TokenStream) -> TokenStream {
2239 let args_tokens = proc_macro2::TokenStream::from(args);
2240 let input_tokens = proc_macro2::TokenStream::from(input);
2241
2242 match macros::federated_network::federated_impl(args_tokens, input_tokens) {
2243 Ok(tokens) => tokens.into(),
2244 Err(e) => e.to_compile_error().into(),
2245 }
2246}
2247
2248/// Advertise a capability to the federated network
2249///
2250/// This macro generates RDF metadata for a CLI command and publishes it to the
2251/// discovery service for remote invocation.
2252///
2253/// # Example
2254///
2255/// ```rust,ignore
2256/// #[advertise_capability(
2257/// capability_id = "process-data",
2258/// description = "Process data files",
2259/// inputs = ["file:path", "format:string"],
2260/// outputs = ["result:json"]
2261/// )]
2262/// #[verb("process")]
2263/// fn process_data(file: PathBuf, format: String) -> Result<ProcessResult> {
2264/// // Implementation
2265/// }
2266/// ```
2267#[proc_macro_attribute]
2268pub fn advertise_capability(args: TokenStream, input: TokenStream) -> TokenStream {
2269 let args_tokens = proc_macro2::TokenStream::from(args);
2270 let input_tokens = proc_macro2::TokenStream::from(input);
2271
2272 match macros::federated_network::advertise_capability_impl(args_tokens, input_tokens) {
2273 Ok(tokens) => tokens.into(),
2274 Err(e) => e.to_compile_error().into(),
2275 }
2276}
2277
2278/// Enable remote invocation of a CLI capability
2279///
2280/// This macro generates type-safe RPC stubs for calling remote CLI commands
2281/// with automatic serialization, authentication, and result validation.
2282///
2283/// # Example
2284///
2285/// ```rust,ignore
2286/// #[remote_invoke(
2287/// target = "remote-cli-v1.0",
2288/// capability = "process-data",
2289/// timeout_ms = 5000
2290/// )]
2291/// fn remote_process(file: PathBuf, format: String) -> Result<ProcessResult>;
2292/// ```
2293#[proc_macro_attribute]
2294pub fn remote_invoke(args: TokenStream, input: TokenStream) -> TokenStream {
2295 let args_tokens = proc_macro2::TokenStream::from(args);
2296 let input_tokens = proc_macro2::TokenStream::from(input);
2297
2298 match macros::federated_network::remote_invoke_impl(args_tokens, input_tokens) {
2299 Ok(tokens) => tokens.into(),
2300 Err(e) => e.to_compile_error().into(),
2301 }
2302}
2303
2304#[cfg(test)]
2305#[allow(clippy::unwrap_used, clippy::expect_used, clippy::items_after_test_module)]
2306mod tests {
2307 use super::*;
2308
2309 #[test]
2310 fn test_parse_doc_relationships_group() {
2311 let desc = "Export as JSON [group: format]";
2312 let result = parse_doc_relationships(desc);
2313 assert_eq!(result.group, Some("format".to_string()));
2314 assert_eq!(result.description, "Export as JSON");
2315 }
2316
2317 #[test]
2318 fn test_parse_doc_relationships_requires() {
2319 let desc = "Output filename [requires: format]";
2320 let result = parse_doc_relationships(desc);
2321 assert!(result.requires.contains(&"format".to_string()));
2322 assert_eq!(result.description, "Output filename");
2323 }
2324
2325 #[test]
2326 fn test_parse_doc_relationships_conflicts() {
2327 let desc = "Output format [conflicts: raw]";
2328 let result = parse_doc_relationships(desc);
2329 assert!(result.conflicts_with.contains(&"raw".to_string()));
2330 assert_eq!(result.description, "Output format");
2331 }
2332
2333 #[test]
2334 fn test_parse_argument_descriptions_with_relationships() {
2335 let docstring = r#"Test command
2336
2337# Arguments
2338* `json` - Export as JSON [group: format]
2339* `yaml` - Export as YAML [group: format]
2340* `filename` - Output filename [requires: format]
2341"#;
2342 let result = parse_argument_descriptions_with_relationships(docstring);
2343
2344 assert!(result.contains_key("json"), "Should contain 'json' key");
2345 assert!(result.contains_key("yaml"), "Should contain 'yaml' key");
2346 assert!(result.contains_key("filename"), "Should contain 'filename' key");
2347
2348 let json_rel = result.get("json").unwrap();
2349 assert_eq!(json_rel.group, Some("format".to_string()), "json should have group='format'");
2350
2351 let yaml_rel = result.get("yaml").unwrap();
2352 assert_eq!(yaml_rel.group, Some("format".to_string()), "yaml should have group='format'");
2353
2354 let filename_rel = result.get("filename").unwrap();
2355 assert!(
2356 filename_rel.requires.contains(&"format".to_string()),
2357 "filename should require 'format'"
2358 );
2359 }
2360}
2361
2362// ============================================================================
2363// FRONTIER: Fractal Pattern Macros
2364// ============================================================================
2365
2366/// Attribute macro for defining nouns at different architectural levels
2367///
2368/// This macro generates `FractalNoun` trait implementations for structs,
2369/// enabling type-safe cross-level composition.
2370///
2371/// # Usage
2372///
2373/// ```rust,ignore
2374/// use clap_noun_verb_macros::noun_level;
2375///
2376/// #[noun_level(Level::CLI)]
2377/// struct ServiceCommand {
2378/// name: String,
2379/// }
2380///
2381/// #[noun_level(Level::Agent)]
2382/// struct ServiceAgent {
2383/// capability: String,
2384/// }
2385///
2386/// #[noun_level(Level::Ecosystem)]
2387/// struct ServiceCollective {
2388/// members: Vec<String>,
2389/// }
2390/// ```
2391#[proc_macro_attribute]
2392pub fn noun_level(args: TokenStream, input: TokenStream) -> TokenStream {
2393 let input_struct = parse_macro_input!(input as syn::DeriveInput);
2394 let args_tokens = proc_macro2::TokenStream::from(args);
2395
2396 // Parse level from arguments
2397 let level = match macros::fractal_patterns::parse_level_arg(args_tokens) {
2398 Ok(level) => level,
2399 Err(e) => return e.to_compile_error().into(),
2400 };
2401
2402 // Generate FractalNoun implementation
2403 let impl_code = macros::fractal_patterns::generate_noun_impl(&input_struct, level);
2404
2405 // Combine original struct with generated implementation
2406 let expanded = quote! {
2407 #input_struct
2408 #impl_code
2409 };
2410
2411 expanded.into()
2412}
2413
2414/// Attribute macro for defining verbs at different architectural levels
2415///
2416/// This macro generates `FractalVerb` trait implementations for impl blocks,
2417/// enabling type-safe verb-noun composition at each level.
2418///
2419/// # Usage
2420///
2421/// ```rust,ignore
2422/// use clap_noun_verb_macros::verb_level;
2423///
2424/// #[verb_level(Level::CLI)]
2425/// impl ServiceCommand {
2426/// fn start(&self) -> Result<(), String> {
2427/// Ok(())
2428/// }
2429/// }
2430///
2431/// #[verb_level(Level::Agent)]
2432/// impl ServiceAgent {
2433/// fn execute(&self) -> Result<(), String> {
2434/// Ok(())
2435/// }
2436/// }
2437///
2438/// #[verb_level(Level::Ecosystem)]
2439/// impl ServiceCollective {
2440/// fn orchestrate(&self) -> Result<(), String> {
2441/// Ok(())
2442/// }
2443/// }
2444/// ```
2445#[proc_macro_attribute]
2446pub fn verb_level(args: TokenStream, input: TokenStream) -> TokenStream {
2447 let input_impl = parse_macro_input!(input as syn::ItemImpl);
2448 let args_tokens = proc_macro2::TokenStream::from(args);
2449
2450 // Parse level from arguments
2451 let level = match macros::fractal_patterns::parse_level_arg(args_tokens) {
2452 Ok(level) => level,
2453 Err(e) => return e.to_compile_error().into(),
2454 };
2455
2456 // Generate FractalVerb implementation
2457 let impl_code = macros::fractal_patterns::generate_verb_impl(&input_impl, level);
2458
2459 // Combine original impl with generated code
2460 let expanded = quote! {
2461 #input_impl
2462 #impl_code
2463 };
2464
2465 expanded.into()
2466}
2467
2468/// Mark a function as semantically composable capability
2469///
2470/// This macro enables semantic discovery, type-safe composition, and MCP protocol
2471/// integration for CLI capabilities. It generates:
2472/// - RDF metadata for SPARQL-based discovery
2473/// - Type-level composition validators
2474/// - MCP protocol descriptors for agent communication
2475/// - Distributed slice registration for auto-discovery
2476///
2477/// # Arguments
2478///
2479/// - `uri` (required): Unique capability URI in IRI format
2480/// - `inputs` (optional): RDF type expression for input parameters
2481/// - `outputs` (optional): RDF type expression for return type
2482/// - `constraints` (optional): SPARQL ASK query for composition constraints
2483/// - `mcp_version` (optional): MCP protocol version (default: "2024.1")
2484///
2485/// # Example
2486///
2487/// ```rust,ignore
2488/// use clap_noun_verb_macros::semantic_composable;
2489///
2490/// #[semantic_composable(
2491/// uri = "urn:example:capability:file-reader",
2492/// inputs = "rdf:type fs:Path",
2493/// outputs = "rdf:type text:Content",
2494/// constraints = "ASK WHERE { ?s rdf:type fs:ReadableFile }"
2495/// )]
2496/// fn read_file(path: PathBuf) -> Result<String, std::io::Error> {
2497/// std::fs::read_to_string(path)
2498/// }
2499/// ```
2500///
2501/// # Compile-Time Validation
2502///
2503/// - Function must return `Result<T, E>` for error handling
2504/// - Async functions not yet supported (FUTURE: tokio integration)
2505/// - Unsafe functions not allowed (memory safety requirement)
2506/// - All parameters must be serializable (for MCP protocol)
2507///
2508/// # Runtime Integration
2509///
2510/// Capabilities are registered in `SEMANTIC_CAPABILITIES` distributed slice
2511/// and can be discovered at runtime via SPARQL queries on the RDF store.
2512///
2513/// See `clap_noun_verb::semantic` module for runtime support.
2514#[proc_macro_attribute]
2515pub fn semantic_composable(args: TokenStream, input: TokenStream) -> TokenStream {
2516 let attrs = match syn::parse::<macros::semantic_composition::SemanticAttributes>(args) {
2517 Ok(attrs) => attrs,
2518 Err(e) => return e.to_compile_error().into(),
2519 };
2520
2521 let function = match syn::parse::<ItemFn>(input) {
2522 Ok(f) => f,
2523 Err(e) => return e.to_compile_error().into(),
2524 };
2525
2526 match macros::semantic_composition::expand_semantic_composable(attrs, function) {
2527 Ok(tokens) => tokens.into(),
2528 Err(e) => e.to_compile_error().into(),
2529 }
2530}
2531
2532// ============================================================================
2533// FRONTIER: Executable Specifications Macros
2534// ============================================================================
2535
2536/// Converts documentation into executable tests with proof generation
2537///
2538/// This macro extracts specifications from doc comments and generates:
2539/// - Property-based tests
2540/// - Proof evidence collection
2541/// - Audit trail metrics
2542/// - Specification versioning
2543///
2544/// # Usage
2545///
2546/// ```rust,ignore
2547/// /// Calculate sum of two numbers
2548/// /// @version 1.0.0
2549/// /// @property[correctness] result >= a && result >= b
2550/// /// @property[performance] execution_time < 1ms
2551/// #[spec]
2552/// fn add(a: u32, b: u32) -> u32 {
2553/// a + b
2554/// }
2555/// ```
2556///
2557/// # Features
2558///
2559/// - **Type-First**: Specifications encoded at compile time
2560/// - **Zero-Cost**: All validation happens at compile time
2561/// - **Evidence**: Automatic proof generation for compliance
2562#[proc_macro_attribute]
2563pub fn spec(_args: TokenStream, input: TokenStream) -> TokenStream {
2564 let input_fn = parse_macro_input!(input as ItemFn);
2565
2566 match macros::executable_specs::generate_spec(&input_fn.attrs, &input_fn) {
2567 Ok(tokens) => tokens.into(),
2568 Err(e) => e.to_compile_error().into(),
2569 }
2570}
2571
2572/// Marks achievement targets with criteria tracking
2573///
2574/// This macro generates compile-time milestone tracking with:
2575/// - Target date validation
2576/// - Criteria collection
2577/// - Status tracking
2578/// - Progress metrics
2579///
2580/// # Usage
2581///
2582/// ```rust,ignore
2583/// /// Feature: User authentication
2584/// /// @milestone Phase1-Auth
2585/// /// @target 2024-12-31
2586/// /// @criteria OAuth2 integration complete
2587/// /// @criteria JWT token validation working
2588/// #[milestone]
2589/// fn auth_milestone() {}
2590/// ```
2591#[proc_macro_attribute]
2592pub fn milestone(_args: TokenStream, input: TokenStream) -> TokenStream {
2593 let input_fn = parse_macro_input!(input as ItemFn);
2594
2595 match macros::executable_specs::generate_milestone(&input_fn.attrs, &input_fn) {
2596 Ok(tokens) => tokens.into(),
2597 Err(e) => e.to_compile_error().into(),
2598 }
2599}
2600
2601/// Runtime validation of invariant properties
2602///
2603/// This macro generates runtime checks that invariants hold:
2604/// - Pre-condition validation
2605/// - Post-condition validation
2606/// - Severity-based handling (error, warning, info)
2607/// - Configurable check frequency
2608///
2609/// # Usage
2610///
2611/// ```rust,ignore
2612/// /// Process user data
2613/// /// @invariant[non_negative] value >= 0
2614/// /// @severity error
2615/// /// @frequency always
2616/// #[invariant]
2617/// fn process_value() {
2618/// // Implementation
2619/// }
2620/// ```
2621///
2622/// # Configuration
2623///
2624/// - Enable panic on failure: `--features invariant_panic`
2625/// - Otherwise prints warning to stderr
2626#[proc_macro_attribute]
2627pub fn invariant(_args: TokenStream, input: TokenStream) -> TokenStream {
2628 let input_fn = parse_macro_input!(input as ItemFn);
2629
2630 match macros::executable_specs::generate_invariant(&input_fn.attrs, &input_fn) {
2631 Ok(tokens) => tokens.into(),
2632 Err(e) => e.to_compile_error().into(),
2633 }
2634}
2635
2636// ============================================================================
2637// Learning Trajectory Macros (Frontier)
2638// ============================================================================
2639
2640/// Define a competency dimension with multi-dimensional skill tracking
2641///
2642/// This macro generates CompetencyDimension trait implementation for a struct,
2643/// enabling proficiency tracking across multiple skill areas.
2644///
2645/// # Arguments
2646///
2647/// - `dimension = "name"` - The name of the competency dimension
2648///
2649/// # Example
2650///
2651/// ```rust,ignore
2652/// use clap_noun_verb_macros::competency;
2653/// use clap_noun_verb_macros::macros::learning_trajectories::ProficiencyLevel;
2654///
2655/// #[competency(dimension = "CLI Development")]
2656/// struct CliSkills {
2657/// parsing: ProficiencyLevel,
2658/// validation: ProficiencyLevel,
2659/// composition: ProficiencyLevel,
2660/// }
2661///
2662/// let skills = CliSkills {
2663/// parsing: ProficiencyLevel::new(0.8),
2664/// validation: ProficiencyLevel::new(0.7),
2665/// composition: ProficiencyLevel::new(0.6),
2666/// };
2667///
2668/// assert_eq!(skills.name(), "CLI Development");
2669/// assert!(skills.aggregate_proficiency().value() >= 0.7);
2670/// ```
2671#[proc_macro_attribute]
2672pub fn competency(args: TokenStream, input: TokenStream) -> TokenStream {
2673 let input_parsed = parse_macro_input!(input as syn::DeriveInput);
2674 let args_stream = proc_macro2::TokenStream::from(args);
2675
2676 match macros::learning_trajectories::parse_competency_args(args_stream) {
2677 Ok(dimension) => {
2678 let impl_tokens =
2679 macros::learning_trajectories::generate_competency_impl(&input_parsed, &dimension);
2680 let original = quote! { #input_parsed };
2681 quote! {
2682 #original
2683 #impl_tokens
2684 }
2685 .into()
2686 }
2687 Err(e) => e.to_compile_error().into(),
2688 }
2689}
2690
2691/// Define an assessment function with proficiency evaluation
2692///
2693/// This macro generates AssessmentEngine trait implementation for a function,
2694/// enabling learner proficiency evaluation with configurable thresholds.
2695///
2696/// # Arguments
2697///
2698/// - `threshold = 0.8` - The passing threshold (default: 0.8)
2699///
2700/// # Example
2701///
2702/// ```rust,ignore
2703/// use clap_noun_verb_macros::assessment;
2704/// use clap_noun_verb_macros::macros::learning_trajectories::AssessmentResult;
2705///
2706/// #[assessment(threshold = 0.75)]
2707/// fn evaluate_proficiency() -> AssessmentResult {
2708/// // Evaluation logic
2709/// AssessmentResult::new(0.85, "Proficient")
2710/// }
2711/// ```
2712#[proc_macro_attribute]
2713pub fn assessment(args: TokenStream, input: TokenStream) -> TokenStream {
2714 let input_parsed = parse_macro_input!(input as syn::ItemFn);
2715 let args_stream = proc_macro2::TokenStream::from(args);
2716
2717 match macros::learning_trajectories::parse_assessment_args(args_stream) {
2718 Ok(threshold) => {
2719 let impl_tokens =
2720 macros::learning_trajectories::generate_assessment_impl(&input_parsed, threshold);
2721 let original = quote! { #input_parsed };
2722 quote! {
2723 #original
2724 #impl_tokens
2725 }
2726 .into()
2727 }
2728 Err(e) => e.to_compile_error().into(),
2729 }
2730}
2731
2732/// Define a learning path generator with optimal sequence planning
2733///
2734/// This macro generates PathOptimizer trait implementation for a function,
2735/// enabling generation of optimal learning sequences to reach target competency.
2736///
2737/// # Arguments
2738///
2739/// - `target = "level"` - Target competency level (foundation, intermediate, advanced, expert)
2740///
2741/// # Example
2742///
2743/// ```rust,ignore
2744/// use clap_noun_verb_macros::learning_path;
2745/// use clap_noun_verb_macros::macros::learning_trajectories::{CompetencyLevel, LearningPath, LearningStep};
2746///
2747/// #[learning_path(target = "Expert")]
2748/// fn generate_cli_path(current: CompetencyLevel, target: CompetencyLevel) -> LearningPath {
2749/// let steps = vec![
2750/// LearningStep::new(CompetencyLevel::Foundation, "Learn CLI basics"),
2751/// LearningStep::new(CompetencyLevel::Intermediate, "Master patterns"),
2752/// LearningStep::new(CompetencyLevel::Advanced, "Implement features"),
2753/// LearningStep::new(CompetencyLevel::Expert, "Design frameworks"),
2754/// ];
2755/// LearningPath::new(steps, target)
2756/// }
2757/// ```
2758#[proc_macro_attribute]
2759pub fn learning_path(args: TokenStream, input: TokenStream) -> TokenStream {
2760 let input_parsed = parse_macro_input!(input as syn::ItemFn);
2761 let args_stream = proc_macro2::TokenStream::from(args);
2762
2763 match macros::learning_trajectories::parse_learning_path_args(args_stream) {
2764 Ok(target) => {
2765 let impl_tokens =
2766 macros::learning_trajectories::generate_path_impl(&input_parsed, target);
2767 let original = quote! { #input_parsed };
2768 quote! {
2769 #original
2770 #impl_tokens
2771 }
2772 .into()
2773 }
2774 Err(e) => e.to_compile_error().into(),
2775 }
2776}
2777
2778/// Automatically generate tests from semantic combinations
2779///
2780/// This macro analyzes the annotated function and generates comprehensive test cases:
2781/// - Basic functionality tests
2782/// - Property-based tests using proptest
2783/// - Edge case and boundary tests
2784///
2785/// # Example
2786///
2787/// ```rust,ignore
2788/// use clap_noun_verb_macros::auto_test;
2789///
2790/// #[auto_test]
2791/// fn parse_command(input: &str) -> Result<Command, ParseError> {
2792/// // Implementation
2793/// Ok(Command::default())
2794/// }
2795///
2796/// // Generates:
2797/// // - test_parse_command_basic
2798/// // - test_parse_command_property
2799/// // - test_parse_command_edge_cases
2800/// ```
2801#[proc_macro_attribute]
2802pub fn auto_test(args: TokenStream, input: TokenStream) -> TokenStream {
2803 let input_parsed = parse_macro_input!(input as ItemFn);
2804 let args_stream = proc_macro2::TokenStream::from(args);
2805
2806 match macros::reflexive_testing_macro::generate_auto_test(args_stream, input_parsed) {
2807 Ok(tokens) => tokens.into(),
2808 Err(e) => e.to_compile_error().into(),
2809 }
2810}