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::{DataNodeId, 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    #[cfg(feature = "metacall-deploy")]
45    pub call_sites: Vec<crate::deploy::scanner::CallSite>,
46    #[cfg(feature = "dataflow")]
47    pub data_nodes: Vec<DataNode>,
48    #[cfg(feature = "dataflow")]
49    pub flow_edges: Vec<FlowEdge>,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
53pub struct LineColumn {
54    pub line: usize,
55    pub column: usize,
56}
57
58#[derive(Debug, Clone, Serialize)]
59pub struct SourceRange {
60    pub byte_start: usize,
61    pub byte_end: usize,
62    pub start: LineColumn,
63    pub end: LineColumn,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
67#[non_exhaustive]
68pub enum SymbolKind {
69    Function,
70    Method,
71    Class,
72    Struct,
73    Interface,
74    Trait,
75    Enum,
76    Object,
77    Constant,
78    Static,
79    Module,
80    Namespace,
81    TypeAlias,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
85#[non_exhaustive]
86pub enum Visibility {
87    Public,
88    Private,
89}
90
91#[derive(Debug, Clone, Serialize)]
92pub struct Symbol {
93    pub id: SymbolId,
94    pub name: String,
95    pub kind: SymbolKind,
96    pub language: LangId,
97    pub file_path: PathBuf,
98    pub source_range: SourceRange,
99    pub visibility: Option<Visibility>,
100    pub signature: Option<String>,
101    pub docstring: Option<String>,
102    pub is_async: bool,
103}
104
105/// Classification of a data-bearing entity's visibility scope.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
107#[non_exhaustive]
108pub enum DataScope {
109    Local,
110    Parameter,
111    Member,
112    Closure,
113    Temporary,
114}
115
116impl DataScope {
117    pub fn as_str(self) -> &'static str {
118        match self {
119            DataScope::Local => "local",
120            DataScope::Parameter => "parameter",
121            DataScope::Member => "member",
122            DataScope::Closure => "closure",
123            DataScope::Temporary => "temporary",
124        }
125    }
126}
127
128/// A value-bearing node for def-use and flow analysis.
129#[derive(Debug, Clone, Serialize)]
130pub struct DataNode {
131    pub id: DataNodeId,
132    pub symbol_id: Option<SymbolId>,
133    pub name: Option<String>,
134    pub scope: DataScope,
135    pub type_hint: Option<String>,
136    pub source_range: SourceRange,
137}
138
139/// A def-use or dataflow edge between two DataNodes.
140#[derive(Debug, Clone, Serialize)]
141pub struct FlowEdge {
142    pub source: DataNodeId,
143    pub target: DataNodeId,
144    pub kind: FlowKind,
145    pub confidence: f32,
146}
147
148/// Semantic kind of a dataflow edge.
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
150#[non_exhaustive]
151pub enum FlowKind {
152    DefUse,
153    Argument,
154    Return,
155    FieldAccess,
156}
157
158impl FlowKind {
159    pub fn as_str(self) -> &'static str {
160        match self {
161            FlowKind::DefUse => "def_use",
162            FlowKind::Argument => "argument",
163            FlowKind::Return => "return",
164            FlowKind::FieldAccess => "field_access",
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use serde_json;
173
174    fn sample_source_range() -> SourceRange {
175        SourceRange {
176            byte_start: 0,
177            byte_end: 10,
178            start: LineColumn { line: 1, column: 0 },
179            end: LineColumn {
180                line: 1,
181                column: 10,
182            },
183        }
184    }
185
186    #[test]
187    fn symbol_construction_all_fields() {
188        let sym = Symbol {
189            id: SymbolId::new(42).unwrap(),
190            name: "my_func".into(),
191            kind: SymbolKind::Function,
192            language: LangId::Rust,
193            file_path: PathBuf::from("src/main.rs"),
194            source_range: sample_source_range(),
195            visibility: Some(Visibility::Public),
196            signature: Some("fn my_func() -> bool".into()),
197            docstring: Some("does a thing".into()),
198            is_async: true,
199        };
200        assert_eq!(sym.id, SymbolId::new(42).unwrap());
201        assert_eq!(sym.name, "my_func");
202        assert!(matches!(sym.kind, SymbolKind::Function));
203        assert_eq!(sym.language, LangId::Rust);
204        assert_eq!(sym.file_path, PathBuf::from("src/main.rs"));
205        assert_eq!(sym.visibility, Some(Visibility::Public));
206        assert_eq!(sym.signature.as_deref(), Some("fn my_func() -> bool"));
207        assert_eq!(sym.docstring.as_deref(), Some("does a thing"));
208        assert!(sym.is_async);
209    }
210
211    #[test]
212    fn symbol_with_optional_fields_none() {
213        let sym = Symbol {
214            id: SymbolId::new(1).unwrap(),
215            name: "x".into(),
216            kind: SymbolKind::Constant,
217            language: LangId::Python,
218            file_path: PathBuf::from("a.py"),
219            source_range: sample_source_range(),
220            visibility: None,
221            signature: None,
222            docstring: None,
223            is_async: false,
224        };
225        assert!(sym.visibility.is_none());
226        assert!(sym.signature.is_none());
227        assert!(sym.docstring.is_none());
228        assert!(!sym.is_async);
229    }
230
231    #[test]
232    fn source_range_fields() {
233        let sr = sample_source_range();
234        assert_eq!(sr.byte_start, 0);
235        assert_eq!(sr.byte_end, 10);
236        assert_eq!(sr.start, LineColumn { line: 1, column: 0 });
237        assert_eq!(
238            sr.end,
239            LineColumn {
240                line: 1,
241                column: 10
242            }
243        );
244    }
245
246    #[test]
247    fn line_column_zero_indexed() {
248        let lc = LineColumn { line: 0, column: 0 };
249        assert_eq!(lc.line, 0);
250        assert_eq!(lc.column, 0);
251    }
252
253    #[test]
254    fn visibility_serialization() {
255        assert_eq!(
256            serde_json::to_string(&Visibility::Public).unwrap(),
257            "\"Public\""
258        );
259        assert_eq!(
260            serde_json::to_string(&Visibility::Private).unwrap(),
261            "\"Private\""
262        );
263    }
264
265    #[test]
266    fn symbol_kind_all_variants_serialize() {
267        let variants: Vec<SymbolKind> = vec![
268            SymbolKind::Function,
269            SymbolKind::Method,
270            SymbolKind::Class,
271            SymbolKind::Struct,
272            SymbolKind::Interface,
273            SymbolKind::Trait,
274            SymbolKind::Enum,
275            SymbolKind::Object,
276            SymbolKind::Constant,
277            SymbolKind::Static,
278            SymbolKind::Module,
279            SymbolKind::Namespace,
280            SymbolKind::TypeAlias,
281        ];
282        for v in &variants {
283            let json = serde_json::to_string(v).unwrap();
284            assert!(
285                json.starts_with('"') && json.ends_with('"'),
286                "expected a JSON string, got: {json}"
287            );
288            assert!(
289                json.len() > 2,
290                "expected non-empty variant name, got: {json}"
291            );
292        }
293    }
294
295    #[test]
296    fn symbol_serde_roundtrip() {
297        let sym = Symbol {
298            id: SymbolId::new(7).unwrap(),
299            name: "roundtrip_fn".into(),
300            kind: SymbolKind::Method,
301            language: LangId::Go,
302            file_path: PathBuf::from("main.go"),
303            source_range: sample_source_range(),
304            visibility: Some(Visibility::Private),
305            signature: Some("func (t T) roundtripFn()".into()),
306            docstring: Some("doc".into()),
307            is_async: false,
308        };
309        let json = serde_json::to_string(&sym).unwrap();
310        let val: serde_json::Value = serde_json::from_str(&json).unwrap();
311        assert_eq!(val["name"], "roundtrip_fn");
312        assert_eq!(val["kind"], "Method");
313        assert_eq!(val["is_async"], false);
314    }
315
316    #[test]
317    fn data_scope_as_str_all_variants() {
318        assert_eq!(DataScope::Local.as_str(), "local");
319        assert_eq!(DataScope::Parameter.as_str(), "parameter");
320        assert_eq!(DataScope::Member.as_str(), "member");
321        assert_eq!(DataScope::Closure.as_str(), "closure");
322        assert_eq!(DataScope::Temporary.as_str(), "temporary");
323    }
324
325    #[test]
326    fn data_scope_serialization() {
327        assert_eq!(
328            serde_json::to_string(&DataScope::Local).unwrap(),
329            "\"Local\""
330        );
331        assert_eq!(
332            serde_json::to_string(&DataScope::Parameter).unwrap(),
333            "\"Parameter\""
334        );
335    }
336
337    #[test]
338    fn flow_kind_as_str_all_variants() {
339        assert_eq!(FlowKind::DefUse.as_str(), "def_use");
340        assert_eq!(FlowKind::Argument.as_str(), "argument");
341        assert_eq!(FlowKind::Return.as_str(), "return");
342        assert_eq!(FlowKind::FieldAccess.as_str(), "field_access");
343    }
344
345    #[test]
346    fn flow_kind_serialization() {
347        assert_eq!(
348            serde_json::to_string(&FlowKind::DefUse).unwrap(),
349            "\"DefUse\""
350        );
351        assert_eq!(
352            serde_json::to_string(&FlowKind::Argument).unwrap(),
353            "\"Argument\""
354        );
355        assert_eq!(
356            serde_json::to_string(&FlowKind::Return).unwrap(),
357            "\"Return\""
358        );
359        assert_eq!(
360            serde_json::to_string(&FlowKind::FieldAccess).unwrap(),
361            "\"FieldAccess\""
362        );
363    }
364
365    #[test]
366    fn data_node_construction_and_serde() {
367        let data = DataNode {
368            id: DataNodeId::new(10).unwrap(),
369            symbol_id: Some(SymbolId::new(5).unwrap()),
370            name: Some("x".into()),
371            scope: DataScope::Local,
372            type_hint: Some("int".into()),
373            source_range: sample_source_range(),
374        };
375        let json = serde_json::to_string(&data).unwrap();
376        let val: serde_json::Value = serde_json::from_str(&json).unwrap();
377        assert_eq!(val["name"], "x");
378        assert_eq!(val["scope"], "Local");
379        assert_eq!(val["type_hint"], "int");
380    }
381
382    #[test]
383    fn data_node_optional_fields_none() {
384        let data = DataNode {
385            id: DataNodeId::new(1).unwrap(),
386            symbol_id: None,
387            name: None,
388            scope: DataScope::Temporary,
389            type_hint: None,
390            source_range: sample_source_range(),
391        };
392        assert!(data.symbol_id.is_none());
393        assert!(data.name.is_none());
394        assert!(data.type_hint.is_none());
395    }
396
397    #[test]
398    fn flow_edge_serde() {
399        let edge = FlowEdge {
400            source: DataNodeId::new(1).unwrap(),
401            target: DataNodeId::new(2).unwrap(),
402            kind: FlowKind::Argument,
403            confidence: 0.85,
404        };
405        let json = serde_json::to_string(&edge).unwrap();
406        let val: serde_json::Value = serde_json::from_str(&json).unwrap();
407        assert_eq!(val["source"], 1);
408        assert_eq!(val["target"], 2);
409        assert_eq!(val["kind"], "Argument");
410        assert_eq!(val["confidence"], 0.85);
411    }
412
413    #[test]
414    fn data_node_id_serde_roundtrip() {
415        let original = DataNodeId::new(99).unwrap();
416        let json = serde_json::to_string(&original).unwrap();
417        let roundtrip: DataNodeId = serde_json::from_str(&json).unwrap();
418        assert_eq!(original, roundtrip);
419    }
420}