1use blake3::Hasher;
7use serde::{Deserialize, Serialize};
8use std::fmt;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct NodeId([u8; 16]);
15
16impl NodeId {
17 pub fn new(repo_id: &str, file_path: &Path, span: &Span, kind: &NodeKind) -> Self {
19 let mut hasher = Hasher::new();
20 hasher.update(repo_id.as_bytes());
21 hasher.update(file_path.to_string_lossy().as_bytes());
22 hasher.update(&span.start_byte.to_le_bytes());
23 hasher.update(&span.end_byte.to_le_bytes());
24 hasher.update(format!("{:?}", kind).as_bytes());
25
26 let hash = hasher.finalize();
27 let mut id = [0u8; 16];
28 id.copy_from_slice(&hash.as_bytes()[..16]);
29 Self(id)
30 }
31
32 pub fn to_hex(&self) -> String {
34 hex::encode(self.0)
35 }
36
37 pub fn from_hex(hex_str: &str) -> Result<Self, hex::FromHexError> {
39 let bytes = hex::decode(hex_str)?;
40 if bytes.len() != 16 {
41 return Err(hex::FromHexError::InvalidStringLength);
42 }
43 let mut id = [0u8; 16];
44 id.copy_from_slice(&bytes);
45 Ok(Self(id))
46 }
47}
48
49impl fmt::Debug for NodeId {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 write!(f, "NodeId({})", &self.to_hex()[..8])
52 }
53}
54
55impl fmt::Display for NodeId {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 write!(f, "{}", &self.to_hex()[..8])
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64pub enum NodeKind {
65 Module,
67 Class,
69 Function,
71 Method,
73 Parameter,
75 Variable,
77 Call,
79 Import,
81 Literal,
83 Route,
85 SqlQuery,
87 Event,
89 Unknown,
91}
92
93impl fmt::Display for NodeKind {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 match self {
96 NodeKind::Module => write!(f, "Module"),
97 NodeKind::Class => write!(f, "Class"),
98 NodeKind::Function => write!(f, "Function"),
99 NodeKind::Method => write!(f, "Method"),
100 NodeKind::Parameter => write!(f, "Parameter"),
101 NodeKind::Variable => write!(f, "Variable"),
102 NodeKind::Call => write!(f, "Call"),
103 NodeKind::Import => write!(f, "Import"),
104 NodeKind::Literal => write!(f, "Literal"),
105 NodeKind::Route => write!(f, "Route"),
106 NodeKind::SqlQuery => write!(f, "SqlQuery"),
107 NodeKind::Event => write!(f, "Event"),
108 NodeKind::Unknown => write!(f, "Unknown"),
109 }
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
115#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
116pub enum EdgeKind {
117 Calls,
119 Reads,
121 Writes,
123 Imports,
125 Emits,
127 RoutesTo,
129 Raises,
131 Extends,
133 Implements,
135}
136
137impl fmt::Display for EdgeKind {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 match self {
140 EdgeKind::Calls => write!(f, "CALLS"),
141 EdgeKind::Reads => write!(f, "READS"),
142 EdgeKind::Writes => write!(f, "WRITES"),
143 EdgeKind::Imports => write!(f, "IMPORTS"),
144 EdgeKind::Emits => write!(f, "EMITS"),
145 EdgeKind::RoutesTo => write!(f, "ROUTES_TO"),
146 EdgeKind::Raises => write!(f, "RAISES"),
147 EdgeKind::Extends => write!(f, "EXTENDS"),
148 EdgeKind::Implements => write!(f, "IMPLEMENTS"),
149 }
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
155pub struct Span {
156 pub start_byte: usize,
158 pub end_byte: usize,
160 pub start_line: usize,
162 pub end_line: usize,
164 pub start_column: usize,
166 pub end_column: usize,
168}
169
170impl Span {
171 pub fn new(
173 start_byte: usize,
174 end_byte: usize,
175 start_line: usize,
176 end_line: usize,
177 start_column: usize,
178 end_column: usize,
179 ) -> Self {
180 Self {
181 start_byte,
182 end_byte,
183 start_line,
184 end_line,
185 start_column,
186 end_column,
187 }
188 }
189
190 pub fn len(&self) -> usize {
192 self.end_byte - self.start_byte
193 }
194
195 pub fn is_empty(&self) -> bool {
197 self.start_byte == self.end_byte
198 }
199}
200
201impl fmt::Display for Span {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 write!(
204 f,
205 "{}:{}-{}:{}",
206 self.start_line, self.start_column, self.end_line, self.end_column
207 )
208 }
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
213#[serde(rename_all = "lowercase")]
214pub enum Language {
215 JavaScript,
217 TypeScript,
219 Python,
221 Java,
223 Go,
225 Rust,
227 C,
229 Cpp,
231 Unknown,
233}
234
235impl Language {
236 pub fn from_extension(ext: &str) -> Self {
238 match ext.to_lowercase().as_str() {
239 "js" | "mjs" | "cjs" => Language::JavaScript,
240 "ts" | "tsx" => Language::TypeScript,
241 "py" | "pyw" => Language::Python,
242 "java" => Language::Java,
243 "go" => Language::Go,
244 "rs" => Language::Rust,
245 "c" | "h" => Language::C,
246 "cpp" | "cc" | "cxx" | "hpp" | "hxx" => Language::Cpp,
247 _ => Language::Unknown,
248 }
249 }
250}
251
252impl fmt::Display for Language {
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 match self {
255 Language::JavaScript => write!(f, "JavaScript"),
256 Language::TypeScript => write!(f, "TypeScript"),
257 Language::Python => write!(f, "Python"),
258 Language::Java => write!(f, "Java"),
259 Language::Go => write!(f, "Go"),
260 Language::Rust => write!(f, "Rust"),
261 Language::C => write!(f, "C"),
262 Language::Cpp => write!(f, "C++"),
263 Language::Unknown => write!(f, "Unknown"),
264 }
265 }
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct Node {
271 pub id: NodeId,
273 pub kind: NodeKind,
275 pub name: String,
277 pub lang: Language,
279 pub file: PathBuf,
281 pub span: Span,
283 pub signature: Option<String>,
285 pub metadata: serde_json::Value,
287}
288
289impl Node {
290 pub fn new(
292 repo_id: &str,
293 kind: NodeKind,
294 name: String,
295 lang: Language,
296 file: PathBuf,
297 span: Span,
298 ) -> Self {
299 let id = NodeId::new(repo_id, &file, &span, &kind);
300 Self {
301 id,
302 kind,
303 name,
304 lang,
305 file,
306 span,
307 signature: None,
308 metadata: serde_json::Value::Null,
309 }
310 }
311
312 pub fn with_arc(
314 repo_id: &str,
315 kind: NodeKind,
316 name: String,
317 lang: Language,
318 file: Arc<PathBuf>,
319 span: Span,
320 ) -> Self {
321 let id = NodeId::new(repo_id, &file, &span, &kind);
322 Self {
323 id,
324 kind,
325 name,
326 lang,
327 file: (*file).clone(),
328 span,
329 signature: None,
330 metadata: serde_json::Value::Null,
331 }
332 }
333
334 pub fn with_signature(mut self, sig: String) -> Self {
336 self.signature = Some(sig);
337 self
338 }
339
340 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
342 self.metadata = metadata;
343 self
344 }
345}
346
347impl fmt::Display for Node {
348 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349 write!(
350 f,
351 "{} {} '{}' at {}:{}",
352 self.lang,
353 self.kind,
354 self.name,
355 self.file.display(),
356 self.span
357 )
358 }
359}
360
361pub struct NodeBuilder {
363 repo_id: String,
364 kind: NodeKind,
365 name: String,
366 lang: Language,
367 file: PathBuf,
368 span: Span,
369 signature: Option<String>,
370 metadata: serde_json::Value,
371}
372
373impl NodeBuilder {
374 pub fn new(repo_id: impl Into<String>, kind: NodeKind) -> Self {
376 Self {
377 repo_id: repo_id.into(),
378 kind,
379 name: String::new(),
380 lang: Language::Unknown,
381 file: PathBuf::new(),
382 span: Span::new(0, 0, 1, 1, 1, 1),
383 signature: None,
384 metadata: serde_json::Value::Null,
385 }
386 }
387
388 pub fn name(mut self, name: impl Into<String>) -> Self {
390 self.name = name.into();
391 self
392 }
393
394 pub fn language(mut self, lang: Language) -> Self {
396 self.lang = lang;
397 self
398 }
399
400 pub fn file(mut self, file: impl Into<PathBuf>) -> Self {
402 self.file = file.into();
403 self
404 }
405
406 pub fn span(mut self, span: Span) -> Self {
408 self.span = span;
409 self
410 }
411
412 pub fn signature(mut self, sig: impl Into<String>) -> Self {
414 self.signature = Some(sig.into());
415 self
416 }
417
418 pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
420 self.metadata = metadata;
421 self
422 }
423
424 pub fn build(self) -> Node {
426 let id = NodeId::new(&self.repo_id, &self.file, &self.span, &self.kind);
427 Node {
428 id,
429 kind: self.kind,
430 name: self.name,
431 lang: self.lang,
432 file: self.file,
433 span: self.span,
434 signature: self.signature,
435 metadata: self.metadata,
436 }
437 }
438}
439
440#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
442pub struct Edge {
443 pub source: NodeId,
445 pub target: NodeId,
447 pub kind: EdgeKind,
449}
450
451impl Edge {
452 pub fn new(source: NodeId, target: NodeId, kind: EdgeKind) -> Self {
454 Self {
455 source,
456 target,
457 kind,
458 }
459 }
460
461 pub fn id(&self) -> String {
463 format!("{}>{}>:{:?}", self.source, self.target, self.kind)
464 }
465}
466
467impl fmt::Display for Edge {
468 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
469 write!(f, "{} --{}-> {}", self.source, self.kind, self.target)
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn test_node_id_generation() {
479 let span = Span::new(0, 10, 1, 1, 1, 11);
480 let id1 = NodeId::new("repo1", Path::new("file.js"), &span, &NodeKind::Function);
481 let id2 = NodeId::new("repo1", Path::new("file.js"), &span, &NodeKind::Function);
482 assert_eq!(id1, id2);
483
484 let id3 = NodeId::new("repo2", Path::new("file.js"), &span, &NodeKind::Function);
485 assert_ne!(id1, id3);
486 }
487
488 #[test]
489 fn test_node_id_edge_cases() {
490 let span = Span::new(0, 10, 1, 1, 1, 11);
492 let id1 = NodeId::new("repo", Path::new(""), &span, &NodeKind::Module);
493 assert!(!id1.to_hex().is_empty());
494
495 let id2 = NodeId::new(
497 "repo",
498 Path::new("src/@types/index.d.ts"),
499 &span,
500 &NodeKind::Module,
501 );
502 assert!(!id2.to_hex().is_empty());
503
504 let id3 = NodeId::new("repo", Path::new("src/文件.js"), &span, &NodeKind::Module);
506 assert!(!id3.to_hex().is_empty());
507 }
508
509 #[test]
510 fn test_language_detection() {
511 assert_eq!(Language::from_extension("js"), Language::JavaScript);
512 assert_eq!(Language::from_extension("ts"), Language::TypeScript);
513 assert_eq!(Language::from_extension("py"), Language::Python);
514 assert_eq!(Language::from_extension("java"), Language::Java);
515 assert_eq!(Language::from_extension("unknown"), Language::Unknown);
516 }
517
518 #[test]
519 fn test_language_detection_edge_cases() {
520 assert_eq!(Language::from_extension("JS"), Language::JavaScript);
522 assert_eq!(Language::from_extension("Py"), Language::Python);
523
524 assert_eq!(Language::from_extension("mjs"), Language::JavaScript);
526 assert_eq!(Language::from_extension("cjs"), Language::JavaScript);
527 assert_eq!(Language::from_extension("tsx"), Language::TypeScript);
528
529 assert_eq!(Language::from_extension("cpp"), Language::Cpp);
531 assert_eq!(Language::from_extension("cc"), Language::Cpp);
532 assert_eq!(Language::from_extension("cxx"), Language::Cpp);
533 assert_eq!(Language::from_extension("hpp"), Language::Cpp);
534
535 assert_eq!(Language::from_extension(""), Language::Unknown);
537 assert_eq!(Language::from_extension("xyz"), Language::Unknown);
538 }
539
540 #[test]
541 fn test_span_utilities() {
542 let span = Span::new(10, 20, 2, 3, 5, 15);
543 assert_eq!(span.len(), 10);
544 assert!(!span.is_empty());
545
546 let empty_span = Span::new(10, 10, 2, 2, 5, 5);
547 assert_eq!(empty_span.len(), 0);
548 assert!(empty_span.is_empty());
549 }
550
551 #[test]
552 fn test_node_serialization() {
553 let span = Span::new(0, 10, 1, 1, 1, 11);
554 let node = Node::new(
555 "test_repo",
556 NodeKind::Function,
557 "test_func".to_string(),
558 Language::JavaScript,
559 PathBuf::from("test.js"),
560 span,
561 );
562
563 let serialized = serde_json::to_string(&node).unwrap();
565 let deserialized: Node = serde_json::from_str(&serialized).unwrap();
566
567 assert_eq!(node.id, deserialized.id);
568 assert_eq!(node.name, deserialized.name);
569 assert_eq!(node.file, deserialized.file);
570 }
571
572 #[test]
573 fn test_node_with_methods() {
574 let span = Span::new(0, 10, 1, 1, 1, 11);
575 let node = Node::new(
576 "test_repo",
577 NodeKind::Function,
578 "test_func".to_string(),
579 Language::JavaScript,
580 PathBuf::from("test.js"),
581 span,
582 )
583 .with_signature("(a: number, b: number) => number".to_string())
584 .with_metadata(serde_json::json!({ "async": true }));
585
586 assert_eq!(
587 node.signature,
588 Some("(a: number, b: number) => number".to_string())
589 );
590 assert_eq!(node.metadata["async"], true);
591 }
592
593 #[test]
594 fn test_node_builder() {
595 let span = Span::new(0, 10, 1, 1, 1, 11);
596 let node = NodeBuilder::new("test_repo", NodeKind::Function)
597 .name("myFunction")
598 .language(Language::TypeScript)
599 .file("src/index.ts")
600 .span(span.clone())
601 .signature("() => void")
602 .metadata(serde_json::json!({ "exported": true }))
603 .build();
604
605 assert_eq!(node.name, "myFunction");
606 assert_eq!(node.lang, Language::TypeScript);
607 assert_eq!(node.file, PathBuf::from("src/index.ts"));
608 assert_eq!(node.span, span);
609 assert_eq!(node.signature, Some("() => void".to_string()));
610 assert_eq!(node.metadata["exported"], true);
611 }
612
613 #[test]
614 fn test_edge_creation_and_serialization() {
615 let span1 = Span::new(0, 10, 1, 1, 1, 11);
616 let span2 = Span::new(20, 30, 2, 1, 2, 11);
617
618 let id1 = NodeId::new("repo", Path::new("file.js"), &span1, &NodeKind::Function);
619 let id2 = NodeId::new("repo", Path::new("file.js"), &span2, &NodeKind::Function);
620
621 let edge = Edge::new(id1, id2, EdgeKind::Calls);
622 assert_eq!(edge.source, id1);
623 assert_eq!(edge.target, id2);
624 assert_eq!(edge.kind, EdgeKind::Calls);
625
626 let serialized = serde_json::to_string(&edge).unwrap();
628 let deserialized: Edge = serde_json::from_str(&serialized).unwrap();
629 assert_eq!(edge, deserialized);
630
631 let edge_id = edge.id();
633 assert!(edge_id.contains(&id1.to_string()));
634 assert!(edge_id.contains(&id2.to_string()));
635 assert!(edge_id.contains("Calls"));
636 }
637
638 #[test]
639 fn test_display_traits() {
640 assert_eq!(NodeKind::Function.to_string(), "Function");
642 assert_eq!(NodeKind::Module.to_string(), "Module");
643
644 assert_eq!(EdgeKind::Calls.to_string(), "CALLS");
646 assert_eq!(EdgeKind::Imports.to_string(), "IMPORTS");
647
648 assert_eq!(Language::JavaScript.to_string(), "JavaScript");
650 assert_eq!(Language::Cpp.to_string(), "C++");
651
652 let span = Span::new(0, 10, 1, 5, 2, 15);
654 assert_eq!(span.to_string(), "1:2-5:15");
655
656 let node = Node::new(
658 "repo",
659 NodeKind::Function,
660 "myFunc".to_string(),
661 Language::JavaScript,
662 PathBuf::from("test.js"),
663 span.clone(),
664 );
665 let display = node.to_string();
666 assert!(display.contains("JavaScript"));
667 assert!(display.contains("Function"));
668 assert!(display.contains("myFunc"));
669 assert!(display.contains("test.js"));
670
671 let id1 = NodeId::new("repo", Path::new("file.js"), &span, &NodeKind::Function);
673 let id2 = NodeId::new("repo", Path::new("file.js"), &span, &NodeKind::Variable);
674 let edge = Edge::new(id1, id2, EdgeKind::Reads);
675 let edge_display = edge.to_string();
676 assert!(edge_display.contains("READS"));
677 }
678}