1use std::path::PathBuf;
10
11use crate::language::LangId;
12use crate::model::{FileId, SnapshotId, SourceRange, SymbolId, SymbolKind, Visibility};
13
14#[derive(Debug, Clone)]
16#[non_exhaustive]
17pub enum NodeData {
18 File(FileNode),
19 Symbol(SymbolNode),
20 External(ExternalNode),
21}
22
23impl NodeData {
24 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 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 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 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 pub fn file_path(&self) -> Option<&PathBuf> {
62 self.as_file().map(|f| &f.path)
63 }
64
65 pub fn symbol_name(&self) -> Option<&str> {
67 self.as_symbol().map(|s| s.name.as_str())
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct FileNode {
74 pub id: FileId,
76 pub path: PathBuf,
78 pub language: LangId,
80 pub snapshot_id: SnapshotId,
82}
83
84#[derive(Debug, Clone)]
86pub struct ExternalNode {
87 pub raw_path: String,
89 pub language: LangId,
91 pub classification: Option<ExternalClassification>,
93}
94
95#[derive(Debug, Clone, serde::Serialize)]
97#[non_exhaustive]
98pub enum ExternalClassification {
99 Classified {
101 package_name: String,
102 version: Option<String>,
103 language: LangId,
104 source: DependencySource,
105 },
106 Unresolved { raw_path: String, reason: String },
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
112#[non_exhaustive]
113pub enum DependencySource {
114 Lockfile,
115 Manifest,
116}
117
118#[derive(Debug, Clone)]
123pub struct SymbolNode {
124 pub id: SymbolId,
126 pub name: String,
128 pub kind: SymbolKind,
130 pub file_id: FileId,
132 pub visibility: Option<Visibility>,
134 pub source_range: SourceRange,
136}
137
138impl FileNode {
139 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 pub fn file_name(&self) -> Option<&str> {
151 self.path.file_name().and_then(|n| n.to_str())
152 }
153
154 pub fn extension(&self) -> Option<&str> {
156 self.path.extension().and_then(|e| e.to_str())
157 }
158}
159
160impl SymbolNode {
161 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}