crux_cli/codegen/
node.rs

1use std::hash::{Hash, Hasher};
2
3use rustdoc_types::{
4    ExternalCrate, GenericArg, GenericArgs, Id, Item, ItemEnum, ItemSummary, Path, Type,
5};
6use serde::{Deserialize, Serialize};
7
8use super::item::{
9    field_ids, has_associated_item, has_field, has_variant, is_impl_for, variant_ids,
10};
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub struct GlobalId {
14    pub crate_: String,
15    pub id: u32,
16}
17
18#[derive(Debug, Clone, Eq, Serialize, Deserialize)]
19pub struct CrateNode {
20    pub id: GlobalId,
21    pub crate_: ExternalCrate,
22}
23
24impl Hash for CrateNode {
25    fn hash<H: Hasher>(&self, state: &mut H) {
26        self.id.hash(state);
27    }
28}
29
30impl PartialEq for CrateNode {
31    fn eq(&self, other: &Self) -> bool {
32        self.id == other.id
33    }
34}
35
36impl CrateNode {
37    pub fn new(crate_name: String, id: u32, crate_: ExternalCrate) -> Self {
38        Self {
39            id: GlobalId {
40                crate_: crate_name,
41                id,
42            },
43            crate_,
44        }
45    }
46}
47
48#[derive(Debug, Clone, Eq, Serialize, Deserialize)]
49pub struct SummaryNode {
50    pub id: GlobalId,
51    pub summary: ItemSummary,
52}
53
54impl Hash for SummaryNode {
55    fn hash<H: Hasher>(&self, state: &mut H) {
56        self.id.hash(state);
57    }
58}
59
60impl PartialEq for SummaryNode {
61    fn eq(&self, other: &Self) -> bool {
62        self.id == other.id
63    }
64}
65
66impl SummaryNode {
67    pub fn new(crate_: String, id: u32, summary: ItemSummary) -> Self {
68        Self {
69            id: GlobalId { crate_, id },
70            summary,
71        }
72    }
73
74    pub fn in_same_module_as(&self, other: &SummaryNode) -> bool {
75        let this = &self.summary.path;
76        let other = &other.summary.path;
77
78        if this.len() != other.len() {
79            return false;
80        }
81
82        this[..(this.len() - 1)] == other[..(other.len() - 1)]
83    }
84
85    pub fn points_to_crate(&self, crate_: &CrateNode) -> bool {
86        self.id.crate_ == crate_.id.crate_ && self.summary.crate_id == crate_.id.id
87    }
88}
89
90#[derive(Debug, Clone, Eq, Serialize, Deserialize)]
91pub struct ItemNode {
92    pub id: GlobalId,
93    pub item: Item,
94}
95
96impl Hash for ItemNode {
97    fn hash<H: Hasher>(&self, state: &mut H) {
98        self.id.hash(state);
99    }
100}
101
102impl PartialEq for ItemNode {
103    fn eq(&self, other: &Self) -> bool {
104        self.id == other.id
105    }
106}
107
108impl ItemNode {
109    pub fn new(crate_: String, item: Item) -> Self {
110        Self {
111            id: GlobalId {
112                crate_,
113                id: item.id.0,
114            },
115            item,
116        }
117    }
118
119    pub fn name(&self) -> Option<&str> {
120        let mut new_name = "";
121        for attr in &self.item.attrs {
122            if let Some((_, n)) =
123                lazy_regex::regex_captures!(r#"\[serde\(rename\s*=\s*"(\w+)"\)\]"#, attr)
124            {
125                new_name = n;
126            }
127        }
128        if new_name.is_empty() {
129            self.item.name.as_deref()
130        } else {
131            Some(new_name)
132        }
133    }
134
135    pub fn has_summary(&self, summary: &SummaryNode) -> bool {
136        self.id == summary.id
137    }
138
139    pub fn is_impl_for(&self, for_: &ItemNode, trait_name: &str) -> bool {
140        if self.id.crate_ != for_.id.crate_ {
141            return false;
142        }
143
144        is_impl_for(&self.item, &for_.item, trait_name)
145    }
146
147    pub fn is_range(&self) -> bool {
148        matches!(
149            &self.item,
150            Item {
151                inner: ItemEnum::StructField(Type::ResolvedPath(Path { path, .. })),
152                ..
153            } if path == "std::ops::Range"
154        )
155    }
156
157    fn should_skip(&self) -> bool {
158        self.item
159            .attrs
160            .iter()
161            .any(|attr| lazy_regex::regex_is_match!(r#"\[serde\s*\(\s*skip\s*\)\s*\]"#, attr))
162    }
163
164    pub fn fields(&self, fields: &[(&ItemNode,)]) -> Vec<ItemNode> {
165        field_ids(&self.item)
166            .iter()
167            .filter_map(|id| {
168                fields
169                    .iter()
170                    .find(|(f,)| !f.should_skip() && id == &f.item.id)
171                    .map(|found| found.0.clone())
172            })
173            .collect()
174    }
175
176    pub fn has_field(&self, field: &ItemNode) -> bool {
177        if self.id.crate_ != field.id.crate_ || field.should_skip() {
178            return false;
179        }
180        if field.name() == Some("__private_field") {
181            return false;
182        }
183
184        has_field(&self.item, &field.item)
185    }
186
187    pub fn variants(&self, variants: &[(&ItemNode,)]) -> Vec<ItemNode> {
188        variant_ids(&self.item)
189            .iter()
190            .filter_map(|id| {
191                variants
192                    .iter()
193                    .find(|(v,)| !v.should_skip() && id == &v.item.id)
194                    .map(|found| found.0.clone())
195            })
196            .collect()
197    }
198
199    pub fn has_variant(&self, variant: &ItemNode) -> bool {
200        if self.id.crate_ != variant.id.crate_ || variant.should_skip() {
201            return false;
202        }
203
204        has_variant(&self.item, &variant.item)
205    }
206
207    pub fn is_of_local_type(&self, type_node: &ItemNode) -> bool {
208        self.is_of_type(&type_node.id, false)
209    }
210
211    pub fn is_of_remote_type(&self, type_node: &SummaryNode) -> bool {
212        self.is_of_type(&type_node.id, true)
213    }
214
215    fn is_of_type(&self, id: &GlobalId, is_remote: bool) -> bool {
216        if self.id.crate_ != id.crate_ {
217            return false;
218        }
219
220        match &self.item {
221            Item {
222                inner: ItemEnum::StructField(t),
223                ..
224            } => check_type(id, t, is_remote),
225            Item {
226                inner:
227                    ItemEnum::AssocType {
228                        type_: Some(Type::ResolvedPath(target)),
229                        ..
230                    },
231                ..
232            } => target.id.0 == id.id,
233            _ => false,
234        }
235    }
236
237    pub fn has_associated_item(&self, associated_item: &ItemNode, with_name: &str) -> bool {
238        if self.id.crate_ != associated_item.id.crate_ {
239            return false;
240        }
241
242        has_associated_item(&self.item, &associated_item.item, with_name)
243    }
244}
245
246fn check_type(parent: &GlobalId, type_: &Type, is_remote: bool) -> bool {
247    match type_ {
248        Type::ResolvedPath(path) => check_path(parent, path, is_remote),
249        Type::QualifiedPath {
250            self_type, args, ..
251        } => check_type(parent, self_type, is_remote) || check_args(parent, args, is_remote),
252        Type::Tuple(vec) => vec.iter().any(|t| check_type(parent, t, is_remote)),
253        Type::Slice(t) | Type::Array { type_: t, .. } => check_type(parent, t, is_remote),
254        _ => false,
255    }
256}
257
258fn check_path(
259    parent: &GlobalId,
260    Path {
261        path,
262        id: Id(id),
263        args,
264    }: &Path,
265    is_remote: bool,
266) -> bool {
267    if is_remote {
268        if let "Option" | "String" | "Vec" | "std::ops::Range" = path.as_str() {
269            return false;
270        }
271    }
272    id == &parent.id || {
273        if let Some(args) = args {
274            check_args(parent, args, is_remote)
275        } else {
276            false
277        }
278    }
279}
280
281fn check_args(parent: &GlobalId, args: &GenericArgs, is_remote: bool) -> bool {
282    match args {
283        GenericArgs::AngleBracketed { args, .. } => args.iter().any(|arg| match arg {
284            GenericArg::Type(t) => check_type(parent, t, is_remote),
285            _ => false,
286        }),
287        GenericArgs::Parenthesized { inputs, .. } => {
288            inputs.iter().any(|t| check_type(parent, t, is_remote))
289        }
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use std::collections::HashMap;
296
297    use pretty_assertions::assert_eq;
298    use rustdoc_types::{Generics, Id, Item, ItemEnum, ItemKind, Struct, StructKind, Visibility};
299
300    use super::*;
301
302    fn make_summary(id: u32, path: Vec<String>) -> SummaryNode {
303        SummaryNode::new(
304            "test".to_string(),
305            id,
306            ItemSummary {
307                crate_id: 0,
308                path,
309                kind: ItemKind::Struct,
310            },
311        )
312    }
313
314    fn make_node(name: Option<String>, attrs: Vec<String>) -> ItemNode {
315        ItemNode::new(
316            "test".to_string(),
317            Item {
318                name,
319                attrs,
320                inner: ItemEnum::Struct(Struct {
321                    kind: StructKind::Plain {
322                        fields: vec![],
323                        has_stripped_fields: false,
324                    },
325                    generics: Generics {
326                        params: vec![],
327                        where_predicates: vec![],
328                    },
329                    impls: vec![],
330                }),
331                id: Id(0),
332                crate_id: 0,
333                span: None,
334                visibility: Visibility::Public,
335                docs: None,
336                links: HashMap::default(),
337                deprecation: None,
338            },
339        )
340    }
341
342    #[test]
343    fn test_in_same_module_as() {
344        let summary1 = make_summary(0, vec!["foo".to_string(), "bar".to_string()]);
345        let summary2 = make_summary(1, vec!["foo".to_string(), "baz".to_string()]);
346        assert!(summary1.in_same_module_as(&summary2));
347    }
348
349    #[test]
350    fn test_in_same_module_as_different_length() {
351        let summary1 = make_summary(0, vec!["foo".to_string(), "bar".to_string()]);
352        let summary2 = make_summary(1, vec!["foo".to_string()]);
353        assert!(!summary1.in_same_module_as(&summary2));
354    }
355
356    #[test]
357    fn test_in_same_module_as_different_module() {
358        let summary1 = make_summary(0, vec!["foo".to_string(), "bar".to_string()]);
359        let summary2 = make_summary(1, vec!["baz".to_string(), "bar".to_string()]);
360        assert!(!summary1.in_same_module_as(&summary2));
361    }
362
363    #[test]
364    fn test_get_name() {
365        let name = Some("Foo".to_string());
366        let attrs = vec![];
367        let node = make_node(name, attrs);
368        assert_eq!(node.name(), Some("Foo"));
369    }
370
371    #[test]
372    fn test_get_name_with_rename() {
373        let name = Some("Foo".to_string());
374        let attrs = vec![r#"#[serde(rename = "Bar")]"#.to_string()];
375        let node = make_node(name, attrs);
376        assert_eq!(node.name(), Some("Bar"));
377    }
378
379    #[test]
380    fn test_get_name_with_rename_no_whitespace() {
381        let name = Some("Foo".to_string());
382        let attrs = vec![r#"#[serde(rename="Bar")]"#.to_string()];
383        let node = make_node(name, attrs);
384        assert_eq!(node.name(), Some("Bar"));
385    }
386
387    #[test]
388    fn test_get_name_with_no_name() {
389        let name = None;
390        let attrs = vec![];
391        let node = make_node(name, attrs);
392        assert_eq!(node.name(), None);
393    }
394}