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