Skip to main content

attribute_dsl/
infer.rs

1use syn::spanned::Spanned as _;
2use syn::visit_mut::{self, VisitMut as _};
3use syn::{
4    AngleBracketedGenericArguments, Error, Expr, GenericArgument, Path, PathArguments, Result,
5    Token, Type,
6};
7
8/// Single terminal type argument split from a path.
9#[derive(Clone, Debug)]
10pub enum SingleTypeArg {
11    /// The path's final segment had no generic arguments.
12    None,
13    /// The path's final segment used `_` as its only type argument.
14    Infer,
15    /// The path's final segment used one explicit type argument.
16    Explicit(Box<Type>),
17}
18
19impl SingleTypeArg {
20    /// Return the explicit type argument, if one was present.
21    pub fn explicit_type(&self) -> Option<&Type> {
22        match self {
23            Self::Explicit(ty) => Some(ty),
24            Self::None | Self::Infer => None,
25        }
26    }
27
28    /// Return whether the terminal type argument was `_`.
29    pub fn is_infer(&self) -> bool {
30        matches!(self, Self::Infer)
31    }
32}
33
34/// Split a path's final generic argument into a normalized single type arg.
35///
36/// This is useful for DSLs where `Thing::<_>` means "infer the field type" and
37/// `Thing::<T>` pins an explicit target type.
38///
39/// # Errors
40///
41/// Returns [`syn::Error`] when the path has no final segment, the final segment
42/// has more than one generic argument, the argument is not a type, or the final
43/// segment uses parenthesized generic arguments.
44pub fn split_terminal_single_type_arg(
45    mut path: Path,
46    subject: &str,
47) -> Result<(Path, SingleTypeArg)> {
48    let path_span = path.span();
49    let last_segment = path
50        .segments
51        .last_mut()
52        .ok_or_else(|| Error::new(path_span, format!("expected {subject} path")))?;
53
54    let args = std::mem::replace(&mut last_segment.arguments, PathArguments::None);
55    let type_arg = match args {
56        PathArguments::None => SingleTypeArg::None,
57        PathArguments::AngleBracketed(mut angle_args) => {
58            if angle_args.args.len() != 1 {
59                return Err(Error::new(
60                    angle_args.span(),
61                    format!("{subject} type syntax expects exactly one type argument"),
62                ));
63            }
64
65            let arg = angle_args.args.pop().expect("len checked").into_value();
66            match arg {
67                GenericArgument::Type(Type::Infer(_)) => SingleTypeArg::Infer,
68                GenericArgument::Type(ty) => SingleTypeArg::Explicit(Box::new(ty)),
69                _ => Err(Error::new(
70                    arg.span(),
71                    format!("{subject} type syntax expects a type argument"),
72                ))?,
73            }
74        },
75        PathArguments::Parenthesized(args) => {
76            return Err(Error::new(
77                args.span(),
78                format!("{subject} path does not support parenthesized arguments"),
79            ));
80        },
81    };
82
83    Ok((path, type_arg))
84}
85
86/// Substitute `replacement` for every `_` occurrence inside a type.
87pub fn substitute_infer_in_type(ty: &Type, replacement: &Type) -> Type {
88    match ty {
89        Type::Infer(_) => replacement.clone(),
90        Type::Path(type_path) => {
91            let mut type_path = type_path.clone();
92            type_path.path = substitute_infer_in_path(&type_path.path, replacement);
93            Type::Path(type_path)
94        },
95        Type::Array(array) => {
96            let mut array = array.clone();
97            array.elem = Box::new(substitute_infer_in_type(&array.elem, replacement));
98            Type::Array(array)
99        },
100        Type::Slice(slice) => {
101            let mut slice = slice.clone();
102            slice.elem = Box::new(substitute_infer_in_type(&slice.elem, replacement));
103            Type::Slice(slice)
104        },
105        Type::Ptr(ptr) => {
106            let mut ptr = ptr.clone();
107            ptr.elem = Box::new(substitute_infer_in_type(&ptr.elem, replacement));
108            Type::Ptr(ptr)
109        },
110        Type::BareFn(bare_fn) => {
111            let mut bare_fn = bare_fn.clone();
112            for input in &mut bare_fn.inputs {
113                input.ty = substitute_infer_in_type(&input.ty, replacement);
114            }
115            substitute_infer_in_return_type(&mut bare_fn.output, replacement);
116            Type::BareFn(bare_fn)
117        },
118        Type::TraitObject(trait_object) => {
119            let mut trait_object = trait_object.clone();
120            substitute_infer_in_bounds(&mut trait_object.bounds, replacement);
121            Type::TraitObject(trait_object)
122        },
123        Type::ImplTrait(impl_trait) => {
124            let mut impl_trait = impl_trait.clone();
125            substitute_infer_in_bounds(&mut impl_trait.bounds, replacement);
126            Type::ImplTrait(impl_trait)
127        },
128        Type::Tuple(tuple) => {
129            let mut tuple = tuple.clone();
130            tuple.elems = tuple
131                .elems
132                .iter()
133                .map(|ty| substitute_infer_in_type(ty, replacement))
134                .collect();
135            Type::Tuple(tuple)
136        },
137        Type::Paren(paren) => {
138            let mut paren = paren.clone();
139            paren.elem = Box::new(substitute_infer_in_type(&paren.elem, replacement));
140            Type::Paren(paren)
141        },
142        Type::Group(group) => {
143            let mut group = group.clone();
144            group.elem = Box::new(substitute_infer_in_type(&group.elem, replacement));
145            Type::Group(group)
146        },
147        Type::Reference(reference) => {
148            let mut reference = reference.clone();
149            *reference.elem = substitute_infer_in_type(&reference.elem, replacement);
150            Type::Reference(reference)
151        },
152        _ => ty.clone(),
153    }
154}
155
156/// Substitute `replacement` for every `_` occurrence inside an expression.
157pub fn substitute_infer_in_expr(expr: &Expr, replacement: &Type) -> Expr {
158    let mut expr = expr.clone();
159    InferSubstitutor { replacement }.visit_expr_mut(&mut expr);
160    expr
161}
162
163/// Substitute `replacement` for every `_` occurrence inside path arguments.
164pub fn substitute_infer_in_path(path: &Path, replacement: &Type) -> Path {
165    let mut path = path.clone();
166
167    for segment in &mut path.segments {
168        substitute_infer_in_path_arguments(&mut segment.arguments, replacement);
169    }
170
171    path
172}
173
174struct InferSubstitutor<'a> {
175    replacement: &'a Type,
176}
177
178impl visit_mut::VisitMut for InferSubstitutor<'_> {
179    fn visit_type_mut(&mut self, node: &mut Type) {
180        *node = substitute_infer_in_type(node, self.replacement);
181    }
182
183    fn visit_path_mut(&mut self, node: &mut Path) {
184        *node = substitute_infer_in_path(node, self.replacement);
185    }
186}
187
188fn substitute_infer_in_return_type(return_type: &mut syn::ReturnType, replacement: &Type) {
189    if let syn::ReturnType::Type(_, ty) = return_type {
190        **ty = substitute_infer_in_type(ty, replacement);
191    }
192}
193
194fn substitute_infer_in_bounds(
195    bounds: &mut syn::punctuated::Punctuated<syn::TypeParamBound, Token![+]>,
196    replacement: &Type,
197) {
198    for bound in bounds {
199        if let syn::TypeParamBound::Trait(trait_bound) = bound {
200            trait_bound.path = substitute_infer_in_path(&trait_bound.path, replacement);
201        }
202    }
203}
204
205fn substitute_infer_in_path_arguments(arguments: &mut PathArguments, replacement: &Type) {
206    match arguments {
207        PathArguments::AngleBracketed(args) => {
208            substitute_infer_in_angle_bracketed_arguments(args, replacement);
209        },
210        PathArguments::Parenthesized(args) => {
211            args.inputs = args
212                .inputs
213                .iter()
214                .map(|ty| substitute_infer_in_type(ty, replacement))
215                .collect();
216            substitute_infer_in_return_type(&mut args.output, replacement);
217        },
218        PathArguments::None => {},
219    }
220}
221
222fn substitute_infer_in_angle_bracketed_arguments(
223    args: &mut AngleBracketedGenericArguments,
224    replacement: &Type,
225) {
226    for arg in &mut args.args {
227        match arg {
228            GenericArgument::Type(ty) => {
229                *ty = substitute_infer_in_type(ty, replacement);
230            },
231            GenericArgument::AssocType(assoc_type) => {
232                if let Some(generics) = &mut assoc_type.generics {
233                    substitute_infer_in_angle_bracketed_arguments(generics, replacement);
234                }
235                assoc_type.ty = substitute_infer_in_type(&assoc_type.ty, replacement);
236            },
237            GenericArgument::Constraint(constraint) => {
238                if let Some(generics) = &mut constraint.generics {
239                    substitute_infer_in_angle_bracketed_arguments(generics, replacement);
240                }
241                substitute_infer_in_bounds(&mut constraint.bounds, replacement);
242            },
243            _ => {},
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use syn::{Type, parse_quote};
252
253    fn compact(tokens: impl quote::ToTokens) -> String {
254        tokens
255            .to_token_stream()
256            .to_string()
257            .chars()
258            .filter(|ch| !ch.is_whitespace())
259            .collect()
260    }
261
262    fn parenthesized_path(output: Type) -> Path {
263        let mut inputs = syn::punctuated::Punctuated::new();
264        inputs.push(parse_quote!(_));
265
266        Path::from(syn::PathSegment {
267            ident: parse_quote!(FnOnce),
268            arguments: PathArguments::Parenthesized(syn::ParenthesizedGenericArguments {
269                paren_token: Default::default(),
270                inputs,
271                output: syn::ReturnType::Type(Default::default(), Box::new(output)),
272            }),
273        })
274    }
275
276    #[test]
277    fn splits_terminal_single_type_arg() {
278        let path: Path = parse_quote!(crate::RangeValidation::<_>);
279        let (path, arg) = split_terminal_single_type_arg(path, "validator").expect("valid path");
280        assert_eq!(compact(&path), "crate::RangeValidation");
281        assert!(arg.is_infer());
282
283        let path: Path = parse_quote!(crate::RangeValidation::<i32>);
284        let (_, arg) = split_terminal_single_type_arg(path, "validator").expect("valid path");
285        assert_eq!(compact(arg.explicit_type().expect("explicit type")), "i32");
286    }
287
288    #[test]
289    fn splits_absent_terminal_type_arg_and_rejects_invalid_args() {
290        let path: Path = parse_quote!(crate::RangeValidation);
291        let (path, arg) = split_terminal_single_type_arg(path, "validator").expect("valid path");
292        assert_eq!(compact(&path), "crate::RangeValidation");
293        assert!(!arg.is_infer());
294        assert!(arg.explicit_type().is_none());
295
296        let path: Path = parse_quote!(crate::RangeValidation::<i32, String>);
297        let err = split_terminal_single_type_arg(path, "validator").expect_err("too many args");
298        assert!(
299            err.to_string()
300                .contains("validator type syntax expects exactly one type argument"),
301            "{err}"
302        );
303
304        let path: Path = parse_quote!(crate::RangeValidation::<3>);
305        let err = split_terminal_single_type_arg(path, "validator").expect_err("const arg");
306        assert!(
307            err.to_string()
308                .contains("validator type syntax expects a type argument"),
309            "{err}"
310        );
311
312        let path = parenthesized_path(parse_quote!(i32));
313        let err = split_terminal_single_type_arg(path, "validator").expect_err("function args");
314        assert!(
315            err.to_string()
316                .contains("validator path does not support parenthesized arguments"),
317            "{err}"
318        );
319    }
320
321    #[test]
322    fn substitutes_infer_in_paths_types_and_exprs() {
323        let replacement: Type = parse_quote!(String);
324        let path: Path = parse_quote!(crate::Input<Option<_>>);
325        assert_eq!(
326            compact(substitute_infer_in_path(&path, &replacement)),
327            "crate::Input<Option<String>>"
328        );
329
330        let ty: Type = parse_quote!(fn([_; 2], &[_]) -> Option<_>);
331        assert_eq!(
332            compact(substitute_infer_in_type(&ty, &replacement)),
333            "fn([String;2],&[String])->Option<String>"
334        );
335
336        let expr: Expr = parse_quote!(crate::Select::<_>.searchable(true));
337        assert_eq!(
338            compact(substitute_infer_in_expr(&expr, &replacement)),
339            "crate::Select::<String>.searchable(true)"
340        );
341    }
342
343    #[test]
344    fn substitutes_infer_in_additional_type_forms() {
345        let replacement: Type = parse_quote!(String);
346
347        let ptr: Type = parse_quote!(*const _);
348        assert_eq!(
349            compact(substitute_infer_in_type(&ptr, &replacement)),
350            "*constString"
351        );
352
353        let trait_object: Type = parse_quote!(dyn Iterator<Item = _> + Send);
354        assert_eq!(
355            compact(substitute_infer_in_type(&trait_object, &replacement)),
356            "dynIterator<Item=String>+Send"
357        );
358
359        let impl_trait: Type = parse_quote!(impl Into<_> + Send);
360        assert_eq!(
361            compact(substitute_infer_in_type(&impl_trait, &replacement)),
362            "implInto<String>+Send"
363        );
364
365        let tuple: Type = parse_quote!((_, Option<_>));
366        assert_eq!(
367            compact(substitute_infer_in_type(&tuple, &replacement)),
368            "(String,Option<String>)"
369        );
370
371        let paren: Type = parse_quote!((Option<_>));
372        assert_eq!(
373            compact(substitute_infer_in_type(&paren, &replacement)),
374            "(Option<String>)"
375        );
376
377        let group = Type::Group(syn::TypeGroup {
378            group_token: Default::default(),
379            elem: Box::new(parse_quote!(Option<_>)),
380        });
381        assert_eq!(
382            compact(substitute_infer_in_type(&group, &replacement)),
383            "Option<String>"
384        );
385
386        let never: Type = parse_quote!(!);
387        assert_eq!(compact(substitute_infer_in_type(&never, &replacement)), "!");
388    }
389
390    #[test]
391    fn substitutes_infer_in_path_argument_variants() {
392        let replacement: Type = parse_quote!(String);
393
394        let parenthesized = parenthesized_path(parse_quote!(_));
395        assert_eq!(
396            compact(substitute_infer_in_path(&parenthesized, &replacement)),
397            "FnOnce(String)->String"
398        );
399
400        let assoc_type: Path = parse_quote!(Trait<Assoc<_> = Result<_, _>>);
401        assert_eq!(
402            compact(substitute_infer_in_path(&assoc_type, &replacement)),
403            "Trait<Assoc<String>=Result<String,String>>"
404        );
405
406        let constraint: Path = parse_quote!(Trait<Assoc<_>: Into<_> + From<_>>);
407        assert_eq!(
408            compact(substitute_infer_in_path(&constraint, &replacement)),
409            "Trait<Assoc<String>:Into<String>+From<String>>"
410        );
411
412        let lifetime_and_const: Path = parse_quote!(Trait<'static, 3, _>);
413        assert_eq!(
414            compact(substitute_infer_in_path(&lifetime_and_const, &replacement)),
415            "Trait<'static,3,String>"
416        );
417    }
418
419    #[test]
420    fn substitutes_infer_inside_expression_types() {
421        let replacement: Type = parse_quote!(String);
422        let expr: Expr = parse_quote!(value as *const _);
423
424        assert_eq!(
425            compact(substitute_infer_in_expr(&expr, &replacement)),
426            "valueas*constString"
427        );
428    }
429}