1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4use crate::symbol_id::SymbolId;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[repr(u32)]
10pub enum NodeKind {
11 Module,
12 File,
13 Folder,
14 Class,
15 Struct,
16 Enum,
17 Interface,
18 Trait,
19 Function,
20 Method,
21 Constructor,
22 Field,
23 Property,
24 Parameter,
25 Variable,
26 Constant,
27 TypeAlias,
28 Import,
29 Decorator,
30 EnumVariant,
31}
32
33impl NodeKind {
34 pub fn is_callable(&self) -> bool {
35 matches!(
36 self,
37 NodeKind::Function | NodeKind::Method | NodeKind::Constructor
38 )
39 }
40
41 pub fn is_type_def(&self) -> bool {
42 matches!(
43 self,
44 NodeKind::Class
45 | NodeKind::Struct
46 | NodeKind::Enum
47 | NodeKind::Interface
48 | NodeKind::Trait
49 | NodeKind::TypeAlias
50 )
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
57pub enum EdgeKind {
58 Contains,
59 Calls,
60 Imports,
61 ImportsFrom,
62 Inherits,
63 Implements,
64 Overrides,
65 ReturnsType,
66 ParamType,
67 FieldType,
68 Instantiates,
69 DataFlowsTo,
70 TaintedBy,
71 CrossLangCalls,
72 AnnotatedWith,
73 CoupledWith,
74 SimilarTo,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
80pub enum Visibility {
81 Public,
82 #[default]
83 Internal,
84 Private,
85 Exported,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
91pub enum Language {
92 Python,
93 TypeScript,
94 JavaScript,
95 Rust,
96 Go,
97 Java,
98 Cpp,
99 C,
100 CSharp,
101 Ruby,
102 Kotlin,
103 Php,
104 Svelte,
105}
106
107impl Language {
108 pub fn from_extension(ext: &str) -> Option<Self> {
109 match ext {
110 "py" => Some(Language::Python),
111 "ts" | "tsx" | "mts" | "cts" => Some(Language::TypeScript),
112 "js" | "jsx" | "mjs" | "cjs" => Some(Language::JavaScript),
113 "rs" => Some(Language::Rust),
114 "go" => Some(Language::Go),
115 "java" => Some(Language::Java),
116 "cpp" | "cc" | "cxx" | "hpp" => Some(Language::Cpp),
117 "c" | "h" => Some(Language::C),
118 "cs" => Some(Language::CSharp),
119 "rb" => Some(Language::Ruby),
120 "kt" => Some(Language::Kotlin),
121 "php" => Some(Language::Php),
122 "svelte" => Some(Language::Svelte),
123 _ => None,
124 }
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
131pub struct Span {
132 pub start_line: u32,
133 pub start_col: u32,
134 pub end_line: u32,
135 pub end_col: u32,
136}
137
138impl Span {
139 pub fn new(start_line: u32, start_col: u32, end_line: u32, end_col: u32) -> Self {
140 Self {
141 start_line,
142 start_col,
143 end_line,
144 end_col,
145 }
146 }
147}
148
149#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
152pub struct ComplexityMetrics {
153 pub cyclomatic: u32,
154 pub cognitive: u32,
155 pub loc: u32,
156 pub sloc: u32,
157 pub parameter_count: u32,
158 pub max_nesting_depth: u32,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct GirNode {
165 pub id: SymbolId,
166 pub name: String,
167 pub kind: NodeKind,
168 pub file_path: PathBuf,
169 pub span: Span,
170 pub visibility: Visibility,
171 pub language: Language,
172 pub signature: Option<String>,
173 pub complexity: Option<ComplexityMetrics>,
174 pub confidence: f32,
175 pub doc: Option<String>,
176 pub coverage: Option<f32>,
178}
179
180impl GirNode {
181 pub fn new(
182 name: String,
183 kind: NodeKind,
184 file_path: PathBuf,
185 span: Span,
186 language: Language,
187 ) -> Self {
188 let id = SymbolId::new(&file_path, &name, kind, span.start_line);
189 Self {
190 id,
191 name,
192 kind,
193 file_path,
194 span,
195 visibility: Visibility::default(),
196 language,
197 signature: None,
198 complexity: None,
199 confidence: 1.0,
200 doc: None,
201 coverage: None,
202 }
203 }
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
209pub enum EdgeMetadata {
210 None,
211 Call {
212 is_dynamic: bool,
213 },
214 Import {
215 alias: Option<String>,
216 items: Vec<String>,
217 },
218 Inheritance {
219 depth: u32,
220 },
221 DataFlow {
222 transform: DataFlowTransform,
223 },
224 Taint {
225 label: String,
226 },
227 Coupling {
228 commit_count: u32,
229 temporal_weight: f64,
230 },
231 Similarity {
232 score: f32,
233 },
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub enum DataFlowTransform {
238 Identity,
239 Map,
240 Filter,
241 Serialize,
242 Deserialize,
243 Validate,
244 Transform,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct GirEdge {
251 pub kind: EdgeKind,
252 pub confidence: f32,
253 pub metadata: EdgeMetadata,
254}
255
256impl GirEdge {
257 pub fn new(kind: EdgeKind) -> Self {
258 Self {
259 kind,
260 confidence: 1.0,
261 metadata: EdgeMetadata::None,
262 }
263 }
264
265 pub fn with_confidence(mut self, confidence: f32) -> Self {
266 self.confidence = confidence;
267 self
268 }
269
270 pub fn with_metadata(mut self, metadata: EdgeMetadata) -> Self {
271 self.metadata = metadata;
272 self
273 }
274}
275
276#[derive(Debug, Clone, Default)]
280pub struct ParseOutput {
281 pub nodes: Vec<GirNode>,
282 pub edges: Vec<(SymbolId, SymbolId, GirEdge)>,
283}
284
285impl ParseOutput {
286 pub fn new() -> Self {
287 Self::default()
288 }
289
290 pub fn add_node(&mut self, node: GirNode) {
291 self.nodes.push(node);
292 }
293
294 pub fn add_edge(&mut self, source: SymbolId, target: SymbolId, edge: GirEdge) {
295 self.edges.push((source, target, edge));
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use std::path::PathBuf;
303
304 #[test]
307 fn callable_kinds() {
308 assert!(NodeKind::Function.is_callable());
309 assert!(NodeKind::Method.is_callable());
310 assert!(NodeKind::Constructor.is_callable());
311 assert!(!NodeKind::Class.is_callable());
312 assert!(!NodeKind::Field.is_callable());
313 assert!(!NodeKind::Variable.is_callable());
314 assert!(!NodeKind::Import.is_callable());
315 assert!(!NodeKind::Module.is_callable());
316 }
317
318 #[test]
319 fn type_def_kinds() {
320 assert!(NodeKind::Class.is_type_def());
321 assert!(NodeKind::Struct.is_type_def());
322 assert!(NodeKind::Enum.is_type_def());
323 assert!(NodeKind::Interface.is_type_def());
324 assert!(NodeKind::Trait.is_type_def());
325 assert!(NodeKind::TypeAlias.is_type_def());
326 assert!(!NodeKind::Function.is_type_def());
327 assert!(!NodeKind::Method.is_type_def());
328 assert!(!NodeKind::Field.is_type_def());
329 assert!(!NodeKind::Decorator.is_type_def());
330 }
331
332 #[test]
335 fn language_from_common_extensions() {
336 assert_eq!(Language::from_extension("py"), Some(Language::Python));
337 assert_eq!(Language::from_extension("ts"), Some(Language::TypeScript));
338 assert_eq!(Language::from_extension("tsx"), Some(Language::TypeScript));
339 assert_eq!(Language::from_extension("js"), Some(Language::JavaScript));
340 assert_eq!(Language::from_extension("jsx"), Some(Language::JavaScript));
341 assert_eq!(Language::from_extension("rs"), Some(Language::Rust));
342 assert_eq!(Language::from_extension("go"), Some(Language::Go));
343 assert_eq!(Language::from_extension("java"), Some(Language::Java));
344 assert_eq!(Language::from_extension("svelte"), Some(Language::Svelte));
345 assert_eq!(Language::from_extension("php"), Some(Language::Php));
346 assert_eq!(Language::from_extension("rb"), Some(Language::Ruby));
347 assert_eq!(Language::from_extension("kt"), Some(Language::Kotlin));
348 }
349
350 #[test]
351 fn language_from_variant_extensions() {
352 assert_eq!(Language::from_extension("mts"), Some(Language::TypeScript));
354 assert_eq!(Language::from_extension("cts"), Some(Language::TypeScript));
355 assert_eq!(Language::from_extension("mjs"), Some(Language::JavaScript));
357 assert_eq!(Language::from_extension("cjs"), Some(Language::JavaScript));
358 assert_eq!(Language::from_extension("c"), Some(Language::C));
360 assert_eq!(Language::from_extension("h"), Some(Language::C));
361 assert_eq!(Language::from_extension("cpp"), Some(Language::Cpp));
362 assert_eq!(Language::from_extension("cc"), Some(Language::Cpp));
363 assert_eq!(Language::from_extension("cxx"), Some(Language::Cpp));
364 assert_eq!(Language::from_extension("hpp"), Some(Language::Cpp));
365 assert_eq!(Language::from_extension("cs"), Some(Language::CSharp));
366 }
367
368 #[test]
369 fn language_from_unknown_extension() {
370 assert_eq!(Language::from_extension(""), None);
371 assert_eq!(Language::from_extension("txt"), None);
372 assert_eq!(Language::from_extension("md"), None);
373 assert_eq!(Language::from_extension("toml"), None);
374 assert_eq!(Language::from_extension("PY"), None); }
376
377 #[test]
380 fn span_new() {
381 let s = Span::new(1, 0, 10, 80);
382 assert_eq!(s.start_line, 1);
383 assert_eq!(s.start_col, 0);
384 assert_eq!(s.end_line, 10);
385 assert_eq!(s.end_col, 80);
386 }
387
388 #[test]
389 fn span_zero() {
390 let s = Span::new(0, 0, 0, 0);
391 assert_eq!(s.start_line, 0);
392 assert_eq!(s.end_line, 0);
393 }
394
395 #[test]
396 fn span_max_values() {
397 let s = Span::new(u32::MAX, u32::MAX, u32::MAX, u32::MAX);
398 assert_eq!(s.start_line, u32::MAX);
399 }
400
401 #[test]
404 fn visibility_default_is_internal() {
405 assert_eq!(Visibility::default(), Visibility::Internal);
406 }
407
408 #[test]
411 fn gir_node_defaults() {
412 let node = GirNode::new(
413 "foo".to_string(),
414 NodeKind::Function,
415 PathBuf::from("test.py"),
416 Span::new(1, 0, 5, 0),
417 Language::Python,
418 );
419 assert_eq!(node.name, "foo");
420 assert_eq!(node.kind, NodeKind::Function);
421 assert_eq!(node.visibility, Visibility::Internal);
422 assert_eq!(node.confidence, 1.0);
423 assert!(node.signature.is_none());
424 assert!(node.complexity.is_none());
425 assert!(node.doc.is_none());
426 assert!(node.coverage.is_none());
427 }
428
429 #[test]
430 fn gir_node_unicode_name() {
431 let node = GirNode::new(
432 "计算_résultat".to_string(),
433 NodeKind::Function,
434 PathBuf::from("test.py"),
435 Span::new(1, 0, 5, 0),
436 Language::Python,
437 );
438 assert_eq!(node.name, "计算_résultat");
439 let node2 = GirNode::new(
441 "计算_résultat".to_string(),
442 NodeKind::Function,
443 PathBuf::from("test.py"),
444 Span::new(1, 0, 5, 0),
445 Language::Python,
446 );
447 assert_eq!(node.id, node2.id);
448 }
449
450 #[test]
451 fn gir_node_empty_name() {
452 let node = GirNode::new(
453 String::new(),
454 NodeKind::Function,
455 PathBuf::from("test.py"),
456 Span::new(1, 0, 5, 0),
457 Language::Python,
458 );
459 assert_eq!(node.name, "");
460 }
461
462 #[test]
465 fn gir_edge_builder_pattern() {
466 let edge = GirEdge::new(EdgeKind::Calls)
467 .with_confidence(0.8)
468 .with_metadata(EdgeMetadata::Call { is_dynamic: true });
469 assert_eq!(edge.kind, EdgeKind::Calls);
470 assert_eq!(edge.confidence, 0.8);
471 match edge.metadata {
472 EdgeMetadata::Call { is_dynamic } => assert!(is_dynamic),
473 _ => panic!("expected Call metadata"),
474 }
475 }
476
477 #[test]
478 fn gir_edge_default_metadata() {
479 let edge = GirEdge::new(EdgeKind::Contains);
480 assert_eq!(edge.confidence, 1.0);
481 assert!(matches!(edge.metadata, EdgeMetadata::None));
482 }
483
484 #[test]
487 fn node_kind_serde_round_trip() {
488 for kind in [
489 NodeKind::Module, NodeKind::File, NodeKind::Folder,
490 NodeKind::Class, NodeKind::Struct, NodeKind::Enum,
491 NodeKind::Interface, NodeKind::Trait, NodeKind::Function,
492 NodeKind::Method, NodeKind::Constructor, NodeKind::Field,
493 NodeKind::Property, NodeKind::Parameter, NodeKind::Variable,
494 NodeKind::Constant, NodeKind::TypeAlias, NodeKind::Import,
495 NodeKind::Decorator, NodeKind::EnumVariant,
496 ] {
497 let json = serde_json::to_string(&kind).unwrap();
498 let back: NodeKind = serde_json::from_str(&json).unwrap();
499 assert_eq!(kind, back);
500 }
501 }
502
503 #[test]
504 fn edge_kind_serde_round_trip() {
505 for kind in [
506 EdgeKind::Contains, EdgeKind::Calls, EdgeKind::Imports,
507 EdgeKind::ImportsFrom, EdgeKind::Inherits, EdgeKind::Implements,
508 EdgeKind::Overrides, EdgeKind::ReturnsType, EdgeKind::ParamType,
509 EdgeKind::FieldType, EdgeKind::Instantiates, EdgeKind::DataFlowsTo,
510 EdgeKind::TaintedBy, EdgeKind::CrossLangCalls, EdgeKind::AnnotatedWith,
511 EdgeKind::CoupledWith, EdgeKind::SimilarTo,
512 ] {
513 let json = serde_json::to_string(&kind).unwrap();
514 let back: EdgeKind = serde_json::from_str(&json).unwrap();
515 assert_eq!(kind, back);
516 }
517 }
518
519 #[test]
520 fn edge_metadata_serde_round_trip() {
521 let cases: Vec<EdgeMetadata> = vec![
522 EdgeMetadata::None,
523 EdgeMetadata::Call { is_dynamic: true },
524 EdgeMetadata::Call { is_dynamic: false },
525 EdgeMetadata::Import { alias: Some("a".into()), items: vec!["x".into(), "y".into()] },
526 EdgeMetadata::Import { alias: None, items: vec![] },
527 EdgeMetadata::Inheritance { depth: 0 },
528 EdgeMetadata::Inheritance { depth: u32::MAX },
529 EdgeMetadata::DataFlow { transform: DataFlowTransform::Identity },
530 EdgeMetadata::DataFlow { transform: DataFlowTransform::Map },
531 EdgeMetadata::DataFlow { transform: DataFlowTransform::Filter },
532 EdgeMetadata::DataFlow { transform: DataFlowTransform::Serialize },
533 EdgeMetadata::DataFlow { transform: DataFlowTransform::Deserialize },
534 EdgeMetadata::DataFlow { transform: DataFlowTransform::Validate },
535 EdgeMetadata::DataFlow { transform: DataFlowTransform::Transform },
536 EdgeMetadata::Taint { label: "XSS".into() },
537 EdgeMetadata::Coupling { commit_count: 42, temporal_weight: 0.95 },
538 EdgeMetadata::Similarity { score: 0.87 },
539 ];
540 for meta in cases {
541 let json = serde_json::to_string(&meta).unwrap();
542 let _back: EdgeMetadata = serde_json::from_str(&json).unwrap();
543 }
544 }
545
546 #[test]
547 fn gir_node_serde_round_trip() {
548 let mut node = GirNode::new(
549 "test_func".to_string(),
550 NodeKind::Function,
551 PathBuf::from("src/main.rs"),
552 Span::new(10, 4, 25, 1),
553 Language::Rust,
554 );
555 node.visibility = Visibility::Public;
556 node.signature = Some("fn test_func(x: i32) -> bool".into());
557 node.doc = Some("A test function".into());
558 node.coverage = Some(0.75);
559 node.confidence = 0.9;
560
561 let json = serde_json::to_string(&node).unwrap();
562 let back: GirNode = serde_json::from_str(&json).unwrap();
563 assert_eq!(back.id, node.id);
564 assert_eq!(back.name, "test_func");
565 assert_eq!(back.kind, NodeKind::Function);
566 assert_eq!(back.visibility, Visibility::Public);
567 assert_eq!(back.confidence, 0.9);
568 assert_eq!(back.signature.as_deref(), Some("fn test_func(x: i32) -> bool"));
569 assert_eq!(back.coverage, Some(0.75));
570 }
571
572 #[test]
573 fn gir_node_bincode_round_trip() {
574 let node = GirNode::new(
575 "my_func".to_string(),
576 NodeKind::Method,
577 PathBuf::from("lib.py"),
578 Span::new(1, 0, 100, 0),
579 Language::Python,
580 );
581 let bytes = bincode::serialize(&node).unwrap();
582 let back: GirNode = bincode::deserialize(&bytes).unwrap();
583 assert_eq!(back.id, node.id);
584 assert_eq!(back.name, node.name);
585 }
586
587 #[test]
588 fn gir_edge_bincode_round_trip() {
589 let edge = GirEdge::new(EdgeKind::Calls)
590 .with_confidence(0.7)
591 .with_metadata(EdgeMetadata::Call { is_dynamic: true });
592 let bytes = bincode::serialize(&edge).unwrap();
593 let back: GirEdge = bincode::deserialize(&bytes).unwrap();
594 assert_eq!(back.kind, EdgeKind::Calls);
595 assert_eq!(back.confidence, 0.7);
596 }
597
598 #[test]
601 fn parse_output_empty() {
602 let po = ParseOutput::new();
603 assert!(po.nodes.is_empty());
604 assert!(po.edges.is_empty());
605 }
606
607 #[test]
608 fn parse_output_add_node_and_edge() {
609 let mut po = ParseOutput::new();
610 let node = GirNode::new(
611 "f".to_string(),
612 NodeKind::Function,
613 PathBuf::from("a.py"),
614 Span::new(1, 0, 5, 0),
615 Language::Python,
616 );
617 let id = node.id;
618 po.add_node(node);
619 assert_eq!(po.nodes.len(), 1);
620
621 let node2 = GirNode::new(
622 "g".to_string(),
623 NodeKind::Function,
624 PathBuf::from("a.py"),
625 Span::new(10, 0, 15, 0),
626 Language::Python,
627 );
628 let id2 = node2.id;
629 po.add_node(node2);
630 po.add_edge(id, id2, GirEdge::new(EdgeKind::Calls));
631 assert_eq!(po.edges.len(), 1);
632 }
633
634 #[test]
637 fn complexity_metrics_default() {
638 let m = ComplexityMetrics::default();
639 assert_eq!(m.cyclomatic, 0);
640 assert_eq!(m.cognitive, 0);
641 assert_eq!(m.loc, 0);
642 assert_eq!(m.sloc, 0);
643 assert_eq!(m.parameter_count, 0);
644 assert_eq!(m.max_nesting_depth, 0);
645 }
646}