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 mutability: Option<KMut>,
110        pub name: KSelfKw,
111    }
112
113    pub struct MethodParam {
114        pub name: Ident,
115        pub _colon: Colon,
116        pub ty: Type,
117    }
118
119    pub struct GenericParams {
120        pub _lt: Lt,
121        pub params: VerbatimUntil<Gt>,
122        pub _gt: Gt,
123    }
124
125    #[derive(Clone)]
126    pub struct TypePath {
127        pub leading: Option<PathSep>,
128        pub first: Ident,
129        pub rest: Any<Cons<PathSep, Ident>>,
130    }
131
132    #[derive(Clone)]
133    pub struct Lifetime {
134        pub _apo: LifetimeTick,
135        pub ident: Ident,
136    }
137
138    #[derive(Clone)]
139    pub enum GenericArgument {
140        Lifetime(Lifetime),
141        Type(Type),
142    }
143
144    #[derive(Clone)]
145    pub enum Type {
146        Reference(TypeRef),
147        Tuple(TypeTuple),
148        PathWithGenerics(PathWithGenerics),
149        Path(TypePath),
150    }
151
152    #[derive(Clone)]
153    pub struct TypeRef {
154        pub _amp: unsynn::operator::names::And,
155        pub lifetime: Option<Cons<LifetimeTick, Ident>>,
156        pub mutable: Option<KMut>,
157        pub inner: Box<Type>,
158    }
159
160    #[derive(Clone)]
161    pub struct TypeTuple(
162        pub ParenthesisGroupContaining<CommaDelimitedVec<Type>>,
163    );
164
165    #[derive(Clone)]
166    pub struct PathWithGenerics {
167        pub path: TypePath,
168        pub _lt: Lt,
169        pub args: CommaDelimitedVec<GenericArgument>,
170        pub _gt: Gt,
171    }
172
173    pub struct ReturnType {
174        pub _arrow: RArrow,
175        pub ty: Type,
176    }
177
178    pub struct WhereClause {
179        pub _where: KWhere,
180        pub bounds: VerbatimUntil<Semicolon>,
181    }
182
183    pub struct MethodParams {
184        pub receiver: RefSelf,
185        pub rest: Optional<Cons<Comma, CommaDelimitedVec<MethodParam>>>,
186    }
187
188    pub struct ServiceMethod {
189        pub attributes: Any<RawAttribute>,
190        pub _async: KAsync,
191        pub _fn: KFn,
192        pub name: Ident,
193        pub generics: Optional<GenericParams>,
194        pub params: ParenthesisGroupContaining<MethodParams>,
195        pub return_type: Optional<ReturnType>,
196        pub where_clause: Optional<WhereClause>,
197        pub _semi: Semicolon,
198    }
199
200    pub struct ServiceTrait {
201        pub attributes: Any<RawAttribute>,
202        pub vis: Optional<Visibility>,
203        pub _trait: KTrait,
204        pub name: Ident,
205        pub generics: Optional<GenericParams>,
206        pub body: BraceGroupContaining<Any<ServiceMethod>>,
207        pub _eos: EndOfStream,
208    }
209}
210
211// ============================================================================
212// Helper methods for GenericArgument
213// ============================================================================
214
215impl GenericArgument {
216    pub fn has_lifetime(&self) -> bool {
217        match self {
218            GenericArgument::Lifetime(_) => true,
219            GenericArgument::Type(ty) => ty.has_lifetime(),
220        }
221    }
222
223    pub fn has_named_lifetime(&self, name: &str) -> bool {
224        match self {
225            GenericArgument::Lifetime(lifetime) => lifetime.ident == name,
226            GenericArgument::Type(ty) => ty.has_named_lifetime(name),
227        }
228    }
229
230    pub fn has_non_named_lifetime(&self, name: &str) -> bool {
231        match self {
232            GenericArgument::Lifetime(lifetime) => lifetime.ident != name,
233            GenericArgument::Type(ty) => ty.has_non_named_lifetime(name),
234        }
235    }
236
237    pub fn has_elided_reference_lifetime(&self) -> bool {
238        match self {
239            GenericArgument::Lifetime(_) => false,
240            GenericArgument::Type(ty) => ty.has_elided_reference_lifetime(),
241        }
242    }
243
244    pub fn contains_channel(&self) -> bool {
245        match self {
246            GenericArgument::Lifetime(_) => false,
247            GenericArgument::Type(ty) => ty.contains_channel(),
248        }
249    }
250}
251
252// ============================================================================
253// Helper methods for Type
254// ============================================================================
255
256impl Type {
257    /// Extract Ok and Err types if this is Result<T, E>
258    pub fn as_result(&self) -> Option<(&Type, &Type)> {
259        match self {
260            Type::PathWithGenerics(PathWithGenerics { path, args, .. })
261                if path.last_segment().as_str() == "Result" && args.len() == 2 =>
262            {
263                let args_slice = args.as_slice();
264                match (&args_slice[0].value, &args_slice[1].value) {
265                    (GenericArgument::Type(ok), GenericArgument::Type(err)) => Some((ok, err)),
266                    _ => None,
267                }
268            }
269            _ => None,
270        }
271    }
272
273    /// Check if type contains a lifetime anywhere in the tree
274    pub fn has_lifetime(&self) -> bool {
275        match self {
276            Type::Reference(TypeRef {
277                lifetime: Some(_), ..
278            }) => true,
279            Type::Reference(TypeRef { inner, .. }) => inner.has_lifetime(),
280            Type::PathWithGenerics(PathWithGenerics { args, .. }) => {
281                args.iter().any(|t| t.value.has_lifetime())
282            }
283            Type::Tuple(TypeTuple(group)) => group.content.iter().any(|t| t.value.has_lifetime()),
284            Type::Path(_) => false,
285        }
286    }
287
288    /// Check if type contains the named lifetime anywhere in the tree.
289    pub fn has_named_lifetime(&self, name: &str) -> bool {
290        match self {
291            Type::Reference(TypeRef {
292                lifetime: Some(lifetime),
293                ..
294            }) => lifetime.second == name,
295            Type::Reference(TypeRef { inner, .. }) => inner.has_named_lifetime(name),
296            Type::PathWithGenerics(PathWithGenerics { args, .. }) => {
297                args.iter().any(|t| t.value.has_named_lifetime(name))
298            }
299            Type::Tuple(TypeTuple(group)) => group
300                .content
301                .iter()
302                .any(|t| t.value.has_named_lifetime(name)),
303            Type::Path(_) => false,
304        }
305    }
306
307    /// Check if type contains any named lifetime other than `name`.
308    pub fn has_non_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_non_named_lifetime(name),
315            Type::PathWithGenerics(PathWithGenerics { args, .. }) => {
316                args.iter().any(|t| t.value.has_non_named_lifetime(name))
317            }
318            Type::Tuple(TypeTuple(group)) => group
319                .content
320                .iter()
321                .any(|t| t.value.has_non_named_lifetime(name)),
322            Type::Path(_) => false,
323        }
324    }
325
326    /// Check if type contains any `&T` reference without an explicit lifetime.
327    ///
328    /// We require explicit `'vox` for borrowed RPC return payloads.
329    pub fn has_elided_reference_lifetime(&self) -> bool {
330        match self {
331            Type::Reference(TypeRef { lifetime: None, .. }) => true,
332            Type::Reference(TypeRef { inner, .. }) => inner.has_elided_reference_lifetime(),
333            Type::PathWithGenerics(PathWithGenerics { args, .. }) => {
334                args.iter().any(|t| t.value.has_elided_reference_lifetime())
335            }
336            Type::Tuple(TypeTuple(group)) => group
337                .content
338                .iter()
339                .any(|t| t.value.has_elided_reference_lifetime()),
340            Type::Path(_) => false,
341        }
342    }
343
344    /// Check if type contains Tx or Rx at any nesting level
345    ///
346    /// Note: This is a heuristic based on type names. Proper validation should
347    /// happen at codegen time when we can resolve types properly.
348    pub fn contains_channel(&self) -> bool {
349        match self {
350            Type::Reference(TypeRef { inner, .. }) => inner.contains_channel(),
351            Type::Tuple(TypeTuple(group)) => {
352                group.content.iter().any(|t| t.value.contains_channel())
353            }
354            Type::PathWithGenerics(PathWithGenerics { path, args, .. }) => {
355                let seg = path.last_segment();
356                if seg == "Tx" || seg == "Rx" {
357                    return true;
358                }
359                args.iter().any(|t| t.value.contains_channel())
360            }
361            Type::Path(path) => {
362                let seg = path.last_segment();
363                seg == "Tx" || seg == "Rx"
364            }
365        }
366    }
367}
368
369// ============================================================================
370// Helper methods for TypePath
371// ============================================================================
372
373impl TypePath {
374    /// Get the last segment (e.g., "Result" from "std::result::Result")
375    pub fn last_segment(&self) -> String {
376        self.rest
377            .iter()
378            .last()
379            .map(|seg| seg.value.second.to_string())
380            .unwrap_or_else(|| self.first.to_string())
381    }
382}
383
384// ============================================================================
385// Helper methods for ServiceTrait
386// ============================================================================
387
388impl ServiceTrait {
389    /// Get the trait name as a string.
390    pub fn name(&self) -> String {
391        self.name.to_string()
392    }
393
394    /// Get the trait's doc string (collected from #[doc = "..."] attributes).
395    pub fn doc(&self) -> Option<String> {
396        collect_doc_string(&self.attributes)
397    }
398
399    /// Get an iterator over the methods.
400    pub fn methods(&self) -> impl Iterator<Item = &ServiceMethod> {
401        self.body.content.iter().map(|entry| &entry.value)
402    }
403}
404
405// ============================================================================
406// Helper methods for ServiceMethod
407// ============================================================================
408
409impl ServiceMethod {
410    /// Get the method name as a string.
411    pub fn name(&self) -> String {
412        self.name.to_string()
413    }
414
415    /// Get the method's doc string (collected from #[doc = "..."] attributes).
416    pub fn doc(&self) -> Option<String> {
417        collect_doc_string(&self.attributes)
418    }
419
420    /// Get an iterator over the method's parameters (excluding &self).
421    pub fn args(&self) -> impl Iterator<Item = &MethodParam> {
422        self.params
423            .content
424            .rest
425            .iter()
426            .flat_map(|rest| rest.value.second.iter().map(|entry| &entry.value))
427    }
428
429    /// Get the return type, defaulting to () if not specified.
430    pub fn return_type(&self) -> Type {
431        self.return_type
432            .iter()
433            .next()
434            .map(|r| r.value.ty.clone())
435            .unwrap_or_else(unit_type)
436    }
437
438    /// Check if receiver is &mut self (not allowed for service methods).
439    pub fn is_mut_receiver(&self) -> bool {
440        self.params.content.receiver.mutability.is_some()
441    }
442
443    /// Check if method has generics.
444    pub fn has_generics(&self) -> bool {
445        !self.generics.is_empty()
446    }
447
448    /// Check whether this method explicitly opts into request context injection.
449    pub fn wants_context(&self) -> bool {
450        has_attr_path(&self.attributes, &["vox", "context"])
451    }
452
453    /// Check whether this method explicitly declares rerun-safe semantics.
454    pub fn is_idem(&self) -> bool {
455        has_attr_helper(&self.attributes, &["vox"], "idem")
456    }
457
458    /// Check whether this method explicitly declares persistent admission.
459    pub fn is_persist(&self) -> bool {
460        has_attr_helper(&self.attributes, &["vox"], "persist")
461    }
462}
463
464// ============================================================================
465// Helper methods for MethodParam
466// ============================================================================
467
468impl MethodParam {
469    /// Get the parameter name as a string.
470    pub fn name(&self) -> String {
471        self.name.to_string()
472    }
473}
474
475// ============================================================================
476// Helper functions
477// ============================================================================
478
479/// Extract Ok and Err types from a return type.
480/// Returns (ok_type, Some(err_type)) for Result<T, E>, or (type, None) otherwise.
481pub fn method_ok_and_err_types(return_ty: &Type) -> (&Type, Option<&Type>) {
482    if let Some((ok, err)) = return_ty.as_result() {
483        (ok, Some(err))
484    } else {
485        (return_ty, None)
486    }
487}
488
489/// Returns the unit type `()`.
490fn unit_type() -> Type {
491    let mut iter = "()".to_token_iter();
492    Type::parse(&mut iter).expect("unit type should always parse")
493}
494
495/// Collect doc strings from attributes.
496fn collect_doc_string(attrs: &Any<RawAttribute>) -> Option<String> {
497    let mut docs = Vec::new();
498
499    for attr in attrs.iter() {
500        let mut body_iter = attr.value.body.content.clone().to_token_iter();
501        if let Ok(doc_attr) = DocAttribute::parse(&mut body_iter) {
502            let line = doc_attr
503                .value
504                .as_str()
505                .replace("\\\"", "\"")
506                .replace("\\'", "'");
507            docs.push(line);
508        }
509    }
510
511    if docs.is_empty() {
512        None
513    } else {
514        Some(docs.join("\n"))
515    }
516}
517
518fn has_attr_path(attrs: &Any<RawAttribute>, expected: &[&str]) -> bool {
519    attrs
520        .iter()
521        .any(|attr| attr_path_matches(&attr.value, expected))
522}
523
524fn has_attr_helper(attrs: &Any<RawAttribute>, path: &[&str], helper: &str) -> bool {
525    attrs
526        .iter()
527        .any(|attr| attr_helper_matches(&attr.value, path, helper))
528}
529
530fn attr_path_matches(attr: &RawAttribute, expected: &[&str]) -> bool {
531    let mut iter = attr.body.content.clone().to_token_iter();
532    let Ok(path) = TypePath::parse(&mut iter) else {
533        return false;
534    };
535    if EndOfStream::parse(&mut iter).is_err() {
536        return false;
537    }
538    path_matches(&path, expected)
539}
540
541fn attr_helper_matches(attr: &RawAttribute, expected_path: &[&str], expected_helper: &str) -> bool {
542    let mut iter = attr.body.content.clone().to_token_iter();
543    let Ok(path) = TypePath::parse(&mut iter) else {
544        return false;
545    };
546    if !path_matches(&path, expected_path) {
547        return false;
548    }
549
550    let Ok(group) = ParenthesisGroupContaining::<TokenStream>::parse(&mut iter) else {
551        return false;
552    };
553    if EndOfStream::parse(&mut iter).is_err() {
554        return false;
555    }
556
557    let mut inner = group.content.to_token_iter();
558    let Ok(helper) = Ident::parse(&mut inner) else {
559        return false;
560    };
561    if EndOfStream::parse(&mut inner).is_err() {
562        return false;
563    }
564    helper == expected_helper
565}
566
567fn path_matches(path: &TypePath, expected: &[&str]) -> bool {
568    let actual = std::iter::once(path.first.to_string())
569        .chain(path.rest.iter().map(|seg| seg.value.second.to_string()))
570        .collect::<Vec<_>>();
571
572    actual.len() == expected.len()
573        && actual
574            .iter()
575            .zip(expected.iter())
576            .all(|(actual, expected)| actual == expected)
577}
578
579/// Parse a trait definition from a token stream.
580#[allow(clippy::result_large_err)] // unsynn::Error is external, we can't box it
581pub fn parse_trait(tokens: &TokenStream2) -> Result<ServiceTrait, unsynn::Error> {
582    let mut iter = tokens.clone().to_token_iter();
583    ServiceTrait::parse(&mut iter)
584}
585
586#[cfg(test)]
587mod tests {
588    use super::*;
589
590    fn parse(src: &str) -> ServiceTrait {
591        let ts: TokenStream2 = src.parse().expect("tokenstream parse");
592        parse_trait(&ts).expect("trait parse")
593    }
594
595    #[test]
596    fn parse_trait_exposes_docs_methods_and_args() {
597        let trait_def = parse(
598            r#"
599            #[doc = "Calculator service."]
600            pub trait Calculator {
601                #[doc = "Adds two numbers."]
602                async fn add(&self, a: i32, b: i32) -> Result<i64, String>;
603            }
604            "#,
605        );
606
607        assert_eq!(trait_def.name(), "Calculator");
608        assert_eq!(trait_def.doc(), Some("Calculator service.".to_string()));
609
610        let method = trait_def.methods().next().expect("method");
611        assert_eq!(method.name(), "add");
612        assert_eq!(method.doc(), Some("Adds two numbers.".to_string()));
613        assert_eq!(
614            method.args().map(|arg| arg.name()).collect::<Vec<_>>(),
615            vec!["a", "b"]
616        );
617
618        let ret = method.return_type();
619        let (ok, err) = method_ok_and_err_types(&ret);
620        assert!(ok.as_result().is_none());
621        assert!(err.is_some());
622    }
623
624    #[test]
625    fn return_type_defaults_to_unit_when_omitted() {
626        let trait_def = parse(
627            r#"
628            trait Svc {
629                async fn ping(&self);
630            }
631            "#,
632        );
633        let method = trait_def.methods().next().expect("method");
634        let ret = method.return_type();
635        match ret {
636            Type::Tuple(TypeTuple(group)) => assert!(group.content.is_empty()),
637            other => panic!(
638                "expected unit tuple return, got {}",
639                other.to_token_stream()
640            ),
641        }
642    }
643
644    #[test]
645    fn method_helpers_detect_generics_and_mut_receiver() {
646        let trait_def = parse(
647            r#"
648            trait Svc {
649                async fn bad<T>(&mut self, value: T) -> T;
650            }
651            "#,
652        );
653        let method = trait_def.methods().next().expect("method");
654        assert!(method.has_generics());
655        assert!(method.is_mut_receiver());
656    }
657
658    #[test]
659    fn method_helpers_detect_explicit_request_context_opt_in() {
660        let trait_def = parse(
661            r#"
662            trait Svc {
663                #[vox::context]
664                async fn contextual(&self) -> u32;
665
666                async fn plain(&self) -> u32;
667            }
668            "#,
669        );
670        let mut methods = trait_def.methods();
671        assert!(methods.next().expect("contextual method").wants_context());
672        assert!(!methods.next().expect("plain method").wants_context());
673    }
674
675    #[test]
676    fn method_helpers_detect_retry_helper_attributes() {
677        let trait_def = parse(
678            r#"
679            trait Svc {
680                #[vox(idem)]
681                async fn cached(&self) -> u32;
682
683                #[vox(persist)]
684                async fn durable(&self) -> u32;
685
686                async fn plain(&self) -> u32;
687            }
688            "#,
689        );
690        let mut methods = trait_def.methods();
691        let cached = methods.next().expect("cached");
692        assert!(cached.is_idem());
693        assert!(!cached.is_persist());
694
695        let durable = methods.next().expect("durable");
696        assert!(!durable.is_idem());
697        assert!(durable.is_persist());
698
699        let plain = methods.next().expect("plain");
700        assert!(!plain.is_idem());
701        assert!(!plain.is_persist());
702    }
703
704    #[test]
705    fn type_helpers_detect_result_lifetime_and_channel_nesting() {
706        let trait_def = parse(
707            r#"
708            trait Svc {
709                async fn stream(&self, input: &'static str) -> Result<Option<Tx<Vec<u8>>>, Rx<u32>>;
710            }
711            "#,
712        );
713        let method = trait_def.methods().next().expect("method");
714        let arg = method.args().next().expect("arg");
715        assert!(arg.ty.has_lifetime());
716        assert!(!arg.ty.contains_channel());
717
718        let ret = method.return_type();
719        let (ok, err) = method_ok_and_err_types(&ret);
720        assert!(ok.contains_channel());
721        assert!(err.expect("result err type").contains_channel());
722    }
723
724    #[test]
725    fn type_helpers_detect_named_and_elided_lifetimes() {
726        let trait_def = parse(
727            r#"
728            trait Svc {
729                async fn borrowed(&self) -> Result<&'vox str, Error>;
730                async fn bad_lifetime(&self) -> Result<&'a str, Error>;
731                async fn elided(&self) -> Result<&str, Error>;
732            }
733            "#,
734        );
735        let mut methods = trait_def.methods();
736
737        let borrowed = methods.next().expect("borrowed method").return_type();
738        let (borrowed_ok, _) = method_ok_and_err_types(&borrowed);
739        assert!(borrowed_ok.has_named_lifetime("vox"));
740        assert!(!borrowed_ok.has_non_named_lifetime("vox"));
741        assert!(!borrowed_ok.has_elided_reference_lifetime());
742
743        let bad_lifetime = methods.next().expect("bad_lifetime method").return_type();
744        let (bad_ok, _) = method_ok_and_err_types(&bad_lifetime);
745        assert!(!bad_ok.has_named_lifetime("vox"));
746        assert!(bad_ok.has_non_named_lifetime("vox"));
747        assert!(!bad_ok.has_elided_reference_lifetime());
748
749        let elided = methods.next().expect("elided method").return_type();
750        let (elided_ok, _) = method_ok_and_err_types(&elided);
751        assert!(!elided_ok.has_named_lifetime("vox"));
752        assert!(!elided_ok.has_non_named_lifetime("vox"));
753        assert!(elided_ok.has_elided_reference_lifetime());
754    }
755
756    #[test]
757    fn type_path_last_segment_uses_trailing_segment() {
758        let trait_def = parse(
759            r#"
760            trait Svc {
761                async fn f(&self) -> std::result::Result<u8, u8>;
762            }
763            "#,
764        );
765        let method = trait_def.methods().next().expect("method");
766        let ret = method.return_type();
767        let Type::PathWithGenerics(path_with_generics) = ret else {
768            panic!("expected path with generics");
769        };
770        assert_eq!(path_with_generics.path.last_segment(), "Result");
771    }
772}