Skip to main content

adze_common_syntax_core/
lib.rs

1//! Shared syntax helpers for parsing macro/tool attributes.
2
3#![forbid(unsafe_op_in_unsafe_fn)]
4#![deny(missing_docs)]
5#![cfg_attr(feature = "strict_api", deny(unreachable_pub))]
6#![cfg_attr(not(feature = "strict_api"), warn(unreachable_pub))]
7#![cfg_attr(feature = "strict_docs", deny(missing_docs))]
8#![cfg_attr(not(feature = "strict_docs"), allow(missing_docs))]
9
10use std::collections::HashSet;
11
12use syn::{
13    parse::{Parse, ParseStream},
14    punctuated::Punctuated,
15    *,
16};
17
18/// Name-value expression for attribute parameters.
19///
20/// Represents a key-value pair in attribute syntax, such as `param = "value"`.
21/// This is commonly used when parsing macro or tool attributes.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct NameValueExpr {
24    /// The parameter name.
25    pub path: Ident,
26    /// The equals token.
27    pub eq_token: Token![=],
28    /// The parameter value expression.
29    pub expr: Expr,
30}
31
32impl Parse for NameValueExpr {
33    fn parse(input: ParseStream) -> syn::Result<Self> {
34        Ok(NameValueExpr {
35            path: input.parse()?,
36            eq_token: input.parse()?,
37            expr: input.parse()?,
38        })
39    }
40}
41
42/// Field declaration followed by optional parameters.
43///
44/// Represents a struct field declaration optionally followed by a comma and additional
45/// named parameters. Used in parsing attribute syntax that includes field definitions with extra metadata.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct FieldThenParams {
48    /// The field declaration.
49    pub field: Field,
50    /// Optional comma separator before params.
51    pub comma: Option<Token![,]>,
52    /// Additional named parameters.
53    pub params: Punctuated<NameValueExpr, Token![,]>,
54}
55
56impl Parse for FieldThenParams {
57    fn parse(input: ParseStream) -> syn::Result<Self> {
58        let field = Field::parse_unnamed(input)?;
59        let comma: Option<Token![,]> = input.parse()?;
60        let params = if comma.is_some() {
61            Punctuated::parse_terminated_with(input, NameValueExpr::parse)?
62        } else {
63            Punctuated::new()
64        };
65
66        Ok(FieldThenParams {
67            field,
68            comma,
69            params,
70        })
71    }
72}
73
74/// Extract the innermost generic argument from a container type.
75///
76/// # Arguments
77/// * `ty` - The type to extract from
78/// * `inner_of` - The target generic type to extract (e.g., "Vec", "Option")
79/// * `skip_over` - Set of container types to skip through (e.g., "Box", "Arc")
80///
81/// # Returns
82/// A tuple `(inner_type, was_extracted)` where `inner_type` is the extracted or original type,
83/// and `was_extracted` indicates whether the target type was found and extracted.
84pub fn try_extract_inner_type(
85    ty: &Type,
86    inner_of: &str,
87    skip_over: &HashSet<&str>,
88) -> (Type, bool) {
89    if let Type::Path(p) = &ty {
90        let type_segment = p.path.segments.last().unwrap();
91        if type_segment.ident == inner_of {
92            match &type_segment.arguments {
93                PathArguments::AngleBracketed(p) => {
94                    if let GenericArgument::Type(t) = p.args.first().unwrap().clone() {
95                        (t, true)
96                    } else {
97                        panic!("Argument in angle brackets must be a type")
98                    }
99                }
100                _ => (ty.clone(), false),
101            }
102        } else if skip_over.contains(type_segment.ident.to_string().as_str()) {
103            match &type_segment.arguments {
104                PathArguments::AngleBracketed(p) => {
105                    if let GenericArgument::Type(t) = p.args.first().unwrap().clone() {
106                        let (inner, extracted) = try_extract_inner_type(&t, inner_of, skip_over);
107                        if extracted {
108                            (inner, true)
109                        } else {
110                            (ty.clone(), false)
111                        }
112                    } else {
113                        panic!("Argument in angle brackets must be a type")
114                    }
115                }
116                _ => (ty.clone(), false),
117            }
118        } else {
119            (ty.clone(), false)
120        }
121    } else {
122        (ty.clone(), false)
123    }
124}
125
126/// Remove configured container wrappers from a type.
127///
128/// # Arguments
129/// * `ty` - The type to filter
130/// * `skip_over` - Set of container types to unwrap (e.g., "Box", "Arc")
131///
132/// # Returns
133/// The type with all specified container wrappers removed. If the type is not a container type
134/// in the skip set, returns the original type unchanged.
135pub fn filter_inner_type(ty: &Type, skip_over: &HashSet<&str>) -> Type {
136    if let Type::Path(p) = &ty {
137        let type_segment = p.path.segments.last().unwrap();
138        if skip_over.contains(type_segment.ident.to_string().as_str()) {
139            match &type_segment.arguments {
140                PathArguments::AngleBracketed(p) => {
141                    if let GenericArgument::Type(t) = p.args.first().unwrap().clone() {
142                        filter_inner_type(&t, skip_over)
143                    } else {
144                        panic!("Argument in angle brackets must be a type")
145                    }
146                }
147                _ => ty.clone(),
148            }
149        } else {
150            ty.clone()
151        }
152    } else {
153        ty.clone()
154    }
155}
156
157/// Wrap leaf types in `adze::WithLeaf` unless they are in the skip set.
158///
159/// # Arguments
160/// * `ty` - The type to potentially wrap
161/// * `skip_over` - Set of container types to skip wrapping (e.g., "Vec", "Option")
162///
163/// # Returns
164/// The type with leaf types wrapped in `adze::WithLeaf`, or the original type if it's
165/// a container type in the skip set. For skipped containers, recursively wraps their inner generic arguments.
166pub fn wrap_leaf_type(ty: &Type, skip_over: &HashSet<&str>) -> Type {
167    let mut ty = ty.clone();
168    if let Type::Path(p) = &mut ty {
169        let type_segment = p.path.segments.last_mut().unwrap();
170        if skip_over.contains(type_segment.ident.to_string().as_str()) {
171            match &mut type_segment.arguments {
172                PathArguments::AngleBracketed(args) => {
173                    for a in args.args.iter_mut() {
174                        if let syn::GenericArgument::Type(t) = a {
175                            *t = wrap_leaf_type(t, skip_over);
176                        }
177                    }
178
179                    ty
180                }
181                _ => ty,
182            }
183        } else {
184            parse_quote!(adze::WithLeaf<#ty>)
185        }
186    } else {
187        parse_quote!(adze::WithLeaf<#ty>)
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use syn::parse_quote;
195
196    #[test]
197    fn test_parse_name_value_expr() {
198        let input: NameValueExpr = parse_quote!(key = "value");
199        assert_eq!(input.path.to_string(), "key");
200
201        let input: NameValueExpr = parse_quote!(precedence = 5);
202        assert_eq!(input.path.to_string(), "precedence");
203    }
204
205    #[test]
206    fn test_parse_field_then_params() {
207        let input: FieldThenParams = parse_quote!(Type);
208        assert!(input.comma.is_none());
209        assert!(input.params.is_empty());
210
211        let input: FieldThenParams = parse_quote!(Type, name = "test", value = 42);
212        assert!(input.comma.is_some());
213        assert_eq!(input.params.len(), 2);
214        assert_eq!(input.params[0].path.to_string(), "name");
215        assert_eq!(input.params[1].path.to_string(), "value");
216    }
217}