Skip to main content

meta_ast/graph/
node.rs

1//! Graph node types for the dependency graph.
2//!
3//! This module defines the heterogeneous node types used in the CodeGraph:
4//! - `FileNode`: Represents a source file
5//! - `SymbolNode`: Represents an extracted symbol (function, class, etc.)
6//!
7//! Nodes are stored in a unified `NodeData` enum for use with petgraph.
8
9use std::path::PathBuf;
10
11use crate::language::LangId;
12use crate::model::{DataNodeId, FileId, SnapshotId, SourceRange, SymbolId, SymbolKind, Visibility};
13
14/// Unified node data enum for the dependency graph.
15#[derive(Debug, Clone)]
16#[non_exhaustive]
17pub enum NodeData {
18    File(FileNode),
19    Symbol(SymbolNode),
20    External(ExternalNode),
21    Data(DataGraphNode),
22}
23
24impl NodeData {
25    /// Returns a string identifier for the node kind.
26    pub fn kind_str(&self) -> &'static str {
27        match self {
28            NodeData::File(_) => "file",
29            NodeData::Symbol(_) => "symbol",
30            NodeData::External(_) => "external",
31            NodeData::Data(_) => "data",
32        }
33    }
34
35    /// Returns the FileId if this is a FileNode.
36    pub fn as_file(&self) -> Option<&FileNode> {
37        if let NodeData::File(f) = self {
38            Some(f)
39        } else {
40            None
41        }
42    }
43
44    /// Returns the SymbolId and associated data if this is a SymbolNode.
45    pub fn as_symbol(&self) -> Option<&SymbolNode> {
46        if let NodeData::Symbol(s) = self {
47            Some(s)
48        } else {
49            None
50        }
51    }
52
53    /// Returns the ExternalNode if this is an ExternalNode.
54    pub fn as_external(&self) -> Option<&ExternalNode> {
55        if let NodeData::External(e) = self {
56            Some(e)
57        } else {
58            None
59        }
60    }
61
62    /// Returns the DataGraphNode if this is a Data node.
63    pub fn as_data(&self) -> Option<&DataGraphNode> {
64        if let NodeData::Data(d) = self {
65            Some(d)
66        } else {
67            None
68        }
69    }
70
71    /// Returns the file path if this is a FileNode.
72    pub fn file_path(&self) -> Option<&PathBuf> {
73        self.as_file().map(|f| &f.path)
74    }
75
76    /// Returns the symbol name if this is a SymbolNode.
77    pub fn symbol_name(&self) -> Option<&str> {
78        self.as_symbol().map(|s| s.name.as_str())
79    }
80}
81
82/// Represents a source file in the dependency graph.
83#[derive(Debug, Clone)]
84pub struct FileNode {
85    /// Stable identifier for this file.
86    pub id: FileId,
87    /// Project-root-relative path for stable identification.
88    pub path: PathBuf,
89    /// Detected language for this file.
90    pub language: LangId,
91    /// Snapshot identifier for versioning support.
92    pub snapshot_id: SnapshotId,
93}
94
95/// Represents an external dependency (not in the project).
96#[derive(Debug, Clone)]
97pub struct ExternalNode {
98    /// Raw import path string (e.g., "react", "std::collections::HashMap")
99    pub raw_path: String,
100    /// Language of the importing file
101    pub language: LangId,
102    /// Classification result - `None` until dependency resolution runs.
103    pub classification: Option<ExternalClassification>,
104}
105
106/// Classification of an external dependency.
107#[derive(Debug, Clone, serde::Serialize)]
108#[non_exhaustive]
109pub enum ExternalClassification {
110    /// Successfully resolved to a known package (lockfile or manifest).
111    Classified {
112        package_name: String,
113        version: Option<String>,
114        language: LangId,
115        source: DependencySource,
116    },
117    /// Best-effort failed; kept as unresolved for transparency.
118    Unresolved { raw_path: String, reason: String },
119}
120
121/// Where the dependency information came from.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
123#[non_exhaustive]
124pub enum DependencySource {
125    Lockfile,
126    Manifest,
127}
128
129/// Represents an extracted symbol in the dependency graph.
130///
131/// Symbols are owned by exactly one FileNode (via Ownership edge) and may
132/// reference other symbols (via Reference edges).
133#[derive(Debug, Clone)]
134pub struct SymbolNode {
135    /// Stable identifier for this symbol.
136    pub id: SymbolId,
137    /// Symbol name as extracted from source.
138    pub name: String,
139    /// Symbol classification (function, class, etc.).
140    pub kind: SymbolKind,
141    /// Reference to the containing file.
142    pub file_id: FileId,
143    /// Visibility modifier if applicable.
144    pub visibility: Option<Visibility>,
145    /// Source location within the file.
146    pub source_range: SourceRange,
147}
148
149/// Represents a data-bearing node in the data/flow subgraph.
150#[derive(Debug, Clone)]
151pub struct DataGraphNode {
152    pub id: DataNodeId,
153    pub symbol_id: Option<SymbolId>,
154    pub name: Option<String>,
155    pub scope: crate::model::DataScope,
156    pub type_hint: Option<String>,
157    pub source_range: SourceRange,
158}
159
160impl FileNode {
161    /// Creates a new FileNode with the given properties.
162    pub fn new(id: FileId, path: PathBuf, language: LangId, snapshot_id: SnapshotId) -> Self {
163        Self {
164            id,
165            path,
166            language,
167            snapshot_id,
168        }
169    }
170
171    /// Returns the file name component of the path.
172    pub fn file_name(&self) -> Option<&str> {
173        self.path.file_name().and_then(|n| n.to_str())
174    }
175
176    /// Returns the file extension if present.
177    pub fn extension(&self) -> Option<&str> {
178        self.path.extension().and_then(|e| e.to_str())
179    }
180}
181
182impl SymbolNode {
183    /// Creates a new SymbolNode from an extracted Symbol.
184    ///
185    /// This factory method bridges the model layer to the graph layer.
186    pub fn from_symbol(symbol: &crate::model::Symbol, file_id: FileId) -> Self {
187        Self {
188            id: symbol.id,
189            name: symbol.name.clone(),
190            kind: symbol.kind,
191            file_id,
192            visibility: symbol.visibility,
193            source_range: symbol.source_range.clone(),
194        }
195    }
196}
197
198impl DataGraphNode {
199    /// Creates a new DataGraphNode from an extracted DataNode.
200    pub fn from_data_node(data: &crate::model::DataNode) -> Self {
201        Self {
202            id: data.id,
203            symbol_id: data.symbol_id,
204            name: data.name.clone(),
205            scope: data.scope,
206            type_hint: data.type_hint.clone(),
207            source_range: data.source_range.clone(),
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use crate::model::{LineColumn, SourceRange};
216
217    fn test_path() -> PathBuf {
218        PathBuf::from("src/main.rs")
219    }
220
221    fn test_source_range() -> SourceRange {
222        SourceRange {
223            byte_start: 0,
224            byte_end: 10,
225            start: LineColumn { line: 0, column: 0 },
226            end: LineColumn {
227                line: 0,
228                column: 10,
229            },
230        }
231    }
232
233    #[test]
234    fn file_node_creation() {
235        let file_id = FileId::new(1).unwrap();
236        let snapshot_id = SnapshotId::new(1).unwrap();
237        let node = FileNode::new(file_id, test_path(), LangId::Rust, snapshot_id);
238
239        assert_eq!(node.id, file_id);
240        assert_eq!(node.path, test_path());
241        assert_eq!(node.language, LangId::Rust);
242        assert_eq!(node.snapshot_id, snapshot_id);
243    }
244
245    #[test]
246    fn file_node_file_name() {
247        let node = FileNode::new(
248            FileId::new(1).unwrap(),
249            PathBuf::from("src/main.rs"),
250            LangId::Rust,
251            SnapshotId::new(1).unwrap(),
252        );
253        assert_eq!(node.file_name(), Some("main.rs"));
254    }
255
256    #[test]
257    fn file_node_extension() {
258        let node = FileNode::new(
259            FileId::new(1).unwrap(),
260            PathBuf::from("test.py"),
261            LangId::Python,
262            SnapshotId::new(1).unwrap(),
263        );
264        assert_eq!(node.extension(), Some("py"));
265    }
266
267    #[test]
268    fn symbol_node_creation() {
269        let symbol = crate::model::Symbol {
270            id: SymbolId::new(42).unwrap(),
271            name: "test_function".to_string(),
272            kind: SymbolKind::Function,
273            language: LangId::Rust,
274            file_path: test_path(),
275            source_range: test_source_range(),
276            name_range: None,
277            visibility: Some(Visibility::Public),
278            signature: None,
279            docstring: None,
280            is_async: false,
281        };
282
283        let file_id = FileId::new(7).unwrap();
284        let node = SymbolNode::from_symbol(&symbol, file_id);
285
286        assert_eq!(node.id, SymbolId::new(42).unwrap());
287        assert_eq!(node.name, "test_function");
288        assert_eq!(node.kind, SymbolKind::Function);
289        assert_eq!(node.file_id, file_id);
290        assert_eq!(node.visibility, Some(Visibility::Public));
291    }
292
293    #[test]
294    fn node_data_file_variant() {
295        let file_node = FileNode::new(
296            FileId::new(1).unwrap(),
297            test_path(),
298            LangId::Rust,
299            SnapshotId::new(1).unwrap(),
300        );
301        let node_data = NodeData::File(file_node);
302
303        assert_eq!(node_data.kind_str(), "file");
304        assert!(node_data.as_file().is_some());
305        assert!(node_data.as_symbol().is_none());
306        assert_eq!(node_data.file_path(), Some(&test_path()));
307        assert_eq!(node_data.symbol_name(), None);
308    }
309
310    #[test]
311    fn node_data_symbol_variant() {
312        let symbol_node = SymbolNode {
313            id: SymbolId::new(1).unwrap(),
314            name: "my_func".to_string(),
315            kind: SymbolKind::Function,
316            file_id: FileId::new(1).unwrap(),
317            visibility: None,
318            source_range: test_source_range(),
319        };
320        let node_data = NodeData::Symbol(symbol_node);
321
322        assert_eq!(node_data.kind_str(), "symbol");
323        assert!(node_data.as_symbol().is_some());
324        assert!(node_data.as_file().is_none());
325        assert_eq!(node_data.file_path(), None);
326        assert_eq!(node_data.symbol_name(), Some("my_func"));
327    }
328
329    #[test]
330    fn data_graph_node_from_model() {
331        use crate::model::{DataNode, DataNodeId, DataScope};
332        let data = DataNode {
333            id: DataNodeId::new(5).unwrap(),
334            symbol_id: None,
335            name: Some("var_x".into()),
336            scope: DataScope::Local,
337            type_hint: Some("int".into()),
338            source_range: test_source_range(),
339        };
340        let gnode = DataGraphNode::from_data_node(&data);
341        assert_eq!(gnode.id, DataNodeId::new(5).unwrap());
342        assert_eq!(gnode.name.as_deref(), Some("var_x"));
343        assert_eq!(gnode.scope, DataScope::Local);
344        assert_eq!(gnode.type_hint.as_deref(), Some("int"));
345        assert!(gnode.symbol_id.is_none());
346    }
347
348    #[test]
349    fn node_data_data_variant() {
350        let dnode = DataGraphNode {
351            id: crate::model::DataNodeId::new(1).unwrap(),
352            symbol_id: Some(SymbolId::new(3).unwrap()),
353            name: Some("param".into()),
354            scope: crate::model::DataScope::Parameter,
355            type_hint: Some("str".into()),
356            source_range: test_source_range(),
357        };
358        let node_data = NodeData::Data(dnode);
359        assert_eq!(node_data.kind_str(), "data");
360        assert!(node_data.as_data().is_some());
361        assert!(node_data.as_file().is_none());
362        assert!(node_data.as_symbol().is_none());
363        assert_eq!(node_data.as_data().unwrap().name.as_deref(), Some("param"));
364    }
365}