use syn::Type;
pub fn substitute_type(ty: &Type, generic_params: &[String], concrete_types: &[&Type]) -> Type {
match ty {
Type::Path(type_path) => {
let path = &type_path.path;
if path.segments.is_empty() {
return ty.clone();
}
if path.segments.len() == 1 {
let segment = &path.segments[0];
let ident_str = segment.ident.to_string();
if matches!(&segment.arguments, syn::PathArguments::None) {
if let Some(index) = generic_params.iter().position(|p| p == &ident_str)
&& let Some(concrete_ty) = concrete_types.get(index)
{
return (*concrete_ty).clone();
}
}
}
let mut new_segments = syn::punctuated::Punctuated::new();
for segment in &path.segments {
let new_arguments = match &segment.arguments {
syn::PathArguments::AngleBracketed(args) => {
let mut new_args = syn::punctuated::Punctuated::new();
for arg in &args.args {
let new_arg = match arg {
syn::GenericArgument::Type(inner_ty) => syn::GenericArgument::Type(
substitute_type(inner_ty, generic_params, concrete_types),
),
other => other.clone(),
};
new_args.push(new_arg);
}
syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
colon2_token: args.colon2_token,
lt_token: args.lt_token,
args: new_args,
gt_token: args.gt_token,
})
}
other => other.clone(),
};
new_segments.push(syn::PathSegment {
ident: segment.ident.clone(),
arguments: new_arguments,
});
}
let mut new_type_path = type_path.clone();
new_type_path.path.segments = new_segments;
Type::Path(new_type_path)
}
Type::Reference(type_ref) => {
let mut new_type_ref = type_ref.clone();
new_type_ref.elem = Box::new(substitute_type(
&type_ref.elem,
generic_params,
concrete_types,
));
Type::Reference(new_type_ref)
}
Type::Slice(type_slice) => {
let mut new_type_slice = type_slice.clone();
new_type_slice.elem = Box::new(substitute_type(
&type_slice.elem,
generic_params,
concrete_types,
));
Type::Slice(new_type_slice)
}
Type::Array(type_array) => {
let mut new_type_array = type_array.clone();
new_type_array.elem = Box::new(substitute_type(
&type_array.elem,
generic_params,
concrete_types,
));
Type::Array(new_type_array)
}
Type::Tuple(type_tuple) => {
let mut new_type_tuple = type_tuple.clone();
new_type_tuple.elems = type_tuple
.elems
.iter()
.map(|elem| substitute_type(elem, generic_params, concrete_types))
.collect();
Type::Tuple(new_type_tuple)
}
_ => ty.clone(),
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case("$invalid", "String")]
fn test_substitute_type_parse_failure_uses_original(
#[case] invalid: &str,
#[case] concrete_src: &str,
) {
use std::str::FromStr;
use proc_macro2::TokenStream;
let ty = Type::Verbatim(TokenStream::from_str(invalid).unwrap());
let concrete: Type = syn::parse_str(concrete_src).unwrap();
let substituted = substitute_type(&ty, &[String::from("T")], &[&concrete]);
assert_eq!(substituted, ty);
}
#[rstest]
#[case("T", &["T"], &["String"], "String")]
#[case("Vec<T>", &["T"], &["String"], "Vec < String >")]
#[case("Option<T>", &["T"], &["i32"], "Option < i32 >")]
#[case("Vec<Option<T>>", &["T"], &["String"], "Vec < Option < String > >")]
#[case("Option<Vec<Option<T>>>", &["T"], &["bool"], "Option < Vec < Option < bool > > >")]
#[case("HashMap<K, V>", &["K", "V"], &["String", "i32"], "HashMap < String , i32 >")]
#[case("Vec<U>", &["T"], &["String"], "Vec < U >")]
#[case("String", &["T"], &["i32"], "String")]
#[case("&T", &["T"], &["String"], "& String")]
#[case("&mut T", &["T"], &["i32"], "& mut i32")]
#[case("[T]", &["T"], &["String"], "[String]")]
#[case("[T; 5]", &["T"], &["u8"], "[u8 ; 5]")]
#[case("(T, U)", &["T", "U"], &["String", "i32"], "(String , i32)")]
#[case("(Vec<T>, Option<U>)", &["T", "U"], &["String", "bool"], "(Vec < String > , Option < bool >)")]
#[case("&Vec<T>", &["T"], &["String"], "& Vec < String >")]
#[case("std::vec::Vec<T>", &["T"], &["String"], "std :: vec :: Vec < String >")]
fn test_substitute_type_comprehensive(
#[case] input: &str,
#[case] params: &[&str],
#[case] concrete: &[&str],
#[case] expected: &str,
) {
let ty: Type = syn::parse_str(input).unwrap();
let generic_params: Vec<String> = params
.iter()
.map(std::string::ToString::to_string)
.collect();
let concrete_types: Vec<Type> = concrete
.iter()
.map(|s| syn::parse_str(s).unwrap())
.collect();
let concrete_refs: Vec<&Type> = concrete_types.iter().collect();
let result = substitute_type(&ty, &generic_params, &concrete_refs);
let result_str = quote::quote!(#result).to_string();
assert_eq!(result_str, expected, "Input: {input}");
}
#[test]
fn test_substitute_type_empty_path_segments() {
let ty = Type::Path(syn::TypePath {
attrs: Vec::new(),
qself: None,
path: syn::Path {
leading_colon: None,
segments: syn::punctuated::Punctuated::new(),
},
});
let concrete: Type = syn::parse_str("String").unwrap();
let result = substitute_type(&ty, &[String::from("T")], &[&concrete]);
assert_eq!(result, ty);
}
#[test]
fn matching_parameter_without_concrete_type_is_preserved() {
let ty: Type = syn::parse_str("T").unwrap();
let result = substitute_type(&ty, &[String::from("T")], &[]);
assert_eq!(result, ty);
}
#[test]
fn test_substitute_type_with_lifetime_generic_argument() {
let ty: Type = syn::parse_str("std::borrow::Cow<'static, T>").unwrap();
let concrete: Type = syn::parse_str("String").unwrap();
let result = substitute_type(&ty, &[String::from("T")], &[&concrete]);
let result_str = quote::quote!(#result).to_string();
assert_eq!(result_str, "std :: borrow :: Cow < 'static , String >");
}
#[test]
fn test_substitute_type_parenthesized_args() {
let ty: Type = syn::parse_str("fn(T) -> U").unwrap();
let concrete_t: Type = syn::parse_str("String").unwrap();
let concrete_u: Type = syn::parse_str("i32").unwrap();
let result = substitute_type(
&ty,
&[String::from("T"), String::from("U")],
&[&concrete_t, &concrete_u],
);
assert_eq!(result, ty);
}
#[test]
fn test_substitute_type_path_without_angle_brackets() {
let ty: Type = syn::parse_str("dyn Fn(T) -> U").unwrap();
let concrete_t: Type = syn::parse_str("String").unwrap();
let concrete_u: Type = syn::parse_str("i32").unwrap();
let result = substitute_type(
&ty,
&[String::from("T"), String::from("U")],
&[&concrete_t, &concrete_u],
);
assert_eq!(result, ty);
}
}