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