Skip to main content

vox_macros_parse/
lib.rs

1#![allow(clippy::result_large_err)]
2//! Parser grammar for vox RPC service trait definitions.
3//!
4//! # This Is Just a Grammar
5//!
6//! This crate contains **only** the [unsynn] grammar for parsing Rust trait definitions
7//! that define vox RPC services. It does not:
8//!
9//! - Generate any code
10//! - Perform validation
11//! - Know anything about vox's wire protocol
12//! - Have opinions about how services should be implemented
13//!
14//! It simply parses syntax like:
15//!
16//! ```ignore
17//! pub trait Calculator {
18//!     /// Add two numbers.
19//!     async fn add(&self, a: i32, b: i32) -> i32;
20//! }
21//! ```
22//!
23//! ...and produces an AST ([`ServiceTrait`]) that downstream crates can inspect.
24//!
25//! # Why a Separate Crate?
26//!
27//! The grammar is extracted into its own crate so that:
28//!
29//! 1. **It can be tested independently** — We use [datatest-stable] + [insta] for
30//!    snapshot testing the parsed AST, which isn't possible in a proc-macro crate.
31//!
32//! 2. **It's reusable** — Other tools (linters, documentation generators, IDE plugins)
33//!    can parse service definitions without pulling in proc-macro dependencies.
34//!
35//! 3. **Separation of concerns** — The grammar is pure parsing; [`vox-macros`] handles
36//!    the proc-macro machinery; [`vox-codegen`] handles actual code generation.
37//!
38//! # The Bigger Picture
39//!
40//! ```text
41//! vox-macros-parse     vox-macros              vox-codegen
42//! ┌──────────────┐     ┌──────────────┐         ┌──────────────┐
43//! │              │     │              │         │              │
44//! │  unsynn      │────▶│  #[service]  │────────▶│  build.rs    │
45//! │  grammar     │     │  proc macro  │         │  code gen    │
46//! │              │     │              │         │              │
47//! └──────────────┘     └──────────────┘         └──────────────┘
48//!    just parsing         emit metadata          Rust, TS, Go...
49//! ```
50//!
51//! [unsynn]: https://docs.rs/unsynn
52//! [datatest-stable]: https://docs.rs/datatest-stable
53//! [insta]: https://docs.rs/insta
54//! [`vox-macros`]: https://docs.rs/vox-service-macros
55//! [`vox-codegen`]: https://docs.rs/vox-codegen
56
57pub use unsynn::Error as ParseError;
58pub use unsynn::ToTokens;
59
60use proc_macro2::TokenStream as TokenStream2;
61use unsynn::operator::names::{
62    Assign, Colon, Comma, Gt, LifetimeTick, Lt, PathSep, Pound, RArrow, Semicolon,
63};
64use unsynn::{
65    Any, BraceGroupContaining, BracketGroupContaining, CommaDelimitedVec, Cons, Either,
66    EndOfStream, Except, Ident, LiteralString, Many, Optional, ParenthesisGroupContaining, Parse,
67    ToTokenIter, TokenStream, keyword, unsynn,
68};
69
70keyword! {
71    pub KAsync = "async";
72    pub KFn = "fn";
73    pub KTrait = "trait";
74    pub KSelfKw = "self";
75    pub KMut = "mut";
76    pub KDoc = "doc";
77    pub KPub = "pub";
78    pub KWhere = "where";
79}
80
81/// Parses tokens and groups until `C` is found, handling `<...>` correctly.
82type VerbatimUntil<C> = Many<Cons<Except<C>, AngleTokenTree>>;
83
84unsynn! {
85    /// Parses either a `TokenTree` or `<...>` grouping.
86    #[derive(Clone)]
87    pub struct AngleTokenTree(
88        pub Either<Cons<Lt, Vec<Cons<Except<Gt>, AngleTokenTree>>, Gt>, unsynn::TokenTree>,
89    );
90
91    pub struct RawAttribute {
92        pub _pound: Pound,
93        pub body: BracketGroupContaining<TokenStream>,
94    }
95
96    pub struct DocAttribute {
97        pub _doc: KDoc,
98        pub _assign: Assign,
99        pub value: LiteralString,
100    }
101
102    pub enum Visibility {
103        Pub(KPub),
104        PubRestricted(Cons<KPub, ParenthesisGroupContaining<TokenStream>>),
105    }
106
107    pub struct RefSelf {
108        pub _amp: unsynn::operator::names::And,
109        pub lifetime: Option<Cons<LifetimeTick, Ident>>,
110        pub mutability: Option<KMut>,
111        pub name: KSelfKw,
112    }
113
114    pub struct ValueSelf {
115        pub mutability: Option<KMut>,
116        pub name: KSelfKw,
117    }
118
119    pub struct TypedSelf {
120        pub mutability: Option<KMut>,
121        pub name: KSelfKw,
122        pub _colon: Colon,
123        pub ty: Type,
124    }
125
126    pub enum MethodReceiver {
127        Ref(RefSelf),
128        Typed(TypedSelf),
129        Value(ValueSelf),
130    }
131
132    pub struct MethodParam {
133        pub name: Ident,
134        pub _colon: Colon,
135        pub ty: Type,
136    }
137
138    pub struct GenericParams {
139        pub _lt: Lt,
140        pub params: VerbatimUntil<Gt>,
141        pub _gt: Gt,
142    }
143
144    #[derive(Clone)]
145    pub struct TypePath {
146        pub leading: Option<PathSep>,
147        pub first: Ident,
148        pub rest: Any<Cons<PathSep, Ident>>,
149    }
150
151    #[derive(Clone)]
152    pub struct Lifetime {
153        pub _apo: LifetimeTick,
154        pub ident: Ident,
155    }
156
157    #[derive(Clone)]
158    pub enum GenericArgument {
159        Lifetime(Lifetime),
160        Type(Type),
161    }
162
163    #[derive(Clone)]
164    pub enum Type {
165        Reference(TypeRef),
166        Tuple(TypeTuple),
167        PathWithGenerics(PathWithGenerics),
168        Path(TypePath),
169    }
170
171    #[derive(Clone)]
172    pub struct TypeRef {
173        pub _amp: unsynn::operator::names::And,
174        pub lifetime: Option<Cons<LifetimeTick, Ident>>,
175        pub mutable: Option<KMut>,
176        pub inner: Box<Type>,
177    }
178
179    #[derive(Clone)]
180    pub struct TypeTuple(
181        pub ParenthesisGroupContaining<CommaDelimitedVec<Type>>,
182    );
183
184    #[derive(Clone)]
185    pub struct PathWithGenerics {
186        pub path: TypePath,
187        pub _lt: Lt,
188        pub args: CommaDelimitedVec<GenericArgument>,
189        pub _gt: Gt,
190    }
191
192    pub struct ReturnType {
193        pub _arrow: RArrow,
194        pub ty: Type,
195    }
196
197    pub struct WhereClause {
198        pub _where: KWhere,
199        pub bounds: VerbatimUntil<Semicolon>,
200    }
201
202    pub struct MethodParams {
203        pub receiver: MethodReceiver,
204        pub rest: Optional<Cons<Comma, CommaDelimitedVec<MethodParam>>>,
205    }
206
207    pub struct ServiceMethod {
208        pub attributes: Any<RawAttribute>,
209        pub _async: Optional<KAsync>,
210        pub _fn: KFn,
211        pub name: Ident,
212        pub generics: Optional<GenericParams>,
213        pub params: ParenthesisGroupContaining<MethodParams>,
214        pub return_type: Optional<ReturnType>,
215        pub where_clause: Optional<WhereClause>,
216        pub _semi: Semicolon,
217    }
218
219    pub struct ServiceTrait {
220        pub attributes: Any<RawAttribute>,
221        pub vis: Optional<Visibility>,
222        pub _trait: KTrait,
223        pub name: Ident,
224        pub generics: Optional<GenericParams>,
225        pub body: BraceGroupContaining<Any<ServiceMethod>>,
226        pub _eos: EndOfStream,
227    }
228}
229
230// ============================================================================
231// Helper methods for GenericArgument
232// ============================================================================
233
234impl GenericArgument {
235    pub fn has_lifetime(&self) -> bool {
236        match self {
237            GenericArgument::Lifetime(_) => true,
238            GenericArgument::Type(ty) => ty.has_lifetime(),
239        }
240    }
241
242    pub fn has_named_lifetime(&self, name: &str) -> bool {
243        match self {
244            GenericArgument::Lifetime(lifetime) => lifetime.ident == name,
245            GenericArgument::Type(ty) => ty.has_named_lifetime(name),
246        }
247    }
248
249    pub fn has_non_named_lifetime(&self, name: &str) -> bool {
250        match self {
251            GenericArgument::Lifetime(lifetime) => lifetime.ident != name,
252            GenericArgument::Type(ty) => ty.has_non_named_lifetime(name),
253        }
254    }
255
256    pub fn has_elided_reference_lifetime(&self) -> bool {
257        match self {
258            GenericArgument::Lifetime(_) => false,
259            GenericArgument::Type(ty) => ty.has_elided_reference_lifetime(),
260        }
261    }
262
263    pub fn contains_channel(&self) -> bool {
264        match self {
265            GenericArgument::Lifetime(_) => false,
266            GenericArgument::Type(ty) => ty.contains_channel(),
267        }
268    }
269}
270
271// ============================================================================
272// Helper methods for Type
273// ============================================================================
274
275impl Type {
276    /// Extract Ok and Err types if this is Result<T, E>
277    pub fn as_result(&self) -> Option<(&Type, &Type)> {
278        match self {
279            Type::PathWithGenerics(PathWithGenerics { path, args, .. })
280                if path.last_segment().as_str() == "Result" && args.len() == 2 =>
281            {
282                let args_slice = args.as_slice();
283                match (&args_slice[0].value, &args_slice[1].value) {
284                    (GenericArgument::Type(ok), GenericArgument::Type(err)) => Some((ok, err)),
285                    _ => None,
286                }
287            }
288            _ => None,
289        }
290    }
291
292    /// Check if type contains a lifetime anywhere in the tree
293    pub fn has_lifetime(&self) -> bool {
294        match self {
295            Type::Reference(TypeRef {
296                lifetime: Some(_), ..
297            }) => true,
298            Type::Reference(TypeRef { inner, .. }) => inner.has_lifetime(),
299            Type::PathWithGenerics(PathWithGenerics { args, .. }) => {
300                args.iter().any(|t| t.value.has_lifetime())
301            }
302            Type::Tuple(TypeTuple(group)) => group.content.iter().any(|t| t.value.has_lifetime()),
303            Type::Path(_) => false,
304        }
305    }
306
307    /// Check if type contains the named lifetime anywhere in the tree.
308    pub fn has_named_lifetime(&self, name: &str) -> bool {
309        match self {
310            Type::Reference(TypeRef {
311                lifetime: Some(lifetime),
312                ..
313            }) => lifetime.second == name,
314            Type::Reference(TypeRef { inner, .. }) => inner.has_named_lifetime(name),
315            Type::PathWithGenerics(PathWithGenerics { args, .. }) => {
316                args.iter().any(|t| t.value.has_named_lifetime(name))
317            }
318            Type::Tuple(TypeTuple(group)) => group
319                .content
320                .iter()
321                .any(|t| t.value.has_named_lifetime(name)),
322            Type::Path(_) => false,
323        }
324    }
325
326    /// Check if type contains any named lifetime other than `name`.
327    pub fn has_non_named_lifetime(&self, name: &str) -> bool {
328        match self {
329            Type::Reference(TypeRef {
330                lifetime: Some(lifetime),
331                ..
332            }) => lifetime.second != name,
333            Type::Reference(TypeRef { inner, .. }) => inner.has_non_named_lifetime(name),
334            Type::PathWithGenerics(PathWithGenerics { args, .. }) => {
335                args.iter().any(|t| t.value.has_non_named_lifetime(name))
336            }
337            Type::Tuple(TypeTuple(group)) => group
338                .content
339                .iter()
340                .any(|t| t.value.has_non_named_lifetime(name)),
341            Type::Path(_) => false,
342        }
343    }
344
345    /// Check if type contains any `&T` reference without an explicit lifetime.
346    ///
347    /// We require explicit `'vox` for borrowed RPC return payloads.
348    pub fn has_elided_reference_lifetime(&self) -> bool {
349        match self {
350            Type::Reference(TypeRef { lifetime: None, .. }) => true,
351            Type::Reference(TypeRef { inner, .. }) => inner.has_elided_reference_lifetime(),
352            Type::PathWithGenerics(PathWithGenerics { args, .. }) => {
353                args.iter().any(|t| t.value.has_elided_reference_lifetime())
354            }
355            Type::Tuple(TypeTuple(group)) => group
356                .content
357                .iter()
358                .any(|t| t.value.has_elided_reference_lifetime()),
359            Type::Path(_) => false,
360        }
361    }
362
363    /// Check if type contains Tx or Rx at any nesting level
364    ///
365    /// Note: This is a heuristic based on type names. Proper validation should
366    /// happen at codegen time when we can resolve types properly.
367    pub fn contains_channel(&self) -> bool {
368        match self {
369            Type::Reference(TypeRef { inner, .. }) => inner.contains_channel(),
370            Type::Tuple(TypeTuple(group)) => {
371                group.content.iter().any(|t| t.value.contains_channel())
372            }
373            Type::PathWithGenerics(PathWithGenerics { path, args, .. }) => {
374                let seg = path.last_segment();
375                if seg == "Tx" || seg == "Rx" {
376                    return true;
377                }
378                args.iter().any(|t| t.value.contains_channel())
379            }
380            Type::Path(path) => {
381                let seg = path.last_segment();
382                seg == "Tx" || seg == "Rx"
383            }
384        }
385    }
386}
387
388// ============================================================================
389// Helper methods for TypePath
390// ============================================================================
391
392impl TypePath {
393    /// Get the last segment (e.g., "Result" from "std::result::Result")
394    pub fn last_segment(&self) -> String {
395        self.rest
396            .iter()
397            .last()
398            .map(|seg| seg.value.second.to_string())
399            .unwrap_or_else(|| self.first.to_string())
400    }
401}
402
403// ============================================================================
404// Helper methods for ServiceTrait
405// ============================================================================
406
407impl ServiceTrait {
408    /// Get the trait name as a string.
409    pub fn name(&self) -> String {
410        self.name.to_string()
411    }
412
413    /// Get the trait's doc string (collected from #[doc = "..."] attributes).
414    pub fn doc(&self) -> Option<String> {
415        collect_doc_string(&self.attributes)
416    }
417
418    /// Get an iterator over the methods.
419    pub fn methods(&self) -> impl Iterator<Item = &ServiceMethod> {
420        self.body.content.iter().map(|entry| &entry.value)
421    }
422}
423
424// ============================================================================
425// Helper methods for ServiceMethod
426// ============================================================================
427
428impl ServiceMethod {
429    /// Get the method name as a string.
430    pub fn name(&self) -> String {
431        self.name.to_string()
432    }
433
434    /// Get the method's doc string (collected from #[doc = "..."] attributes).
435    pub fn doc(&self) -> Option<String> {
436        collect_doc_string(&self.attributes)
437    }
438
439    /// Get an iterator over the method's parameters (excluding &self).
440    pub fn args(&self) -> impl Iterator<Item = &MethodParam> {
441        self.params
442            .content
443            .rest
444            .iter()
445            .flat_map(|rest| rest.value.second.iter().map(|entry| &entry.value))
446    }
447
448    /// Get the return type, defaulting to () if not specified.
449    pub fn return_type(&self) -> Type {
450        self.return_type
451            .iter()
452            .next()
453            .map(|r| r.value.ty.clone())
454            .unwrap_or_else(unit_type)
455    }
456
457    pub fn receiver_kind(&self) -> ReceiverKind {
458        match &self.params.content.receiver {
459            MethodReceiver::Ref(RefSelf {
460                mutability: Some(_),
461                ..
462            }) => ReceiverKind::RefMutSelf,
463            MethodReceiver::Ref(_) => ReceiverKind::RefSelf,
464            MethodReceiver::Value(ValueSelf {
465                mutability: Some(_),
466                ..
467            }) => ReceiverKind::MutSelfValue,
468            MethodReceiver::Value(_) => ReceiverKind::SelfValue,
469            MethodReceiver::Typed(TypedSelf {
470                mutability: Some(_),
471                ..
472            }) => ReceiverKind::MutTypedSelf,
473            MethodReceiver::Typed(_) => ReceiverKind::TypedSelf,
474        }
475    }
476
477    /// Check if receiver is &mut self (not allowed for service methods).
478    pub fn is_mut_receiver(&self) -> bool {
479        matches!(
480            self.receiver_kind(),
481            ReceiverKind::RefMutSelf | ReceiverKind::MutSelfValue
482        )
483    }
484
485    /// Check if method has generics.
486    pub fn has_generics(&self) -> bool {
487        !self.generics.is_empty()
488    }
489
490    /// Check if method is declared with the async keyword.
491    pub fn is_async(&self) -> bool {
492        !self._async.is_empty()
493    }
494
495    /// Check whether this method explicitly opts into request context injection.
496    pub fn wants_context(&self) -> bool {
497        has_attr_path(&self.attributes, &["vox", "context"])
498    }
499
500    /// Check whether this method explicitly declares rerun-safe semantics.
501    pub fn is_idem(&self) -> bool {
502        has_attr_helper(&self.attributes, &["vox"], "idem")
503    }
504
505    /// Check whether this method explicitly declares persistent admission.
506    pub fn is_persist(&self) -> bool {
507        has_attr_helper(&self.attributes, &["vox"], "persist")
508    }
509}
510
511#[derive(Debug, Clone, Copy, PartialEq, Eq)]
512pub enum ReceiverKind {
513    RefSelf,
514    RefMutSelf,
515    SelfValue,
516    MutSelfValue,
517    TypedSelf,
518    MutTypedSelf,
519}
520
521// ============================================================================
522// Helper methods for MethodParam
523// ============================================================================
524
525impl MethodParam {
526    /// Get the parameter name as a string.
527    pub fn name(&self) -> String {
528        self.name.to_string()
529    }
530}
531
532// ============================================================================
533// Helper functions
534// ============================================================================
535
536/// Extract Ok and Err types from a return type.
537/// Returns (ok_type, Some(err_type)) for Result<T, E>, or (type, None) otherwise.
538pub fn method_ok_and_err_types(return_ty: &Type) -> (&Type, Option<&Type>) {
539    if let Some((ok, err)) = return_ty.as_result() {
540        (ok, Some(err))
541    } else {
542        (return_ty, None)
543    }
544}
545
546/// Returns the unit type `()`.
547fn unit_type() -> Type {
548    let mut iter = "()".to_token_iter();
549    Type::parse(&mut iter).expect("unit type should always parse")
550}
551
552/// Collect doc strings from attributes.
553fn collect_doc_string(attrs: &Any<RawAttribute>) -> Option<String> {
554    let mut docs = Vec::new();
555
556    for attr in attrs.iter() {
557        let mut body_iter = attr.value.body.content.clone().to_token_iter();
558        if let Ok(doc_attr) = DocAttribute::parse(&mut body_iter) {
559            let line = doc_attr
560                .value
561                .as_str()
562                .replace("\\\"", "\"")
563                .replace("\\'", "'");
564            docs.push(line);
565        }
566    }
567
568    if docs.is_empty() {
569        None
570    } else {
571        Some(docs.join("\n"))
572    }
573}
574
575fn has_attr_path(attrs: &Any<RawAttribute>, expected: &[&str]) -> bool {
576    attrs
577        .iter()
578        .any(|attr| attr_path_matches(&attr.value, expected))
579}
580
581fn has_attr_helper(attrs: &Any<RawAttribute>, path: &[&str], helper: &str) -> bool {
582    attrs
583        .iter()
584        .any(|attr| attr_helper_matches(&attr.value, path, helper))
585}
586
587fn attr_path_matches(attr: &RawAttribute, expected: &[&str]) -> bool {
588    let mut iter = attr.body.content.clone().to_token_iter();
589    let Ok(path) = TypePath::parse(&mut iter) else {
590        return false;
591    };
592    if EndOfStream::parse(&mut iter).is_err() {
593        return false;
594    }
595    path_matches(&path, expected)
596}
597
598fn attr_helper_matches(attr: &RawAttribute, expected_path: &[&str], expected_helper: &str) -> bool {
599    let mut iter = attr.body.content.clone().to_token_iter();
600    let Ok(path) = TypePath::parse(&mut iter) else {
601        return false;
602    };
603    if !path_matches(&path, expected_path) {
604        return false;
605    }
606
607    let Ok(group) = ParenthesisGroupContaining::<TokenStream>::parse(&mut iter) else {
608        return false;
609    };
610    if EndOfStream::parse(&mut iter).is_err() {
611        return false;
612    }
613
614    let mut inner = group.content.to_token_iter();
615    let Ok(helper) = Ident::parse(&mut inner) else {
616        return false;
617    };
618    if EndOfStream::parse(&mut inner).is_err() {
619        return false;
620    }
621    helper == expected_helper
622}
623
624fn path_matches(path: &TypePath, expected: &[&str]) -> bool {
625    let actual = std::iter::once(path.first.to_string())
626        .chain(path.rest.iter().map(|seg| seg.value.second.to_string()))
627        .collect::<Vec<_>>();
628
629    actual.len() == expected.len()
630        && actual
631            .iter()
632            .zip(expected.iter())
633            .all(|(actual, expected)| actual == expected)
634}
635
636/// Parse a trait definition from a token stream.
637#[allow(clippy::result_large_err)] // unsynn::Error is external, we can't box it
638pub fn parse_trait(tokens: &TokenStream2) -> Result<ServiceTrait, unsynn::Error> {
639    let mut iter = tokens.clone().to_token_iter();
640    ServiceTrait::parse(&mut iter)
641}
642
643#[cfg(test)]
644mod tests {
645    use super::*;
646
647    fn parse(src: &str) -> ServiceTrait {
648        let ts: TokenStream2 = src.parse().expect("tokenstream parse");
649        parse_trait(&ts).expect("trait parse")
650    }
651
652    #[test]
653    fn parse_trait_exposes_docs_methods_and_args() {
654        let trait_def = parse(
655            r#"
656            #[doc = "Calculator service."]
657            pub trait Calculator {
658                #[doc = "Adds two numbers."]
659                async fn add(&self, a: i32, b: i32) -> Result<i64, String>;
660            }
661            "#,
662        );
663
664        assert_eq!(trait_def.name(), "Calculator");
665        assert_eq!(trait_def.doc(), Some("Calculator service.".to_string()));
666
667        let method = trait_def.methods().next().expect("method");
668        assert_eq!(method.name(), "add");
669        assert_eq!(method.doc(), Some("Adds two numbers.".to_string()));
670        assert_eq!(
671            method.args().map(|arg| arg.name()).collect::<Vec<_>>(),
672            vec!["a", "b"]
673        );
674
675        let ret = method.return_type();
676        let (ok, err) = method_ok_and_err_types(&ret);
677        assert!(ok.as_result().is_none());
678        assert!(err.is_some());
679    }
680
681    #[test]
682    fn return_type_defaults_to_unit_when_omitted() {
683        let trait_def = parse(
684            r#"
685            trait Svc {
686                async fn ping(&self);
687            }
688            "#,
689        );
690        let method = trait_def.methods().next().expect("method");
691        let ret = method.return_type();
692        match ret {
693            Type::Tuple(TypeTuple(group)) => assert!(group.content.is_empty()),
694            other => panic!(
695                "expected unit tuple return, got {}",
696                other.to_token_stream()
697            ),
698        }
699    }
700
701    #[test]
702    fn method_helpers_detect_generics_and_mut_receiver() {
703        let trait_def = parse(
704            r#"
705            trait Svc {
706                async fn bad<T>(&mut self, value: T) -> T;
707            }
708            "#,
709        );
710        let method = trait_def.methods().next().expect("method");
711        assert!(method.has_generics());
712        assert!(method.is_mut_receiver());
713    }
714
715    #[test]
716    fn method_helpers_detect_async_keyword_presence() {
717        let trait_def = parse(
718            r#"
719            trait Svc {
720                fn plain(&self) -> u32;
721                async fn async_one(&self) -> u32;
722            }
723            "#,
724        );
725        let mut methods = trait_def.methods();
726        assert!(!methods.next().expect("plain method").is_async());
727        assert!(methods.next().expect("async method").is_async());
728    }
729
730    #[test]
731    fn method_helpers_detect_receiver_kinds() {
732        let trait_def = parse(
733            r#"
734            trait Svc {
735                async fn by_ref(&self) -> u32;
736                async fn by_ref_lifetime(&'a self) -> u32;
737                async fn by_mut_ref(&mut self) -> u32;
738                async fn by_mut_ref_lifetime(&'a mut self) -> u32;
739                async fn by_value(self) -> u32;
740                async fn by_mut_value(mut self) -> u32;
741                async fn by_typed(self: Box<Self>) -> u32;
742                async fn by_mut_typed(mut self: Box<Self>) -> u32;
743            }
744            "#,
745        );
746        let mut methods = trait_def.methods();
747        assert_eq!(
748            methods.next().expect("by_ref").receiver_kind(),
749            ReceiverKind::RefSelf
750        );
751        assert_eq!(
752            methods.next().expect("by_ref_lifetime").receiver_kind(),
753            ReceiverKind::RefSelf
754        );
755        assert_eq!(
756            methods.next().expect("by_mut_ref").receiver_kind(),
757            ReceiverKind::RefMutSelf
758        );
759        assert_eq!(
760            methods.next().expect("by_mut_ref_lifetime").receiver_kind(),
761            ReceiverKind::RefMutSelf
762        );
763        assert_eq!(
764            methods.next().expect("by_value").receiver_kind(),
765            ReceiverKind::SelfValue
766        );
767        assert_eq!(
768            methods.next().expect("by_mut_value").receiver_kind(),
769            ReceiverKind::MutSelfValue
770        );
771        assert_eq!(
772            methods.next().expect("by_typed").receiver_kind(),
773            ReceiverKind::TypedSelf
774        );
775        assert_eq!(
776            methods.next().expect("by_mut_typed").receiver_kind(),
777            ReceiverKind::MutTypedSelf
778        );
779    }
780
781    #[test]
782    fn method_helpers_detect_explicit_request_context_opt_in() {
783        let trait_def = parse(
784            r#"
785            trait Svc {
786                #[vox::context]
787                async fn contextual(&self) -> u32;
788
789                async fn plain(&self) -> u32;
790            }
791            "#,
792        );
793        let mut methods = trait_def.methods();
794        assert!(methods.next().expect("contextual method").wants_context());
795        assert!(!methods.next().expect("plain method").wants_context());
796    }
797
798    #[test]
799    fn method_helpers_detect_retry_helper_attributes() {
800        let trait_def = parse(
801            r#"
802            trait Svc {
803                #[vox(idem)]
804                async fn cached(&self) -> u32;
805
806                #[vox(persist)]
807                async fn durable(&self) -> u32;
808
809                async fn plain(&self) -> u32;
810            }
811            "#,
812        );
813        let mut methods = trait_def.methods();
814        let cached = methods.next().expect("cached");
815        assert!(cached.is_idem());
816        assert!(!cached.is_persist());
817
818        let durable = methods.next().expect("durable");
819        assert!(!durable.is_idem());
820        assert!(durable.is_persist());
821
822        let plain = methods.next().expect("plain");
823        assert!(!plain.is_idem());
824        assert!(!plain.is_persist());
825    }
826
827    #[test]
828    fn type_helpers_detect_result_lifetime_and_channel_nesting() {
829        let trait_def = parse(
830            r#"
831            trait Svc {
832                async fn stream(&self, input: &'static str) -> Result<Option<Tx<Vec<u8>>>, Rx<u32>>;
833            }
834            "#,
835        );
836        let method = trait_def.methods().next().expect("method");
837        let arg = method.args().next().expect("arg");
838        assert!(arg.ty.has_lifetime());
839        assert!(!arg.ty.contains_channel());
840
841        let ret = method.return_type();
842        let (ok, err) = method_ok_and_err_types(&ret);
843        assert!(ok.contains_channel());
844        assert!(err.expect("result err type").contains_channel());
845    }
846
847    #[test]
848    fn type_helpers_detect_named_and_elided_lifetimes() {
849        let trait_def = parse(
850            r#"
851            trait Svc {
852                async fn borrowed(&self) -> Result<&'vox str, Error>;
853                async fn bad_lifetime(&self) -> Result<&'a str, Error>;
854                async fn elided(&self) -> Result<&str, Error>;
855            }
856            "#,
857        );
858        let mut methods = trait_def.methods();
859
860        let borrowed = methods.next().expect("borrowed method").return_type();
861        let (borrowed_ok, _) = method_ok_and_err_types(&borrowed);
862        assert!(borrowed_ok.has_named_lifetime("vox"));
863        assert!(!borrowed_ok.has_non_named_lifetime("vox"));
864        assert!(!borrowed_ok.has_elided_reference_lifetime());
865
866        let bad_lifetime = methods.next().expect("bad_lifetime method").return_type();
867        let (bad_ok, _) = method_ok_and_err_types(&bad_lifetime);
868        assert!(!bad_ok.has_named_lifetime("vox"));
869        assert!(bad_ok.has_non_named_lifetime("vox"));
870        assert!(!bad_ok.has_elided_reference_lifetime());
871
872        let elided = methods.next().expect("elided method").return_type();
873        let (elided_ok, _) = method_ok_and_err_types(&elided);
874        assert!(!elided_ok.has_named_lifetime("vox"));
875        assert!(!elided_ok.has_non_named_lifetime("vox"));
876        assert!(elided_ok.has_elided_reference_lifetime());
877    }
878
879    #[test]
880    fn type_path_last_segment_uses_trailing_segment() {
881        let trait_def = parse(
882            r#"
883            trait Svc {
884                async fn f(&self) -> std::result::Result<u8, u8>;
885            }
886            "#,
887        );
888        let method = trait_def.methods().next().expect("method");
889        let ret = method.return_type();
890        let Type::PathWithGenerics(path_with_generics) = ret else {
891            panic!("expected path with generics");
892        };
893        assert_eq!(path_with_generics.path.last_segment(), "Result");
894    }
895}