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::{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}
22
23impl NodeData {
24    /// Returns a string identifier for the node kind.
25    pub fn kind_str(&self) -> &'static str {
26        match self {
27            NodeData::File(_) => "file",
28            NodeData::Symbol(_) => "symbol",
29            NodeData::External(_) => "external",
30        }
31    }
32
33    /// Returns the FileId if this is a FileNode.
34    pub fn as_file(&self) -> Option<&FileNode> {
35        if let NodeData::File(f) = self {
36            Some(f)
37        } else {
38            None
39        }
40    }
41
42    /// Returns the SymbolId and associated data if this is a SymbolNode.
43    pub fn as_symbol(&self) -> Option<&SymbolNode> {
44        if let NodeData::Symbol(s) = self {
45            Some(s)
46        } else {
47            None
48        }
49    }
50
51    /// Returns the ExternalNode if this is an ExternalNode.
52    pub fn as_external(&self) -> Option<&ExternalNode> {
53        if let NodeData::External(e) = self {
54            Some(e)
55        } else {
56            None
57        }
58    }
59
60    /// Returns the file path if this is a FileNode.
61    pub fn file_path(&self) -> Option<&PathBuf> {
62        self.as_file().map(|f| &f.path)
63    }
64
65    /// Returns the symbol name if this is a SymbolNode.
66    pub fn symbol_name(&self) -> Option<&str> {
67        self.as_symbol().map(|s| s.name.as_str())
68    }
69}
70
71/// Represents a source file in the dependency graph.
72#[derive(Debug, Clone)]
73pub struct FileNode {
74    /// Stable identifier for this file.
75    pub id: FileId,
76    /// Project-root-relative path for stable identification.
77    pub path: PathBuf,
78    /// Detected language for this file.
79    pub language: LangId,
80    /// Snapshot identifier for versioning support.
81    pub snapshot_id: SnapshotId,
82}
83
84/// Represents an external dependency (not in the project).
85#[derive(Debug, Clone)]
86pub struct ExternalNode {
87    /// Raw import path string (e.g., "react", "std::collections::HashMap")
88    pub raw_path: String,
89    /// Language of the importing file
90    pub language: LangId,
91    /// Classification result - `None` until dependency resolution runs.
92    pub classification: Option<ExternalClassification>,
93}
94
95/// Classification of an external dependency.
96#[derive(Debug, Clone, serde::Serialize)]
97#[non_exhaustive]
98pub enum ExternalClassification {
99    /// Successfully resolved to a known package (lockfile or manifest).
100    Classified {
101        package_name: String,
102        version: Option<String>,
103        language: LangId,
104        source: DependencySource,
105    },
106    /// Best-effort failed; kept as unresolved for transparency.
107    Unresolved { raw_path: String, reason: String },
108}
109
110/// Where the dependency information came from.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
112#[non_exhaustive]
113pub enum DependencySource {
114    Lockfile,
115    Manifest,
116}
117
118/// Represents an extracted symbol in the dependency graph.
119///
120/// Symbols are owned by exactly one FileNode (via Ownership edge) and may
121/// reference other symbols (via Reference edges).
122#[derive(Debug, Clone)]
123pub struct SymbolNode {
124    /// Stable identifier for this symbol.
125    pub id: SymbolId,
126    /// Symbol name as extracted from source.
127    pub name: String,
128    /// Symbol classification (function, class, etc.).
129    pub kind: SymbolKind,
130    /// Reference to the containing file.
131    pub file_id: FileId,
132    /// Visibility modifier if applicable.
133    pub visibility: Option<Visibility>,
134    /// Source location within the file.
135    pub source_range: SourceRange,
136}
137
138impl FileNode {
139    /// Creates a new FileNode with the given properties.
140    pub fn new(id: FileId, path: PathBuf, language: LangId, snapshot_id: SnapshotId) -> Self {
141        Self {
142            id,
143            path,
144            language,
145            snapshot_id,
146        }
147    }
148
149    /// Returns the file name component of the path.
150    pub fn file_name(&self) -> Option<&str> {
151        self.path.file_name().and_then(|n| n.to_str())
152    }
153
154    /// Returns the file extension if present.
155    pub fn extension(&self) -> Option<&str> {
156        self.path.extension().and_then(|e| e.to_str())
157    }
158}
159
160impl SymbolNode {
161    /// Creates a new SymbolNode from an extracted Symbol.
162    ///
163    /// This factory method bridges the model layer to the graph layer.
164    pub fn from_symbol(symbol: &crate::model::Symbol, file_id: FileId) -> Self {
165        Self {
166            id: symbol.id,
167            name: symbol.name.clone(),
168            kind: symbol.kind,
169            file_id,
170            visibility: symbol.visibility,
171            source_range: symbol.source_range.clone(),
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::model::{LineColumn, SourceRange};
180
181    fn test_path() -> PathBuf {
182        PathBuf::from("src/main.rs")
183    }
184
185    fn test_source_range() -> SourceRange {
186        SourceRange {
187            byte_start: 0,
188            byte_end: 10,
189            start: LineColumn { line: 0, column: 0 },
190            end: LineColumn {
191                line: 0,
192                column: 10,
193            },
194        }
195    }
196
197    #[test]
198    fn file_node_creation() {
199        let file_id = FileId::from(0);
200        let snapshot_id = SnapshotId::from(1);
201        let node = FileNode::new(file_id, test_path(), LangId::Rust, snapshot_id);
202
203        assert_eq!(node.id, file_id);
204        assert_eq!(node.path, test_path());
205        assert_eq!(node.language, LangId::Rust);
206        assert_eq!(node.snapshot_id, snapshot_id);
207    }
208
209    #[test]
210    fn file_node_file_name() {
211        let node = FileNode::new(
212            FileId::from(0),
213            PathBuf::from("src/main.rs"),
214            LangId::Rust,
215            SnapshotId::from(0),
216        );
217        assert_eq!(node.file_name(), Some("main.rs"));
218    }
219
220    #[test]
221    fn file_node_extension() {
222        let node = FileNode::new(
223            FileId::from(0),
224            PathBuf::from("test.py"),
225            LangId::Python,
226            SnapshotId::from(0),
227        );
228        assert_eq!(node.extension(), Some("py"));
229    }
230
231    #[test]
232    fn symbol_node_creation() {
233        let symbol = crate::model::Symbol {
234            id: SymbolId::from(42),
235            name: "test_function".to_string(),
236            kind: SymbolKind::Function,
237            language: LangId::Rust,
238            file_path: test_path(),
239            source_range: test_source_range(),
240            visibility: Some(Visibility::Public),
241            signature: None,
242            docstring: None,
243            is_async: false,
244        };
245
246        let file_id = FileId::from(7);
247        let node = SymbolNode::from_symbol(&symbol, file_id);
248
249        assert_eq!(node.id, SymbolId::from(42));
250        assert_eq!(node.name, "test_function");
251        assert_eq!(node.kind, SymbolKind::Function);
252        assert_eq!(node.file_id, file_id);
253        assert_eq!(node.visibility, Some(Visibility::Public));
254    }
255
256    #[test]
257    fn node_data_file_variant() {
258        let file_node = FileNode::new(
259            FileId::from(0),
260            test_path(),
261            LangId::Rust,
262            SnapshotId::from(0),
263        );
264        let node_data = NodeData::File(file_node);
265
266        assert_eq!(node_data.kind_str(), "file");
267        assert!(node_data.as_file().is_some());
268        assert!(node_data.as_symbol().is_none());
269        assert_eq!(node_data.file_path(), Some(&test_path()));
270        assert_eq!(node_data.symbol_name(), None);
271    }
272
273    #[test]
274    fn node_data_symbol_variant() {
275        let symbol_node = SymbolNode {
276            id: SymbolId::from(1),
277            name: "my_func".to_string(),
278            kind: SymbolKind::Function,
279            file_id: FileId::from(0),
280            visibility: None,
281            source_range: test_source_range(),
282        };
283        let node_data = NodeData::Symbol(symbol_node);
284
285        assert_eq!(node_data.kind_str(), "symbol");
286        assert!(node_data.as_symbol().is_some());
287        assert!(node_data.as_file().is_none());
288        assert_eq!(node_data.file_path(), None);
289        assert_eq!(node_data.symbol_name(), Some("my_func"));
290    }
291}