Skip to main content

frs/cmds/rsl/
ast.rs

1//! Shared syntax classification helpers for the rsl rules.
2
3use std::collections::HashMap;
4use std::collections::HashSet;
5use std::collections::VecDeque;
6
7use proc_macro2::Span;
8use proc_macro2::TokenTree;
9use syn::Item;
10use syn::spanned::Spanned;
11
12#[derive(Clone, Copy, Debug, strum::Display, Eq, PartialEq)]
13#[strum(serialize_all = "snake_case")]
14pub enum ItemGroup {
15    ExternCrate,
16    Use,
17    Modules,
18    GlobalAsm,
19    Constants,
20    Aliases,
21    Items,
22}
23
24#[derive(Clone, Copy, Debug, Eq, strum::IntoStaticStr, PartialEq)]
25#[strum(serialize_all = "snake_case")]
26pub enum ItemKind {
27    ExternCrate,
28    Use,
29    ForeignMod,
30    Mod,
31    GlobalAsm,
32    Const,
33    Static,
34    #[strum(to_string = "ty_alias")]
35    TypeAlias,
36    Macro,
37    Enum,
38    Struct,
39    Union,
40    Trait,
41    TraitAlias,
42    Impl,
43    Fn,
44}
45
46impl ItemKind {
47    pub fn label(self) -> &'static str {
48        self.into()
49    }
50
51    pub const fn group(self) -> ItemGroup {
52        match self {
53            Self::ExternCrate => ItemGroup::ExternCrate,
54            Self::Use => ItemGroup::Use,
55            Self::ForeignMod | Self::Mod => ItemGroup::Modules,
56            Self::GlobalAsm => ItemGroup::GlobalAsm,
57            Self::Const | Self::Static => ItemGroup::Constants,
58            Self::TypeAlias => ItemGroup::Aliases,
59            Self::Macro
60            | Self::Enum
61            | Self::Struct
62            | Self::Union
63            | Self::Trait
64            | Self::TraitAlias
65            | Self::Impl
66            | Self::Fn => ItemGroup::Items,
67        }
68    }
69}
70
71#[derive(Clone, Copy, Debug, strum::Display, Eq, Ord, PartialEq, PartialOrd)]
72pub enum VisibilityClass {
73    #[strum(to_string = "pub")]
74    Public,
75    #[strum(to_string = "pub(crate)")]
76    Crate,
77    #[strum(to_string = "restricted")]
78    Restricted,
79    #[strum(to_string = "private")]
80    Private,
81}
82
83impl From<&syn::Visibility> for VisibilityClass {
84    fn from(visibility: &syn::Visibility) -> Self {
85        match visibility {
86            syn::Visibility::Public(_) => Self::Public,
87            syn::Visibility::Restricted(restricted) => {
88                if restricted.path.is_ident("crate") {
89                    Self::Crate
90                } else if restricted.path.is_ident("self") {
91                    Self::Private
92                } else {
93                    Self::Restricted
94                }
95            }
96            syn::Visibility::Inherited => Self::Private,
97        }
98    }
99}
100
101#[derive(Clone, Debug)]
102pub struct ClassifiedItem {
103    pub kind: ItemKind,
104    pub span: Span,
105}
106
107pub fn type_definition(item: &Item) -> Option<(String, ItemKind, VisibilityClass)> {
108    match item {
109        Item::Enum(item) => Some((item.ident.to_string(), ItemKind::Enum, VisibilityClass::from(&item.vis))),
110        Item::Struct(item) => Some((
111            item.ident.to_string(),
112            ItemKind::Struct,
113            VisibilityClass::from(&item.vis),
114        )),
115        Item::Union(item) => Some((
116            item.ident.to_string(),
117            ItemKind::Union,
118            VisibilityClass::from(&item.vis),
119        )),
120        Item::Const(_)
121        | Item::ExternCrate(_)
122        | Item::Fn(_)
123        | Item::ForeignMod(_)
124        | Item::Impl(_)
125        | Item::Macro(_)
126        | Item::Mod(_)
127        | Item::Static(_)
128        | Item::Trait(_)
129        | Item::TraitAlias(_)
130        | Item::Type(_)
131        | Item::Use(_)
132        | Item::Verbatim(_)
133        | _ => None,
134    }
135}
136
137pub fn impl_target_name(item_impl: &syn::ItemImpl) -> Option<String> {
138    let syn::Type::Path(type_path) = item_impl.self_ty.as_ref() else {
139        return None;
140    };
141    if type_path.qself.is_some() || type_path.path.leading_colon.is_some() || type_path.path.segments.len() != 1 {
142        return None;
143    }
144    type_path.path.segments.first().map(|segment| segment.ident.to_string())
145}
146
147pub fn item_visibility(item: &Item) -> Option<VisibilityClass> {
148    let visibility = match item {
149        Item::Const(item) => &item.vis,
150        Item::Enum(item) => &item.vis,
151        Item::ExternCrate(item) => &item.vis,
152        Item::Fn(item) => &item.vis,
153        Item::Mod(item) => &item.vis,
154        Item::Static(item) => &item.vis,
155        Item::Struct(item) => &item.vis,
156        Item::Trait(item) => &item.vis,
157        Item::TraitAlias(item) => &item.vis,
158        Item::Type(item) => &item.vis,
159        Item::Union(item) => &item.vis,
160        Item::Use(item) => &item.vis,
161        Item::Macro(_) | Item::Impl(_) | Item::Verbatim(_) | _ => {
162            return None;
163        }
164    };
165    Some(VisibilityClass::from(visibility))
166}
167
168pub fn item_label(item: &Item, kind: ItemKind) -> String {
169    let name = match item {
170        Item::Const(item) => Some(item.ident.to_string()),
171        Item::Enum(item) => Some(item.ident.to_string()),
172        Item::ExternCrate(item) => Some(item.ident.to_string()),
173        Item::Fn(item) => Some(item.sig.ident.to_string()),
174        Item::Mod(item) => Some(item.ident.to_string()),
175        Item::Static(item) => Some(item.ident.to_string()),
176        Item::Struct(item) => Some(item.ident.to_string()),
177        Item::Trait(item) => Some(item.ident.to_string()),
178        Item::TraitAlias(item) => Some(item.ident.to_string()),
179        Item::Type(item) => Some(item.ident.to_string()),
180        Item::Union(item) => Some(item.ident.to_string()),
181        Item::Use(_) | Item::ForeignMod(_) | Item::Macro(_) | Item::Impl(_) | Item::Verbatim(_) | _ => None,
182    };
183    name.map_or_else(|| kind.label().to_owned(), |name| format!("{} {name}", kind.label()))
184}
185
186pub fn impl_order_label(item: &Item) -> String {
187    if let Item::Impl(item_impl) = item {
188        let target = self::impl_target_name(item_impl).unwrap_or_else(|| "type".to_owned());
189        if item_impl.trait_.is_some() {
190            format!("trait impl {target}")
191        } else {
192            format!("inherent impl {target}")
193        }
194    } else {
195        self::classify_item(item).map_or_else(
196            || "item".to_owned(),
197            |classified| self::item_label(item, classified.kind),
198        )
199    }
200}
201
202pub fn item_span(item: &Item) -> Span {
203    match item {
204        Item::Const(item) => item.const_token.span(),
205        Item::Enum(item) => item.enum_token.span(),
206        Item::ExternCrate(item) => item.extern_token.span(),
207        Item::Fn(item) => item.sig.fn_token.span(),
208        Item::ForeignMod(item) => item.abi.extern_token.span(),
209        Item::Impl(item) => item.impl_token.span(),
210        Item::Macro(item) => item.mac.path.span(),
211        Item::Mod(item) => item.mod_token.span(),
212        Item::Static(item) => item.static_token.span(),
213        Item::Struct(item) => item.struct_token.span(),
214        Item::Trait(item) => item.trait_token.span(),
215        Item::TraitAlias(item) => item.trait_token.span(),
216        Item::Type(item) => item.type_token.span(),
217        Item::Union(item) => item.union_token.span(),
218        Item::Use(item) => item.use_token.span(),
219        Item::Verbatim(tokens) => self::explicit_macro_span(tokens).unwrap_or_else(|| tokens.span()),
220        _ => item.span(),
221    }
222}
223
224pub fn classify_item(item: &Item) -> Option<ClassifiedItem> {
225    let kind = match item {
226        Item::ExternCrate(_) => ItemKind::ExternCrate,
227        Item::Use(_) => ItemKind::Use,
228        Item::ForeignMod(_) => ItemKind::ForeignMod,
229        Item::Mod(_) => ItemKind::Mod,
230        Item::Macro(item_macro) if self::is_global_asm(item_macro) => ItemKind::GlobalAsm,
231        Item::Const(_) => ItemKind::Const,
232        Item::Static(_) => ItemKind::Static,
233        Item::Type(_) => ItemKind::TypeAlias,
234        Item::Macro(item_macro) if item_macro.ident.is_some() => ItemKind::Macro,
235        Item::Enum(_) => ItemKind::Enum,
236        Item::Struct(_) => ItemKind::Struct,
237        Item::Union(_) => ItemKind::Union,
238        Item::Trait(_) => ItemKind::Trait,
239        Item::TraitAlias(_) => ItemKind::TraitAlias,
240        Item::Impl(_) => ItemKind::Impl,
241        Item::Fn(_) => ItemKind::Fn,
242        Item::Verbatim(tokens) if self::explicit_macro_span(tokens).is_some() => ItemKind::Macro,
243        Item::Macro(_) | Item::Verbatim(_) | _ => return None,
244    };
245
246    Some(ClassifiedItem {
247        kind,
248        span: self::item_span(item),
249    })
250}
251
252pub(super) fn is_test_module(item: &Item) -> bool {
253    let Item::Mod(module) = item else {
254        return false;
255    };
256
257    self::is_test_module_declaration(module)
258}
259
260pub(super) fn is_test_module_declaration(module: &syn::ItemMod) -> bool {
261    module.ident == "tests"
262        && module.attrs.iter().any(|attribute| {
263            let syn::Meta::List(meta) = &attribute.meta else {
264                return false;
265            };
266            meta.path.is_ident("cfg")
267                && syn::parse2::<syn::Path>(meta.tokens.clone()).is_ok_and(|path| path.is_ident("test"))
268        })
269}
270
271fn is_global_asm(item: &syn::ItemMacro) -> bool {
272    item.mac
273        .path
274        .segments
275        .last()
276        .is_some_and(|segment| segment.ident == "global_asm")
277}
278
279fn explicit_macro_span(tokens: &proc_macro2::TokenStream) -> Option<Span> {
280    let mut tokens = tokens.clone().into_iter().peekable();
281    loop {
282        match tokens.next()? {
283            TokenTree::Punct(punct) if punct.as_char() == '#' => {
284                if !matches!(tokens.next(), Some(TokenTree::Group(group)) if group.delimiter() == proc_macro2::Delimiter::Bracket)
285                {
286                    return None;
287                }
288            }
289            TokenTree::Ident(ident) if ident == "pub" => {
290                if matches!(tokens.peek(), Some(TokenTree::Group(group)) if group.delimiter() == proc_macro2::Delimiter::Parenthesis)
291                {
292                    tokens.next();
293                }
294            }
295            TokenTree::Ident(ident) if matches!(ident.to_string().as_str(), "crate" | "self" | "super") => {}
296            TokenTree::Ident(ident) if ident == "macro" => {
297                let Some(TokenTree::Ident(_)) = tokens.next() else {
298                    return None;
299                };
300                if matches!(tokens.peek(), Some(TokenTree::Group(group)) if group.delimiter() == proc_macro2::Delimiter::Parenthesis)
301                {
302                    tokens.next();
303                }
304                return matches!(tokens.next(), Some(TokenTree::Group(group)) if group.delimiter() == proc_macro2::Delimiter::Brace)
305                    .then_some(ident.span());
306            }
307            TokenTree::Group(_) | TokenTree::Ident(_) | TokenTree::Punct(_) | TokenTree::Literal(_) => return None,
308        }
309    }
310}
311
312#[derive(Clone, Debug)]
313pub(super) struct OrderNode {
314    pub(super) source_idx: usize,
315    pub(super) span: Span,
316    pub(super) group: Option<ItemGroup>,
317    pub(super) visibility: Option<VisibilityClass>,
318    pub(super) label: String,
319}
320
321#[derive(Clone, Debug)]
322pub(super) struct ModuleNode {
323    pub(super) order: OrderNode,
324    pub(super) idxs: Vec<usize>,
325}
326
327#[derive(Clone, Debug)]
328struct TypeCluster {
329    type_idx: usize,
330    name: String,
331    kind: ItemKind,
332    visibility: VisibilityClass,
333    inherent_impls: Vec<usize>,
334    trait_impls: Vec<usize>,
335}
336
337#[derive(Clone, Copy, Debug)]
338pub(super) enum ItemVisibility {
339    Known(VisibilityClass),
340    NotApplicable,
341}
342
343#[derive(Debug)]
344pub(super) enum ItemMetadata {
345    Unclassified,
346    Classified {
347        item: ClassifiedItem,
348        visibility: ItemVisibility,
349    },
350    TestModule {
351        item: ClassifiedItem,
352        visibility: VisibilityClass,
353    },
354}
355
356impl ItemMetadata {
357    fn from_item(item: &Item) -> Self {
358        let Some(classified) = self::classify_item(item) else {
359            return Self::Unclassified;
360        };
361        if self::is_test_module(item)
362            && let Item::Mod(module) = item
363        {
364            return Self::TestModule {
365                item: classified,
366                visibility: VisibilityClass::from(&module.vis),
367            };
368        }
369
370        let visibility = self::item_visibility(item).map_or(ItemVisibility::NotApplicable, ItemVisibility::Known);
371        Self::Classified {
372            item: classified,
373            visibility,
374        }
375    }
376
377    pub(super) const fn classified(&self) -> Option<&ClassifiedItem> {
378        match self {
379            Self::Unclassified => None,
380            Self::Classified { item, .. } | Self::TestModule { item, .. } => Some(item),
381        }
382    }
383
384    pub(super) const fn is_test_module(&self) -> bool {
385        matches!(self, Self::TestModule { .. })
386    }
387
388    pub(super) const fn visibility(&self) -> Option<VisibilityClass> {
389        match self {
390            Self::Unclassified => None,
391            Self::Classified { visibility, .. } => visibility.value(),
392            Self::TestModule { visibility, .. } => Some(*visibility),
393        }
394    }
395}
396
397impl ItemVisibility {
398    const fn value(self) -> Option<VisibilityClass> {
399        match self {
400            Self::Known(visibility) => Some(visibility),
401            Self::NotApplicable => None,
402        }
403    }
404}
405
406pub(super) struct ModuleItem<'ast> {
407    item: &'ast Item,
408    metadata: ItemMetadata,
409}
410
411impl<'ast> ModuleItem<'ast> {
412    pub(super) const fn item(&self) -> &'ast Item {
413        self.item
414    }
415
416    pub(super) const fn metadata(&self) -> &ItemMetadata {
417        &self.metadata
418    }
419}
420
421pub(super) fn module_item_lists(file: &syn::File) -> Vec<Vec<ModuleItem<'_>>> {
422    let mut pending = VecDeque::from([file.items.as_slice()]);
423    let mut scopes = Vec::new();
424
425    while let Some(items) = pending.pop_front() {
426        let module_items = items
427            .iter()
428            .map(|item| ModuleItem {
429                item,
430                metadata: ItemMetadata::from_item(item),
431            })
432            .collect();
433        scopes.push(module_items);
434        for item in items {
435            if let Item::Mod(module) = item
436                && let Some((_, nested_items)) = &module.content
437            {
438                pending.push_back(nested_items);
439            }
440        }
441    }
442
443    scopes
444}
445
446pub(super) fn module_nodes(items: &[ModuleItem<'_>]) -> Vec<ModuleNode> {
447    let mut type_idxs = HashMap::new();
448    let mut clusters = HashMap::new();
449
450    for (idx, module_item) in items.iter().enumerate() {
451        let item = module_item.item();
452        let Some((name, kind, visibility)) = self::type_definition(item) else {
453            continue;
454        };
455        if type_idxs.insert(name.clone(), Some(idx)).is_some() {
456            type_idxs.insert(name, None);
457            continue;
458        }
459        clusters.insert(
460            idx,
461            TypeCluster {
462                type_idx: idx,
463                name,
464                kind,
465                visibility,
466                inherent_impls: Vec::new(),
467                trait_impls: Vec::new(),
468            },
469        );
470    }
471
472    for (idx, module_item) in items.iter().enumerate() {
473        let item = module_item.item();
474        let Item::Impl(item_impl) = item else {
475            continue;
476        };
477        let Some(target_name) = self::impl_target_name(item_impl) else {
478            continue;
479        };
480        let Some(Some(type_idx)) = type_idxs.get(&target_name) else {
481            continue;
482        };
483        let Some(cluster) = clusters.get_mut(type_idx) else {
484            continue;
485        };
486        if item_impl.trait_.is_some() {
487            cluster.trait_impls.push(idx);
488        } else {
489            cluster.inherent_impls.push(idx);
490        }
491    }
492
493    let mut item_to_cluster = HashMap::new();
494    for (&type_idx, cluster) in &clusters {
495        item_to_cluster.insert(type_idx, type_idx);
496        for &idx in cluster.inherent_impls.iter().chain(&cluster.trait_impls) {
497            item_to_cluster.insert(idx, type_idx);
498        }
499    }
500
501    let mut emitted_clusters = HashSet::new();
502    let mut nodes = Vec::new();
503    for (idx, module_item) in items.iter().enumerate() {
504        let item = module_item.item();
505        if let Some(&type_idx) = item_to_cluster.get(&idx) {
506            if !emitted_clusters.insert(type_idx) {
507                continue;
508            }
509            let Some(cluster) = clusters.get(&type_idx) else {
510                continue;
511            };
512            let Some(type_item) = items.get(cluster.type_idx) else {
513                continue;
514            };
515            let mut idxs = vec![cluster.type_idx];
516            idxs.extend(&cluster.inherent_impls);
517            idxs.extend(&cluster.trait_impls);
518            let source_idx = idxs.iter().copied().min().unwrap_or(cluster.type_idx);
519            nodes.push(ModuleNode {
520                order: OrderNode {
521                    source_idx,
522                    span: self::item_span(type_item.item()),
523                    group: Some(cluster.kind.group()),
524                    visibility: Some(cluster.visibility),
525                    label: format!("{} {}", cluster.kind.label(), cluster.name),
526                },
527                idxs,
528            });
529        } else if let Some(classified) = module_item.metadata().classified() {
530            nodes.push(ModuleNode {
531                order: OrderNode {
532                    source_idx: idx,
533                    span: classified.span,
534                    group: Some(classified.kind.group()),
535                    visibility: module_item.metadata().visibility(),
536                    label: self::item_label(item, classified.kind),
537                },
538                idxs: vec![idx],
539            });
540        }
541    }
542
543    nodes.sort_unstable_by_key(|node| node.order.source_idx);
544    nodes
545}
546
547pub(super) fn impl_nodes(item_impl: &syn::ItemImpl) -> Vec<OrderNode> {
548    item_impl
549        .items
550        .iter()
551        .enumerate()
552        .filter_map(|(source_idx, item)| {
553            let (kind, visibility) = match item {
554                syn::ImplItem::Const(item) => (ItemKind::Const, Some(VisibilityClass::from(&item.vis))),
555                syn::ImplItem::Fn(item) => (ItemKind::Fn, Some(VisibilityClass::from(&item.vis))),
556                syn::ImplItem::Type(item) => (ItemKind::TypeAlias, Some(VisibilityClass::from(&item.vis))),
557                syn::ImplItem::Macro(_) | syn::ImplItem::Verbatim(_) | _ => return None,
558            };
559            Some(OrderNode {
560                source_idx,
561                span: self::impl_item_span(item),
562                group: None,
563                visibility,
564                label: self::impl_item_label(item, kind),
565            })
566        })
567        .collect()
568}
569
570fn impl_item_label(item: &syn::ImplItem, kind: ItemKind) -> String {
571    let name = match item {
572        syn::ImplItem::Const(item) => Some(item.ident.to_string()),
573        syn::ImplItem::Fn(item) => Some(item.sig.ident.to_string()),
574        syn::ImplItem::Type(item) => Some(item.ident.to_string()),
575        syn::ImplItem::Macro(_) | syn::ImplItem::Verbatim(_) | _ => None,
576    };
577    name.map_or_else(|| kind.label().to_owned(), |name| format!("{} {name}", kind.label()))
578}
579
580fn impl_item_span(item: &syn::ImplItem) -> Span {
581    match item {
582        syn::ImplItem::Const(item) => item.const_token.span,
583        syn::ImplItem::Fn(item) => item.sig.fn_token.span,
584        syn::ImplItem::Type(item) => item.type_token.span,
585        syn::ImplItem::Macro(item) => item.mac.path.span(),
586        syn::ImplItem::Verbatim(tokens) => tokens.span(),
587        _ => item.span(),
588    }
589}
590
591#[cfg(test)]
592mod tests {
593    use super::*;
594
595    #[test]
596    fn test_module_item_lists_when_metadata_is_requested_returns_cached_item_details() {
597        let file = syn::parse_file(
598            r"
599            fn run() {}
600            #[cfg(test)]
601            mod tests {}
602            ",
603        )
604        .expect("source should parse");
605
606        let item_lists = module_item_lists(&file);
607        let root = item_lists.first().expect("root item list should exist");
608        let first_metadata = root.first().expect("root item should exist").metadata();
609
610        assert_eq!(root.len(), 2);
611        assert_eq!(first_metadata.classified().map(|item| item.kind), Some(ItemKind::Fn));
612        assert!(!first_metadata.is_test_module());
613        assert_eq!(first_metadata.visibility(), Some(VisibilityClass::Private));
614        let second_item = root.get(1).expect("second root item should exist");
615        assert_eq!(
616            second_item.metadata().classified().map(|item| item.kind),
617            Some(ItemKind::Mod)
618        );
619        assert!(second_item.metadata().is_test_module());
620    }
621
622    #[test]
623    fn test_item_metadata_when_item_has_no_module_visibility_marks_it_not_applicable() {
624        let file = syn::parse_file(
625            r"
626            struct Data;
627            impl Data {}
628            ",
629        )
630        .expect("source should parse");
631
632        let item_lists = module_item_lists(&file);
633        let root = item_lists.first().expect("root item list should exist");
634        let metadata = root.get(1).expect("impl item should exist").metadata();
635
636        assert!(matches!(
637            metadata,
638            ItemMetadata::Classified {
639                visibility: ItemVisibility::NotApplicable,
640                ..
641            }
642        ));
643    }
644}