Skip to main content

meta_ast/model/
mod.rs

1//! Canonical symbol model and IR types.
2//!
3//! Defines `Symbol`, `UnresolvedImport`, `UnresolvedReference`,
4//! `FileExtraction`, and supporting types (`SymbolKind`, `Visibility`,
5//! `SourceRange`, `LineColumn`). This is the core data model with
6//! zero knowledge of parsing, I/O, or language specifics.
7
8pub mod ids;
9pub mod output;
10
11use std::path::PathBuf;
12
13pub use ids::{FileId, IdGenerator, SnapshotId, SymbolId};
14pub use output::{ClassEntry, FuncEntry, InspectOutput, ObjectEntry};
15
16use crate::language::LangId;
17use serde::Serialize;
18
19#[derive(Debug, Clone, Serialize)]
20pub struct UnresolvedImport {
21    pub import_specifier: String,
22    pub alias: Option<String>,
23    pub symbol: Option<String>,
24    pub star: bool,
25    pub range: SourceRange,
26}
27
28#[derive(Debug, Clone, Serialize)]
29pub struct UnresolvedReference {
30    pub name: String,
31    pub range: SourceRange,
32}
33
34#[derive(Debug, Clone, Serialize)]
35pub struct FileExtraction {
36    pub path: PathBuf,
37    pub lang: LangId,
38    pub symbols: Vec<Symbol>,
39    pub imports: Vec<UnresolvedImport>,
40    pub references: Vec<UnresolvedReference>,
41    pub diagnostics: Vec<crate::error::Diagnostic>,
42    /// Total number of tree-sitter AST nodes in the parse tree.
43    pub ast_node_count: usize,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
47pub struct LineColumn {
48    pub line: usize,
49    pub column: usize,
50}
51
52#[derive(Debug, Clone, Serialize)]
53pub struct SourceRange {
54    pub byte_start: usize,
55    pub byte_end: usize,
56    pub start: LineColumn,
57    pub end: LineColumn,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
61#[non_exhaustive]
62pub enum SymbolKind {
63    Function,
64    Method,
65    Class,
66    Struct,
67    Interface,
68    Trait,
69    Enum,
70    Object,
71    Constant,
72    Static,
73    Module,
74    Namespace,
75    TypeAlias,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
79#[non_exhaustive]
80pub enum Visibility {
81    Public,
82    Private,
83}
84
85#[derive(Debug, Clone, Serialize)]
86pub struct Symbol {
87    pub id: SymbolId,
88    pub name: String,
89    pub kind: SymbolKind,
90    pub language: LangId,
91    pub file_path: PathBuf,
92    pub source_range: SourceRange,
93    pub visibility: Option<Visibility>,
94    pub signature: Option<String>,
95    pub docstring: Option<String>,
96    pub is_async: bool,
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use serde_json;
103
104    fn sample_source_range() -> SourceRange {
105        SourceRange {
106            byte_start: 0,
107            byte_end: 10,
108            start: LineColumn { line: 1, column: 0 },
109            end: LineColumn {
110                line: 1,
111                column: 10,
112            },
113        }
114    }
115
116    #[test]
117    fn symbol_construction_all_fields() {
118        let sym = Symbol {
119            id: SymbolId(42),
120            name: "my_func".into(),
121            kind: SymbolKind::Function,
122            language: LangId::Rust,
123            file_path: PathBuf::from("src/main.rs"),
124            source_range: sample_source_range(),
125            visibility: Some(Visibility::Public),
126            signature: Some("fn my_func() -> bool".into()),
127            docstring: Some("does a thing".into()),
128            is_async: true,
129        };
130        assert_eq!(sym.id, SymbolId(42));
131        assert_eq!(sym.name, "my_func");
132        assert!(matches!(sym.kind, SymbolKind::Function));
133        assert_eq!(sym.language, LangId::Rust);
134        assert_eq!(sym.file_path, PathBuf::from("src/main.rs"));
135        assert_eq!(sym.visibility, Some(Visibility::Public));
136        assert_eq!(sym.signature.as_deref(), Some("fn my_func() -> bool"));
137        assert_eq!(sym.docstring.as_deref(), Some("does a thing"));
138        assert!(sym.is_async);
139    }
140
141    #[test]
142    fn symbol_with_optional_fields_none() {
143        let sym = Symbol {
144            id: SymbolId(1),
145            name: "x".into(),
146            kind: SymbolKind::Constant,
147            language: LangId::Python,
148            file_path: PathBuf::from("a.py"),
149            source_range: sample_source_range(),
150            visibility: None,
151            signature: None,
152            docstring: None,
153            is_async: false,
154        };
155        assert!(sym.visibility.is_none());
156        assert!(sym.signature.is_none());
157        assert!(sym.docstring.is_none());
158        assert!(!sym.is_async);
159    }
160
161    #[test]
162    fn source_range_fields() {
163        let sr = sample_source_range();
164        assert_eq!(sr.byte_start, 0);
165        assert_eq!(sr.byte_end, 10);
166        assert_eq!(sr.start, LineColumn { line: 1, column: 0 });
167        assert_eq!(
168            sr.end,
169            LineColumn {
170                line: 1,
171                column: 10
172            }
173        );
174    }
175
176    #[test]
177    fn line_column_zero_indexed() {
178        let lc = LineColumn { line: 0, column: 0 };
179        assert_eq!(lc.line, 0);
180        assert_eq!(lc.column, 0);
181    }
182
183    #[test]
184    fn visibility_serialization() {
185        assert_eq!(
186            serde_json::to_string(&Visibility::Public).unwrap(),
187            "\"Public\""
188        );
189        assert_eq!(
190            serde_json::to_string(&Visibility::Private).unwrap(),
191            "\"Private\""
192        );
193    }
194
195    #[test]
196    fn symbol_kind_all_variants_serialize() {
197        let variants: Vec<SymbolKind> = vec![
198            SymbolKind::Function,
199            SymbolKind::Method,
200            SymbolKind::Class,
201            SymbolKind::Struct,
202            SymbolKind::Interface,
203            SymbolKind::Trait,
204            SymbolKind::Enum,
205            SymbolKind::Object,
206            SymbolKind::Constant,
207            SymbolKind::Static,
208            SymbolKind::Module,
209            SymbolKind::Namespace,
210            SymbolKind::TypeAlias,
211        ];
212        for v in &variants {
213            let json = serde_json::to_string(v).unwrap();
214            assert!(
215                json.starts_with('"') && json.ends_with('"'),
216                "expected a JSON string, got: {json}"
217            );
218            assert!(
219                json.len() > 2,
220                "expected non-empty variant name, got: {json}"
221            );
222        }
223    }
224
225    #[test]
226    fn symbol_serde_roundtrip() {
227        let sym = Symbol {
228            id: SymbolId(7),
229            name: "roundtrip_fn".into(),
230            kind: SymbolKind::Method,
231            language: LangId::Go,
232            file_path: PathBuf::from("main.go"),
233            source_range: sample_source_range(),
234            visibility: Some(Visibility::Private),
235            signature: Some("func (t T) roundtripFn()".into()),
236            docstring: Some("doc".into()),
237            is_async: false,
238        };
239        let json = serde_json::to_string(&sym).unwrap();
240        let val: serde_json::Value = serde_json::from_str(&json).unwrap();
241        assert_eq!(val["name"], "roundtrip_fn");
242        assert_eq!(val["kind"], "Method");
243        assert_eq!(val["is_async"], false);
244    }
245}