Skip to main content

graphy_core/
gir.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4use crate::symbol_id::SymbolId;
5
6// ── Node Types ──────────────────────────────────────────────
7
8#[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// ── Edge Types ──────────────────────────────────────────────
55
56#[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// ── Visibility ──────────────────────────────────────────────
78
79#[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// ── Language ────────────────────────────────────────────────
89
90#[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// ── Span ────────────────────────────────────────────────────
129
130#[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// ── Complexity Metrics ──────────────────────────────────────
150
151#[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// ── GIR Node ────────────────────────────────────────────────
162
163#[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    /// Test coverage (0.0-1.0), set by coverage overlay. None if no coverage data.
177    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// ── Edge Metadata ───────────────────────────────────────────
207
208#[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// ── GIR Edge ────────────────────────────────────────────────
248
249#[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// ── Parse Output ────────────────────────────────────────────
277
278/// The output of parsing a single file: a collection of nodes and edges.
279#[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    // ── NodeKind ───────────────────────────────────────────
305
306    #[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    // ── Language ───────────────────────────────────────────
333
334    #[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        // TS variants
353        assert_eq!(Language::from_extension("mts"), Some(Language::TypeScript));
354        assert_eq!(Language::from_extension("cts"), Some(Language::TypeScript));
355        // JS variants
356        assert_eq!(Language::from_extension("mjs"), Some(Language::JavaScript));
357        assert_eq!(Language::from_extension("cjs"), Some(Language::JavaScript));
358        // C/C++ variants
359        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); // case-sensitive
375    }
376
377    // ── Span ──────────────────────────────────────────────
378
379    #[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    // ── Visibility ────────────────────────────────────────
402
403    #[test]
404    fn visibility_default_is_internal() {
405        assert_eq!(Visibility::default(), Visibility::Internal);
406    }
407
408    // ── GirNode ───────────────────────────────────────────
409
410    #[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        // ID should still be deterministic
440        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    // ── GirEdge ───────────────────────────────────────────
463
464    #[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    // ── Serde round-trips ─────────────────────────────────
485
486    #[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    // ── ParseOutput ───────────────────────────────────────
599
600    #[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    // ── ComplexityMetrics ─────────────────────────────────
635
636    #[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}