Skip to content

Commit f05d49f

Browse files
committed
feat: parse chained types and improve errors for external types
1 parent 19b5ac3 commit f05d49f

File tree

1 file changed

+251
-50
lines changed

1 file changed

+251
-50
lines changed

soroban-sdk-macros/src/syn_ext.rs

Lines changed: 251 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -318,83 +318,107 @@ fn flatten_associated_items_in_impl_fns(imp: &mut ItemImpl) -> Result<(), Error>
318318
})
319319
.collect();
320320

321-
// Resolve Self::* in function input types and return types.
321+
// Resolve Self::* in function input types and return types, including
322+
// inside generic arguments like Vec<Self::Val>, Result<Self::Val, Error>, or &Self::Val.
323+
// Uses default 128 depth limit for types. This is a somewhat arbitrary limit if it needs
324+
// to be increased in the future.
322325
for item in imp.items.iter_mut() {
323326
if let ImplItem::Fn(f) = item {
324327
for input in f.sig.inputs.iter_mut() {
325328
if let FnArg::Typed(t) = input {
326-
if let Some(resolved) = resolve_self_type(&t.ty, &associated_types) {
327-
*t.ty = resolved;
328-
}
329+
resolve_self_types(&mut t.ty, &associated_types, 128)?;
329330
}
330331
}
331332
if let ReturnType::Type(_, ty) = &mut f.sig.output {
332-
if let Some(resolved) = resolve_self_type(ty, &associated_types) {
333-
**ty = resolved;
334-
}
333+
resolve_self_types(ty, &associated_types, 128)?;
335334
}
336335
}
337336
}
338337

339-
// Check for any remaining unresolved Self::* types in fn signatures.
340-
for item in imp.items.iter() {
341-
if let ImplItem::Fn(f) = item {
342-
for input in f.sig.inputs.iter() {
343-
if let FnArg::Typed(t) = input {
344-
if let Some(ident) = self_type_ident(&t.ty) {
345-
return Err(Error::new(
346-
t.ty.span(),
347-
format!(
348-
"unresolved associated type `Self::{ident}` in function \
349-
parameter; use a concrete type instead"
350-
),
351-
));
338+
Ok(())
339+
}
340+
341+
/// Recursively resolve `Self::Ident` types within a type, including inside
342+
/// generic arguments like `Vec<Self::Val>`, `Result<Self::Val, Error>`, or `&Self::Val`.
343+
///
344+
/// ### Errors
345+
/// If we cannot resolve the type or any unresolved `Self::Ident` remains after resolution.
346+
fn resolve_self_types(
347+
ty: &mut Type,
348+
associated_types: &HashMap<Ident, Type>,
349+
depth: usize,
350+
) -> Result<(), Error> {
351+
if depth == 0 {
352+
return Err(Error::new(
353+
ty.span(),
354+
"unable to resolve type; type depth limit exceeded",
355+
));
356+
}
357+
358+
if let Some(ident) = self_type_ident(ty)? {
359+
if let Some(resolved) = associated_types.get(ident).cloned() {
360+
*ty = resolved;
361+
return resolve_self_types(ty, associated_types, depth - 1);
362+
}
363+
return Err(Error::new(
364+
ty.span(),
365+
format!("unresolved associated type `Self::{ident}`; use a concrete type instead"),
366+
));
367+
}
368+
369+
match ty {
370+
// Reject qualified Self paths like `<Self as Trait>::Foo`.
371+
Type::Path(TypePath { qself: Some(qself), .. })
372+
if matches!(qself.ty.as_ref(), Type::Path(TypePath { qself: None, path }) if path.is_ident("Self")) =>
373+
{
374+
Err(Error::new(
375+
ty.span(),
376+
"qualified associated types like `<Self as Trait>::Type` are not supported; use a concrete type instead",
377+
))
378+
}
379+
// Recurse into generic arguments of path types.
380+
Type::Path(TypePath { path, .. }) => {
381+
for segment in path.segments.iter_mut() {
382+
if let PathArguments::AngleBracketed(args) = &mut segment.arguments {
383+
for arg in args.args.iter_mut() {
384+
if let GenericArgument::Type(inner_ty) = arg {
385+
resolve_self_types(inner_ty, associated_types, depth - 1)?;
386+
}
352387
}
353388
}
354389
}
355-
if let ReturnType::Type(_, ty) = &f.sig.output {
356-
if let Some(ident) = self_type_ident(ty) {
357-
return Err(Error::new(
358-
ty.span(),
359-
format!(
360-
"unresolved associated type `Self::{ident}` in return type; \
361-
use a concrete type instead"
362-
),
363-
));
364-
}
365-
}
390+
Ok(())
366391
}
367-
}
368-
369-
Ok(())
370-
}
371-
372-
/// If the type is `Self::Ident` and `Ident` exists in `associated_types`,
373-
/// return the resolved type. Otherwise return `None`.
374-
fn resolve_self_type(ty: &Type, associated_types: &HashMap<Ident, Type>) -> Option<Type> {
375-
match self_type_ident(ty) {
376-
Some(ident) => associated_types.get(ident).cloned(),
377-
None => None,
392+
// Recurse into reference types like &Self::Val.
393+
Type::Reference(TypeReference { elem, .. }) => {
394+
resolve_self_types(elem, associated_types, depth - 1)
395+
}
396+
_ => Ok(()),
378397
}
379398
}
380399

381400
/// If the type is `Self::Ident`, return the `Ident`. Otherwise return `None`.
382-
fn self_type_ident(ty: &Type) -> Option<&Ident> {
401+
///
402+
/// ### Errors
403+
/// If the type is a generic associated type like `Self::Foo<T>`.
404+
fn self_type_ident(ty: &Type) -> Result<Option<&Ident>, Error> {
383405
if let Type::Path(TypePath { qself: None, path }) = ty {
384406
let segments = &path.segments;
385407
if segments.len() == 2
386408
&& segments.first() == Some(&PathSegment::from(format_ident!("Self")))
387409
{
388-
if let Some(PathSegment {
389-
arguments: PathArguments::None,
390-
ident,
391-
}) = segments.get(1)
392-
{
393-
return Some(ident);
410+
if let Some(seg) = segments.get(1) {
411+
return match seg.arguments {
412+
PathArguments::None => Ok(Some(&seg.ident)),
413+
_ => Err(Error::new(
414+
path.span(),
415+
format!("generic associated types like `Self::{}<..>` are not supported; use a concrete type instead", seg.ident),
416+
)),
417+
};
394418
}
395419
}
396420
}
397-
None
421+
Ok(None)
398422
}
399423

400424
pub fn ty_to_safe_ident_str(ty: &Type) -> String {
@@ -487,3 +511,180 @@ mod test_path_in_macro_rules {
487511
assert_paths_eq(input, expected);
488512
}
489513
}
514+
515+
#[cfg(test)]
516+
mod test_fns_parse {
517+
use super::*;
518+
use quote::quote;
519+
use syn::parse2;
520+
521+
/// Parse an impl block through HasFnsItem and return the resolved fns.
522+
fn parse_fns(input: TokenStream) -> syn::Result<Vec<Fn>> {
523+
parse2::<HasFnsItem>(input).map(|item| item.fns())
524+
}
525+
526+
/// Parse an impl block and return the string representation of the nth
527+
/// fn's input types (excluding self) and return type.
528+
fn parsed_fn_sig(input: TokenStream, n: usize) -> (Vec<String>, String) {
529+
let fns = parse_fns(input).expect("parse failed");
530+
let f = &fns[n];
531+
let inputs: Vec<String> = f
532+
.inputs
533+
.iter()
534+
.filter_map(|arg| match arg {
535+
FnArg::Typed(t) => Some(quote!(#t).to_string()),
536+
_ => None,
537+
})
538+
.collect();
539+
let output = match &f.output {
540+
ReturnType::Default => "()".to_string(),
541+
ReturnType::Type(_, ty) => quote!(#ty).to_string(),
542+
};
543+
(inputs, output)
544+
}
545+
546+
#[test]
547+
fn test_no_associated_types() {
548+
let input = quote! {
549+
impl MyContract {
550+
pub fn hello(x: u32) -> u64 {}
551+
}
552+
};
553+
let (inputs, output) = parsed_fn_sig(input, 0);
554+
assert_eq!(inputs, vec!["x : u32"]);
555+
assert_eq!(output, "u64");
556+
}
557+
558+
#[test]
559+
fn test_basic_param_and_return() {
560+
let input = quote! {
561+
impl MyContract {
562+
type Val = u64;
563+
pub fn get(x: Self::Val) -> Self::Val {}
564+
}
565+
};
566+
let (inputs, output) = parsed_fn_sig(input, 0);
567+
assert_eq!(inputs, vec!["x : u64"]);
568+
assert_eq!(output, "u64");
569+
}
570+
571+
#[test]
572+
fn test_chained_two_step() {
573+
let input = quote! {
574+
impl MyContract {
575+
type A = u32;
576+
type B = Self::A;
577+
pub fn get(x: Self::B) {}
578+
}
579+
};
580+
let (inputs, _) = parsed_fn_sig(input, 0);
581+
assert_eq!(inputs, vec!["x : u32"]);
582+
}
583+
584+
#[test]
585+
fn test_wrapped_option() {
586+
let input = quote! {
587+
impl MyContract {
588+
type A = u64;
589+
pub fn get(x: Option<Self::A>) {}
590+
}
591+
};
592+
let (inputs, _) = parsed_fn_sig(input, 0);
593+
assert_eq!(inputs, vec!["x : Option < u64 >"]);
594+
}
595+
596+
#[test]
597+
fn test_double_wrapped_result_vec() {
598+
let input = quote! {
599+
impl MyContract {
600+
type A = u64;
601+
pub fn get(x: Result<Vec<Self::A>, Error>) {}
602+
}
603+
};
604+
let (inputs, _) = parsed_fn_sig(input, 0);
605+
assert_eq!(inputs, vec!["x : Result < Vec < u64 > , Error >"]);
606+
}
607+
608+
#[test]
609+
fn test_reject_qualified_self_path() {
610+
let input = quote! {
611+
impl MyContract {
612+
pub fn get(x: <Self as Trait>::A) {}
613+
}
614+
};
615+
let Err(err) = parse_fns(input) else {
616+
panic!("expected error");
617+
};
618+
assert!(
619+
err.to_string().contains("qualified associated types"),
620+
"unexpected error: {err}"
621+
);
622+
}
623+
624+
#[test]
625+
fn test_reject_generic_associated_type() {
626+
let input = quote! {
627+
impl MyContract {
628+
pub fn get(x: Self::Foo<u32>) {}
629+
}
630+
};
631+
let Err(err) = parse_fns(input) else {
632+
panic!("expected error");
633+
};
634+
assert!(
635+
err.to_string().contains("generic associated types"),
636+
"unexpected error: {err}"
637+
);
638+
}
639+
640+
#[test]
641+
fn test_reject_buried_qualified_self_path() {
642+
let input = quote! {
643+
impl MyContract {
644+
pub fn get(x: Result<Vec<<Self as Trait>::A>, Error>) {}
645+
}
646+
};
647+
let Err(err) = parse_fns(input) else {
648+
panic!("expected error");
649+
};
650+
assert!(
651+
err.to_string().contains("qualified associated types"),
652+
"unexpected error: {err}"
653+
);
654+
}
655+
656+
#[test]
657+
fn test_reject_unresolved_type() {
658+
let input = quote! {
659+
impl MyContract {
660+
pub fn get(x: Self::Elsewhere) {}
661+
}
662+
};
663+
let Err(err) = parse_fns(input) else {
664+
panic!("expected error");
665+
};
666+
assert!(
667+
err.to_string()
668+
.contains("unresolved associated type `Self::Elsewhere`"),
669+
"unexpected error: {err}"
670+
);
671+
}
672+
673+
#[test]
674+
fn test_reject_recursive_cycle() {
675+
let input = quote! {
676+
impl MyContract {
677+
type A = Self::B;
678+
type B = Self::A;
679+
pub fn get(x: Self::A) {}
680+
}
681+
};
682+
let Err(err) = parse_fns(input) else {
683+
panic!("expected error");
684+
};
685+
assert!(
686+
err.to_string().contains("depth limit exceeded"),
687+
"unexpected error: {err}"
688+
);
689+
}
690+
}

0 commit comments

Comments
 (0)