Skip to main content

grip/
struct_registry.rs

1// Copyright 2026 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use std::collections::{HashMap, HashSet};
6use std::path::PathBuf;
7
8use syn::visit::Visit;
9use syn::{GenericArgument, Item, ItemMod, ItemStruct, PathArguments, Type, parse_file};
10
11pub const KNOWN_STD_VALUE_TYPES: &[&str] = &[
12    "Vec", "HashMap", "HashSet", "BTreeMap", "BTreeSet", "VecDeque", "String", "Option", "Cell",
13    "RefCell",
14];
15
16pub fn field_type_head(ty: &Type) -> String {
17    if let Type::Path(type_path) = ty {
18        type_path
19            .path
20            .segments
21            .last()
22            .map(|s| s.ident.to_string())
23            .unwrap_or_default()
24    } else {
25        String::new()
26    }
27}
28
29pub(crate) fn self_ty_name(ty: &Type) -> String {
30    if let Type::Path(type_path) = ty {
31        type_path
32            .path
33            .segments
34            .first()
35            .map(|s| s.ident.to_string())
36            .unwrap_or_default()
37    } else {
38        String::new()
39    }
40}
41
42pub(crate) fn is_trait_object_type(ty: &Type) -> bool {
43    match ty {
44        Type::TraitObject(_) => true,
45        Type::Reference(r) => is_trait_object_type(&r.elem),
46        Type::Path(p) => p.path.segments.iter().any(|seg| match &seg.arguments {
47            PathArguments::AngleBracketed(args) => args.args.iter().any(|arg| match arg {
48                GenericArgument::Type(t) => is_trait_object_type(t),
49                _ => false,
50            }),
51            _ => false,
52        }),
53        _ => false,
54    }
55}
56
57#[derive(Debug, Default)]
58pub struct StructRegistry {
59    fields_by_struct: HashMap<String, Vec<String>>,
60    concrete_fields_by_struct: HashMap<String, HashMap<String, String>>,
61}
62
63impl StructRegistry {
64    #[must_use]
65    pub fn build(files: &[(PathBuf, String)]) -> Self {
66        let mut registry = Self::default();
67        for (_, source) in files {
68            if let Ok(syntax) = parse_file(source) {
69                for item in &syntax.items {
70                    registry.visit_item(item);
71                }
72            }
73        }
74        registry
75    }
76
77    #[must_use]
78    pub fn is_transitive_value_type(&self, type_name: &str) -> bool {
79        self.resolve(type_name, &mut HashSet::new())
80    }
81
82    #[must_use]
83    pub fn concrete_fields_of(&self, type_name: &str) -> HashMap<String, String> {
84        self.concrete_fields_by_struct
85            .get(type_name)
86            .cloned()
87            .unwrap_or_default()
88    }
89
90    fn resolve(&self, type_name: &str, visiting: &mut HashSet<String>) -> bool {
91        if KNOWN_STD_VALUE_TYPES.contains(&type_name) {
92            return true;
93        }
94        if !visiting.insert(type_name.to_string()) {
95            return false;
96        }
97        let result = self
98            .fields_by_struct
99            .get(type_name)
100            .is_some_and(|fields| fields.iter().all(|field| self.resolve(field, visiting)));
101        visiting.remove(type_name);
102        result
103    }
104}
105
106impl<'ast> Visit<'ast> for StructRegistry {
107    fn visit_item(&mut self, item: &'ast Item) {
108        match item {
109            Item::Struct(item_struct) => self.record_struct(item_struct),
110            Item::Mod(item_mod) => self.visit_inline_mod(item_mod),
111            _ => {}
112        }
113    }
114}
115
116impl StructRegistry {
117    fn record_struct(&mut self, item_struct: &ItemStruct) {
118        let name = item_struct.ident.to_string();
119        let mut all_field_types = Vec::new();
120        let mut concrete_fields = HashMap::new();
121        for f in &item_struct.fields {
122            let type_head = field_type_head(&f.ty);
123            all_field_types.push(type_head.clone());
124            if !is_trait_object_type(&f.ty) {
125                if let Some(field_name) = f.ident.as_ref().map(ToString::to_string) {
126                    concrete_fields.insert(field_name, type_head);
127                }
128            }
129        }
130        self.fields_by_struct.insert(name.clone(), all_field_types);
131        self.concrete_fields_by_struct.insert(name, concrete_fields);
132    }
133
134    fn visit_inline_mod(&mut self, item_mod: &ItemMod) {
135        if let Some((_, items)) = &item_mod.content {
136            for inner in items {
137                self.visit_item(inner);
138            }
139        }
140    }
141}