Skip to main content

thread_flow/incremental/
types.rs

1// SPDX-FileCopyrightText: 2025 Knitli Inc. <knitli@knit.li>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Core data structures for the incremental update system.
5//!
6//! This module defines the foundational types used for fingerprint tracking,
7//! dependency edges, and symbol-level dependency information. The design is
8//! adapted from ReCoco's `FieldDefFingerprint` pattern (analyzer.rs:69-84).
9
10use recoco::utils::fingerprint::{Fingerprint, Fingerprinter};
11use serde::{Deserialize, Serialize};
12use std::path::{Path, PathBuf};
13use thread_utilities::RapidSet;
14
15/// Tracks the fingerprint and source files for an analysis result.
16///
17/// Adapted from ReCoco's `FieldDefFingerprint` pattern. Combines content
18/// fingerprinting with source file tracking to enable precise invalidation
19/// scope determination.
20///
21/// # Examples
22///
23/// ```rust
24/// use thread_flow::incremental::types::AnalysisDefFingerprint;
25///
26/// // Create a fingerprint from file content
27/// let fp = AnalysisDefFingerprint::new(b"fn main() {}");
28/// assert!(fp.content_matches(b"fn main() {}"));
29/// assert!(!fp.content_matches(b"fn other() {}"));
30/// ```
31#[derive(Debug, Clone)]
32pub struct AnalysisDefFingerprint {
33    /// Source files that contribute to this analysis result.
34    /// Used to determine invalidation scope when dependencies change.
35    pub source_files: RapidSet<PathBuf>,
36
37    /// Content fingerprint of the analyzed file (Blake3, 16 bytes).
38    /// Combines file content hash for change detection.
39    pub fingerprint: Fingerprint,
40
41    /// Timestamp of last successful analysis (Unix microseconds).
42    /// `None` if this fingerprint has never been persisted.
43    pub last_analyzed: Option<i64>,
44}
45
46/// A dependency edge representing a relationship between two files.
47///
48/// Edges are directed: `from` depends on `to`. For example, if `main.rs`
49/// imports `utils.rs`, the edge is `from: main.rs, to: utils.rs`.
50///
51/// # Examples
52///
53/// ```rust
54/// use thread_flow::incremental::types::{DependencyEdge, DependencyType};
55/// use std::path::PathBuf;
56///
57/// let edge = DependencyEdge {
58///     from: PathBuf::from("src/main.rs"),
59///     to: PathBuf::from("src/utils.rs"),
60///     dep_type: DependencyType::Import,
61///     symbol: None,
62/// };
63/// assert_eq!(edge.dep_type, DependencyType::Import);
64/// ```
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
66pub struct DependencyEdge {
67    /// Source file path (the file that depends on another).
68    pub from: PathBuf,
69
70    /// Target file path (the file being depended upon).
71    pub to: PathBuf,
72
73    /// The type of dependency relationship.
74    pub dep_type: DependencyType,
75
76    /// Optional symbol-level dependency information.
77    /// When present, enables finer-grained invalidation.
78    pub symbol: Option<SymbolDependency>,
79}
80
81/// The type of dependency relationship between files.
82///
83/// Determines how changes propagate through the dependency graph.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
85pub enum DependencyType {
86    /// Direct import/require/use statement (e.g., `use crate::utils;`).
87    Import,
88
89    /// Export declaration that other files may consume.
90    Export,
91
92    /// Macro expansion dependency.
93    Macro,
94
95    /// Type dependency (e.g., TypeScript interfaces, Rust type aliases).
96    Type,
97
98    /// Trait implementation dependency (Rust-specific).
99    Trait,
100}
101
102/// The strength of a dependency relationship.
103///
104/// Strong dependencies always trigger reanalysis on change.
105/// Weak dependencies may be skipped during invalidation traversal.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
107pub enum DependencyStrength {
108    /// Hard dependency: change always requires reanalysis of dependents.
109    Strong,
110
111    /// Soft dependency: change may require reanalysis (e.g., dev-dependencies).
112    Weak,
113}
114
115/// Symbol-level dependency tracking for fine-grained invalidation.
116///
117/// Tracks which specific symbol in the source file depends on which
118/// specific symbol in the target file.
119///
120/// # Examples
121///
122/// ```rust
123/// use thread_flow::incremental::types::{SymbolDependency, SymbolKind, DependencyStrength};
124///
125/// let dep = SymbolDependency {
126///     from_symbol: "parse_config".to_string(),
127///     to_symbol: "ConfigReader".to_string(),
128///     kind: SymbolKind::Function,
129///     strength: DependencyStrength::Strong,
130/// };
131/// assert_eq!(dep.kind, SymbolKind::Function);
132/// ```
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134pub struct SymbolDependency {
135    /// Symbol path in the source file (the dependent symbol).
136    pub from_symbol: String,
137
138    /// Symbol path in the target file (the dependency).
139    pub to_symbol: String,
140
141    /// The kind of symbol being depended upon.
142    pub kind: SymbolKind,
143
144    /// Strength of this symbol-level dependency.
145    pub strength: DependencyStrength,
146}
147
148/// Classification of symbols for dependency tracking.
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
150pub enum SymbolKind {
151    /// Function or method definition.
152    Function,
153
154    /// Class or struct definition.
155    Class,
156
157    /// Interface or trait definition.
158    Interface,
159
160    /// Type alias or typedef.
161    TypeAlias,
162
163    /// Constant or static variable.
164    Constant,
165
166    /// Enum definition.
167    Enum,
168
169    /// Module or namespace.
170    Module,
171
172    /// Macro definition.
173    Macro,
174}
175
176// ─── Implementation ──────────────────────────────────────────────────────────
177
178impl AnalysisDefFingerprint {
179    /// Creates a new fingerprint from raw file content bytes.
180    ///
181    /// Computes a Blake3-based fingerprint of the content using ReCoco's
182    /// `Fingerprinter` builder pattern.
183    ///
184    /// # Arguments
185    ///
186    /// * `content` - The raw bytes of the file to fingerprint.
187    ///
188    /// # Examples
189    ///
190    /// ```rust
191    /// use thread_flow::incremental::types::AnalysisDefFingerprint;
192    ///
193    /// let fp = AnalysisDefFingerprint::new(b"hello world");
194    /// assert!(fp.content_matches(b"hello world"));
195    /// ```
196    pub fn new(content: &[u8]) -> Self {
197        let mut fingerprinter = Fingerprinter::default();
198        fingerprinter.write_raw_bytes(content);
199        Self {
200            source_files: thread_utilities::get_set(),
201            fingerprint: fingerprinter.into_fingerprint(),
202            last_analyzed: None,
203        }
204    }
205
206    /// Creates a new fingerprint with associated source files.
207    ///
208    /// The source files represent the set of files that contributed to
209    /// this analysis result, enabling precise invalidation scope.
210    ///
211    /// # Arguments
212    ///
213    /// * `content` - The raw bytes of the primary file.
214    /// * `source_files` - Files that contributed to this analysis.
215    ///
216    /// # Examples
217    ///
218    /// ```rust
219    /// use thread_flow::incremental::types::AnalysisDefFingerprint;
220    /// use thread_utilities::RapidSet;
221    /// use std::path::PathBuf;
222    ///
223    /// let sources = RapidSet::from([PathBuf::from("dep.rs")]);
224    /// let fp = AnalysisDefFingerprint::with_sources(b"content", sources);
225    /// assert_eq!(fp.source_files.len(), 1);
226    /// ```
227    pub fn with_sources(content: &[u8], source_files: RapidSet<PathBuf>) -> Self {
228        let mut fingerprinter = Fingerprinter::default();
229        fingerprinter.write_raw_bytes(content);
230        Self {
231            source_files,
232            fingerprint: fingerprinter.into_fingerprint(),
233            last_analyzed: None,
234        }
235    }
236
237    /// Updates the fingerprint with new content, preserving source files.
238    ///
239    /// Returns a new `AnalysisDefFingerprint` with an updated fingerprint
240    /// computed from the new content bytes.
241    ///
242    /// # Arguments
243    ///
244    /// * `content` - The new raw bytes to fingerprint.
245    ///
246    /// # Examples
247    ///
248    /// ```rust
249    /// use thread_flow::incremental::types::AnalysisDefFingerprint;
250    ///
251    /// let fp = AnalysisDefFingerprint::new(b"old content");
252    /// let updated = fp.update_fingerprint(b"new content");
253    /// assert!(!updated.content_matches(b"old content"));
254    /// assert!(updated.content_matches(b"new content"));
255    /// ```
256    pub fn update_fingerprint(&self, content: &[u8]) -> Self {
257        let mut fingerprinter = Fingerprinter::default();
258        fingerprinter.write_raw_bytes(content);
259        Self {
260            source_files: self.source_files.clone(),
261            fingerprint: fingerprinter.into_fingerprint(),
262            last_analyzed: None,
263        }
264    }
265
266    /// Checks if the given content matches this fingerprint.
267    ///
268    /// Computes a fresh fingerprint from the content and compares it
269    /// byte-for-byte with the stored fingerprint.
270    ///
271    /// # Arguments
272    ///
273    /// * `content` - The raw bytes to check against the stored fingerprint.
274    ///
275    /// # Examples
276    ///
277    /// ```rust
278    /// use thread_flow::incremental::types::AnalysisDefFingerprint;
279    ///
280    /// let fp = AnalysisDefFingerprint::new(b"fn main() {}");
281    /// assert!(fp.content_matches(b"fn main() {}"));
282    /// assert!(!fp.content_matches(b"fn main() { println!(); }"));
283    /// ```
284    pub fn content_matches(&self, content: &[u8]) -> bool {
285        let mut fingerprinter = Fingerprinter::default();
286        fingerprinter.write_raw_bytes(content);
287        let other = fingerprinter.into_fingerprint();
288        self.fingerprint.as_slice() == other.as_slice()
289    }
290
291    /// Adds a source file to the tracked set.
292    ///
293    /// # Arguments
294    ///
295    /// * `path` - Path to add to the source files set.
296    pub fn add_source_file(&mut self, path: PathBuf) {
297        self.source_files.insert(path);
298    }
299
300    /// Removes a source file from the tracked set.
301    ///
302    /// # Arguments
303    ///
304    /// * `path` - Path to remove from the source files set.
305    ///
306    /// # Returns
307    ///
308    /// `true` if the path was present and removed.
309    pub fn remove_source_file(&mut self, path: &Path) -> bool {
310        self.source_files.remove(path)
311    }
312
313    /// Sets the last analyzed timestamp.
314    ///
315    /// # Arguments
316    ///
317    /// * `timestamp` - Unix timestamp in microseconds.
318    pub fn set_last_analyzed(&mut self, timestamp: i64) {
319        self.last_analyzed = Some(timestamp);
320    }
321
322    /// Returns the number of source files tracked.
323    pub fn source_file_count(&self) -> usize {
324        self.source_files.len()
325    }
326
327    /// Returns a reference to the underlying [`Fingerprint`].
328    pub fn fingerprint(&self) -> &Fingerprint {
329        &self.fingerprint
330    }
331}
332
333impl DependencyEdge {
334    /// Creates a new dependency edge with the given parameters.
335    ///
336    /// # Arguments
337    ///
338    /// * `from` - The source file path (dependent).
339    /// * `to` - The target file path (dependency).
340    /// * `dep_type` - The type of dependency.
341    ///
342    /// # Examples
343    ///
344    /// ```rust
345    /// use thread_flow::incremental::types::{DependencyEdge, DependencyType};
346    /// use std::path::PathBuf;
347    ///
348    /// let edge = DependencyEdge::new(
349    ///     PathBuf::from("a.rs"),
350    ///     PathBuf::from("b.rs"),
351    ///     DependencyType::Import,
352    /// );
353    /// assert!(edge.symbol.is_none());
354    /// ```
355    pub fn new(from: PathBuf, to: PathBuf, dep_type: DependencyType) -> Self {
356        Self {
357            from,
358            to,
359            dep_type,
360            symbol: None,
361        }
362    }
363
364    /// Creates a new dependency edge with symbol-level tracking.
365    ///
366    /// # Arguments
367    ///
368    /// * `from` - The source file path (dependent).
369    /// * `to` - The target file path (dependency).
370    /// * `dep_type` - The type of dependency.
371    /// * `symbol` - Symbol-level dependency information.
372    pub fn with_symbol(
373        from: PathBuf,
374        to: PathBuf,
375        dep_type: DependencyType,
376        symbol: SymbolDependency,
377    ) -> Self {
378        Self {
379            from,
380            to,
381            dep_type,
382            symbol: Some(symbol),
383        }
384    }
385
386    /// Returns the effective dependency strength.
387    ///
388    /// If a symbol-level dependency is present, uses its strength.
389    /// Otherwise, defaults to [`DependencyStrength::Strong`] for import/trait
390    /// edges and [`DependencyStrength::Weak`] for export edges.
391    pub fn effective_strength(&self) -> DependencyStrength {
392        if let Some(ref sym) = self.symbol {
393            return sym.strength;
394        }
395        match self.dep_type {
396            DependencyType::Import | DependencyType::Trait | DependencyType::Macro => {
397                DependencyStrength::Strong
398            }
399            DependencyType::Export | DependencyType::Type => DependencyStrength::Weak,
400        }
401    }
402}
403
404impl std::fmt::Display for DependencyType {
405    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406        match self {
407            Self::Import => write!(f, "import"),
408            Self::Export => write!(f, "export"),
409            Self::Macro => write!(f, "macro"),
410            Self::Type => write!(f, "type"),
411            Self::Trait => write!(f, "trait"),
412        }
413    }
414}
415
416impl std::fmt::Display for DependencyStrength {
417    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418        match self {
419            Self::Strong => write!(f, "strong"),
420            Self::Weak => write!(f, "weak"),
421        }
422    }
423}
424
425impl std::fmt::Display for SymbolKind {
426    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427        match self {
428            Self::Function => write!(f, "function"),
429            Self::Class => write!(f, "class"),
430            Self::Interface => write!(f, "interface"),
431            Self::TypeAlias => write!(f, "type_alias"),
432            Self::Constant => write!(f, "constant"),
433            Self::Enum => write!(f, "enum"),
434            Self::Module => write!(f, "module"),
435            Self::Macro => write!(f, "macro"),
436        }
437    }
438}
439
440// ─── Tests (TDD: Written BEFORE implementation) ──────────────────────────────
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445
446    // ── AnalysisDefFingerprint Tests ─────────────────────────────────────
447
448    #[test]
449    fn test_fingerprint_new_creates_valid_fingerprint() {
450        let content = b"fn main() { println!(\"hello\"); }";
451        let fp = AnalysisDefFingerprint::new(content);
452
453        // Fingerprint should be 16 bytes
454        assert_eq!(fp.fingerprint.as_slice().len(), 16);
455        // No source files by default
456        assert!(fp.source_files.is_empty());
457        // Not yet analyzed
458        assert!(fp.last_analyzed.is_none());
459    }
460
461    #[test]
462    fn test_fingerprint_content_matches_same_content() {
463        let content = b"use std::collections::HashMap;";
464        let fp = AnalysisDefFingerprint::new(content);
465        assert!(fp.content_matches(content));
466    }
467
468    #[test]
469    fn test_fingerprint_content_does_not_match_different_content() {
470        let fp = AnalysisDefFingerprint::new(b"original content");
471        assert!(!fp.content_matches(b"modified content"));
472    }
473
474    #[test]
475    fn test_fingerprint_deterministic() {
476        let content = b"deterministic test content";
477        let fp1 = AnalysisDefFingerprint::new(content);
478        let fp2 = AnalysisDefFingerprint::new(content);
479        assert_eq!(fp1.fingerprint.as_slice(), fp2.fingerprint.as_slice());
480    }
481
482    #[test]
483    fn test_fingerprint_different_content_different_hash() {
484        let fp1 = AnalysisDefFingerprint::new(b"content A");
485        let fp2 = AnalysisDefFingerprint::new(b"content B");
486        assert_ne!(fp1.fingerprint.as_slice(), fp2.fingerprint.as_slice());
487    }
488
489    #[test]
490    fn test_fingerprint_empty_content() {
491        let fp = AnalysisDefFingerprint::new(b"");
492        assert_eq!(fp.fingerprint.as_slice().len(), 16);
493        assert!(fp.content_matches(b""));
494        assert!(!fp.content_matches(b"non-empty"));
495    }
496
497    #[test]
498    fn test_fingerprint_with_sources() {
499        let sources: RapidSet<PathBuf> = [
500            PathBuf::from("src/utils.rs"),
501            PathBuf::from("src/config.rs"),
502        ]
503        .into_iter()
504        .collect();
505        let fp = AnalysisDefFingerprint::with_sources(b"content", sources.clone());
506        assert_eq!(fp.source_files, sources);
507        assert!(fp.content_matches(b"content"));
508    }
509
510    #[test]
511    fn test_fingerprint_update_changes_hash() {
512        let fp = AnalysisDefFingerprint::new(b"old content");
513        let updated = fp.update_fingerprint(b"new content");
514
515        assert_ne!(
516            fp.fingerprint.as_slice(),
517            updated.fingerprint.as_slice(),
518            "Updated fingerprint should differ from original"
519        );
520        assert!(updated.content_matches(b"new content"));
521        assert!(!updated.content_matches(b"old content"));
522    }
523
524    #[test]
525    fn test_fingerprint_update_preserves_source_files() {
526        let sources: RapidSet<PathBuf> = [PathBuf::from("dep.rs")].into_iter().collect();
527        let fp = AnalysisDefFingerprint::with_sources(b"old", sources.clone());
528        let updated = fp.update_fingerprint(b"new");
529        assert_eq!(updated.source_files, sources);
530    }
531
532    #[test]
533    fn test_fingerprint_update_resets_timestamp() {
534        let mut fp = AnalysisDefFingerprint::new(b"content");
535        fp.set_last_analyzed(1000000);
536        let updated = fp.update_fingerprint(b"new content");
537        assert!(
538            updated.last_analyzed.is_none(),
539            "Updated fingerprint should reset timestamp"
540        );
541    }
542
543    #[test]
544    fn test_fingerprint_add_source_file() {
545        let mut fp = AnalysisDefFingerprint::new(b"content");
546        assert_eq!(fp.source_file_count(), 0);
547
548        fp.add_source_file(PathBuf::from("a.rs"));
549        assert_eq!(fp.source_file_count(), 1);
550
551        fp.add_source_file(PathBuf::from("b.rs"));
552        assert_eq!(fp.source_file_count(), 2);
553
554        // Duplicate should not increase count
555        fp.add_source_file(PathBuf::from("a.rs"));
556        assert_eq!(fp.source_file_count(), 2);
557    }
558
559    #[test]
560    fn test_fingerprint_remove_source_file() {
561        let mut fp = AnalysisDefFingerprint::with_sources(
562            b"content",
563            [PathBuf::from("a.rs"), PathBuf::from("b.rs")]
564                .into_iter()
565                .collect::<RapidSet<PathBuf>>(),
566        );
567
568        assert!(fp.remove_source_file(Path::new("a.rs")));
569        assert_eq!(fp.source_file_count(), 1);
570
571        // Removing non-existent returns false
572        assert!(!fp.remove_source_file(Path::new("c.rs")));
573        assert_eq!(fp.source_file_count(), 1);
574    }
575
576    #[test]
577    fn test_fingerprint_set_last_analyzed() {
578        let mut fp = AnalysisDefFingerprint::new(b"content");
579        assert!(fp.last_analyzed.is_none());
580
581        fp.set_last_analyzed(1_706_400_000_000_000); // Some timestamp
582        assert_eq!(fp.last_analyzed, Some(1_706_400_000_000_000));
583    }
584
585    #[test]
586    fn test_fingerprint_accessor() {
587        let fp = AnalysisDefFingerprint::new(b"test");
588        let fingerprint_ref = fp.fingerprint();
589        assert_eq!(fingerprint_ref.as_slice().len(), 16);
590    }
591
592    // ── DependencyEdge Tests ─────────────────────────────────────────────
593
594    #[test]
595    fn test_dependency_edge_new() {
596        let edge = DependencyEdge::new(
597            PathBuf::from("src/main.rs"),
598            PathBuf::from("src/utils.rs"),
599            DependencyType::Import,
600        );
601
602        assert_eq!(edge.from, PathBuf::from("src/main.rs"));
603        assert_eq!(edge.to, PathBuf::from("src/utils.rs"));
604        assert_eq!(edge.dep_type, DependencyType::Import);
605        assert!(edge.symbol.is_none());
606    }
607
608    #[test]
609    fn test_dependency_edge_with_symbol() {
610        let symbol = SymbolDependency {
611            from_symbol: "main".to_string(),
612            to_symbol: "parse_config".to_string(),
613            kind: SymbolKind::Function,
614            strength: DependencyStrength::Strong,
615        };
616
617        let edge = DependencyEdge::with_symbol(
618            PathBuf::from("main.rs"),
619            PathBuf::from("config.rs"),
620            DependencyType::Import,
621            symbol.clone(),
622        );
623
624        assert!(edge.symbol.is_some());
625        assert_eq!(edge.symbol.unwrap().to_symbol, "parse_config");
626    }
627
628    #[test]
629    fn test_dependency_edge_effective_strength_import() {
630        let edge = DependencyEdge::new(
631            PathBuf::from("a.rs"),
632            PathBuf::from("b.rs"),
633            DependencyType::Import,
634        );
635        assert_eq!(edge.effective_strength(), DependencyStrength::Strong);
636    }
637
638    #[test]
639    fn test_dependency_edge_effective_strength_export() {
640        let edge = DependencyEdge::new(
641            PathBuf::from("a.rs"),
642            PathBuf::from("b.rs"),
643            DependencyType::Export,
644        );
645        assert_eq!(edge.effective_strength(), DependencyStrength::Weak);
646    }
647
648    #[test]
649    fn test_dependency_edge_effective_strength_trait() {
650        let edge = DependencyEdge::new(
651            PathBuf::from("a.rs"),
652            PathBuf::from("b.rs"),
653            DependencyType::Trait,
654        );
655        assert_eq!(edge.effective_strength(), DependencyStrength::Strong);
656    }
657
658    #[test]
659    fn test_dependency_edge_effective_strength_macro() {
660        let edge = DependencyEdge::new(
661            PathBuf::from("a.rs"),
662            PathBuf::from("b.rs"),
663            DependencyType::Macro,
664        );
665        assert_eq!(edge.effective_strength(), DependencyStrength::Strong);
666    }
667
668    #[test]
669    fn test_dependency_edge_effective_strength_type() {
670        let edge = DependencyEdge::new(
671            PathBuf::from("a.rs"),
672            PathBuf::from("b.rs"),
673            DependencyType::Type,
674        );
675        assert_eq!(edge.effective_strength(), DependencyStrength::Weak);
676    }
677
678    #[test]
679    fn test_dependency_edge_symbol_overrides_strength() {
680        let symbol = SymbolDependency {
681            from_symbol: "a".to_string(),
682            to_symbol: "b".to_string(),
683            kind: SymbolKind::Function,
684            strength: DependencyStrength::Weak,
685        };
686
687        // Import would be Strong, but symbol overrides to Weak
688        let edge = DependencyEdge::with_symbol(
689            PathBuf::from("a.rs"),
690            PathBuf::from("b.rs"),
691            DependencyType::Import,
692            symbol,
693        );
694        assert_eq!(edge.effective_strength(), DependencyStrength::Weak);
695    }
696
697    #[test]
698    fn test_dependency_edge_equality() {
699        let edge1 = DependencyEdge::new(
700            PathBuf::from("a.rs"),
701            PathBuf::from("b.rs"),
702            DependencyType::Import,
703        );
704        let edge2 = DependencyEdge::new(
705            PathBuf::from("a.rs"),
706            PathBuf::from("b.rs"),
707            DependencyType::Import,
708        );
709        assert_eq!(edge1, edge2);
710    }
711
712    #[test]
713    fn test_dependency_edge_inequality_different_type() {
714        let edge1 = DependencyEdge::new(
715            PathBuf::from("a.rs"),
716            PathBuf::from("b.rs"),
717            DependencyType::Import,
718        );
719        let edge2 = DependencyEdge::new(
720            PathBuf::from("a.rs"),
721            PathBuf::from("b.rs"),
722            DependencyType::Export,
723        );
724        assert_ne!(edge1, edge2);
725    }
726
727    // ── DependencyEdge Serialization Tests ───────────────────────────────
728
729    #[test]
730    fn test_dependency_edge_serialization_roundtrip() {
731        let edge = DependencyEdge::new(
732            PathBuf::from("src/main.rs"),
733            PathBuf::from("src/lib.rs"),
734            DependencyType::Import,
735        );
736
737        let json = serde_json::to_string(&edge).expect("serialize");
738        let deserialized: DependencyEdge = serde_json::from_str(&json).expect("deserialize");
739
740        assert_eq!(edge, deserialized);
741    }
742
743    #[test]
744    fn test_dependency_edge_with_symbol_serialization_roundtrip() {
745        let symbol = SymbolDependency {
746            from_symbol: "handler".to_string(),
747            to_symbol: "Router".to_string(),
748            kind: SymbolKind::Class,
749            strength: DependencyStrength::Strong,
750        };
751
752        let edge = DependencyEdge::with_symbol(
753            PathBuf::from("api.rs"),
754            PathBuf::from("router.rs"),
755            DependencyType::Import,
756            symbol,
757        );
758
759        let json = serde_json::to_string(&edge).expect("serialize");
760        let deserialized: DependencyEdge = serde_json::from_str(&json).expect("deserialize");
761
762        assert_eq!(edge, deserialized);
763    }
764
765    // ── DependencyType Display Tests ─────────────────────────────────────
766
767    #[test]
768    fn test_dependency_type_display() {
769        assert_eq!(format!("{}", DependencyType::Import), "import");
770        assert_eq!(format!("{}", DependencyType::Export), "export");
771        assert_eq!(format!("{}", DependencyType::Macro), "macro");
772        assert_eq!(format!("{}", DependencyType::Type), "type");
773        assert_eq!(format!("{}", DependencyType::Trait), "trait");
774    }
775
776    #[test]
777    fn test_dependency_strength_display() {
778        assert_eq!(format!("{}", DependencyStrength::Strong), "strong");
779        assert_eq!(format!("{}", DependencyStrength::Weak), "weak");
780    }
781
782    #[test]
783    fn test_symbol_kind_display() {
784        assert_eq!(format!("{}", SymbolKind::Function), "function");
785        assert_eq!(format!("{}", SymbolKind::Class), "class");
786        assert_eq!(format!("{}", SymbolKind::Interface), "interface");
787        assert_eq!(format!("{}", SymbolKind::TypeAlias), "type_alias");
788        assert_eq!(format!("{}", SymbolKind::Constant), "constant");
789        assert_eq!(format!("{}", SymbolKind::Enum), "enum");
790        assert_eq!(format!("{}", SymbolKind::Module), "module");
791        assert_eq!(format!("{}", SymbolKind::Macro), "macro");
792    }
793
794    // ── SymbolDependency Tests ───────────────────────────────────────────
795
796    #[test]
797    fn test_symbol_dependency_creation() {
798        let dep = SymbolDependency {
799            from_symbol: "parse".to_string(),
800            to_symbol: "Config".to_string(),
801            kind: SymbolKind::Class,
802            strength: DependencyStrength::Strong,
803        };
804
805        assert_eq!(dep.from_symbol, "parse");
806        assert_eq!(dep.to_symbol, "Config");
807        assert_eq!(dep.kind, SymbolKind::Class);
808        assert_eq!(dep.strength, DependencyStrength::Strong);
809    }
810
811    #[test]
812    fn test_symbol_dependency_serialization_roundtrip() {
813        let dep = SymbolDependency {
814            from_symbol: "main".to_string(),
815            to_symbol: "run_server".to_string(),
816            kind: SymbolKind::Function,
817            strength: DependencyStrength::Strong,
818        };
819
820        let json = serde_json::to_string(&dep).expect("serialize");
821        let deserialized: SymbolDependency = serde_json::from_str(&json).expect("deserialize");
822
823        assert_eq!(dep, deserialized);
824    }
825
826    // ── Large Content Tests ──────────────────────────────────────────────
827
828    #[test]
829    fn test_fingerprint_large_content() {
830        // 1MB of content
831        let large_content: Vec<u8> = (0..1_000_000).map(|i| (i % 256) as u8).collect();
832        let fp = AnalysisDefFingerprint::new(&large_content);
833        assert!(fp.content_matches(&large_content));
834
835        // Changing one byte should invalidate
836        let mut modified = large_content.clone();
837        modified[500_000] = modified[500_000].wrapping_add(1);
838        assert!(!fp.content_matches(&modified));
839    }
840
841    #[test]
842    fn test_fingerprint_binary_content() {
843        // Binary content (null bytes, high bytes)
844        let binary = vec![0u8, 1, 255, 128, 0, 0, 64, 32];
845        let fp = AnalysisDefFingerprint::new(&binary);
846        assert!(fp.content_matches(&binary));
847    }
848}