Skip to main content

spec_drift/parsers/
rust.rs

1use crate::domain::{CodeFact, FactKind, Location};
2use crate::error::SpecDriftError;
3use std::path::Path;
4use syn::spanned::Spanned;
5
6pub struct RustParser;
7
8impl RustParser {
9    /// Parse `path` as a Rust file and extract every top-level (and nested)
10    /// item as a [`CodeFact`]. Free functions inside `impl` blocks are included
11    /// so method references like `Client::new` can be resolved.
12    pub fn parse(path: &Path) -> Result<Vec<CodeFact>, SpecDriftError> {
13        let source = std::fs::read_to_string(path).map_err(|e| SpecDriftError::Io {
14            path: path.to_path_buf(),
15            source: e,
16        })?;
17
18        let file = syn::parse_file(&source).map_err(|e| SpecDriftError::RustParse {
19            path: path.to_path_buf(),
20            source: e,
21        })?;
22
23        let mut facts = Vec::new();
24        collect_items(&file.items, path, &mut facts);
25        Ok(facts)
26    }
27}
28
29fn collect_items(items: &[syn::Item], path: &Path, facts: &mut Vec<CodeFact>) {
30    for item in items {
31        match item {
32            syn::Item::Fn(f) => {
33                push(
34                    facts,
35                    path,
36                    FactKind::Function,
37                    f.sig.ident.to_string(),
38                    f.span(),
39                );
40            }
41            syn::Item::Struct(s) => {
42                push(facts, path, FactKind::Struct, s.ident.to_string(), s.span());
43            }
44            syn::Item::Enum(e) => {
45                push(facts, path, FactKind::Enum, e.ident.to_string(), e.span());
46            }
47            syn::Item::Trait(t) => {
48                push(facts, path, FactKind::Trait, t.ident.to_string(), t.span());
49            }
50            syn::Item::Type(t) => {
51                push(
52                    facts,
53                    path,
54                    FactKind::TypeAlias,
55                    t.ident.to_string(),
56                    t.span(),
57                );
58            }
59            syn::Item::Const(c) => {
60                push(
61                    facts,
62                    path,
63                    FactKind::Constant,
64                    c.ident.to_string(),
65                    c.span(),
66                );
67            }
68            syn::Item::Static(s) => {
69                push(
70                    facts,
71                    path,
72                    FactKind::Constant,
73                    s.ident.to_string(),
74                    s.span(),
75                );
76            }
77            syn::Item::Macro(m) => {
78                if let Some(ident) = m.ident.as_ref() {
79                    push(facts, path, FactKind::Macro, ident.to_string(), m.span());
80                }
81            }
82            syn::Item::Mod(m) => {
83                push(facts, path, FactKind::Module, m.ident.to_string(), m.span());
84                if let Some((_, items)) = &m.content {
85                    collect_items(items, path, facts);
86                }
87            }
88            syn::Item::Impl(i) => {
89                for impl_item in &i.items {
90                    if let syn::ImplItem::Fn(f) = impl_item {
91                        push(
92                            facts,
93                            path,
94                            FactKind::Function,
95                            f.sig.ident.to_string(),
96                            f.span(),
97                        );
98                    }
99                }
100            }
101            _ => {}
102        }
103    }
104}
105
106fn push(
107    facts: &mut Vec<CodeFact>,
108    path: &Path,
109    kind: FactKind,
110    name: String,
111    span: proc_macro2::Span,
112) {
113    facts.push(CodeFact {
114        location: Location::new(path, span.start().line as u32),
115        kind,
116        name,
117    });
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn extracts_fn_struct_enum_trait_impl_methods() {
126        let tmp = tempfile::tempdir().unwrap();
127        let path = tmp.path().join("lib.rs");
128        std::fs::write(
129            &path,
130            r#"
131                pub fn hello() {}
132                pub struct Widget;
133                pub enum Color { Red, Blue }
134                pub trait Sing { fn sing(&self); }
135                impl Widget { pub fn make() {} }
136            "#,
137        )
138        .unwrap();
139
140        let facts = RustParser::parse(&path).unwrap();
141        let names: Vec<_> = facts.iter().map(|f| f.name.as_str()).collect();
142        assert!(names.contains(&"hello"));
143        assert!(names.contains(&"Widget"));
144        assert!(names.contains(&"Color"));
145        assert!(names.contains(&"Sing"));
146        assert!(names.contains(&"make"));
147    }
148}