Skip to main content

forge_core/analysis/
mod.rs

1//! Analysis module - Combined analysis operations
2//!
3//! This module provides composite operations using graph, search, cfg, and edit modules.
4
5use crate::graph::GraphModule;
6use crate::search::SearchModule;
7use crate::cfg::CfgModule;
8use crate::edit::EditModule;
9use crate::error::Result;
10use crate::types::Symbol;
11use std::time::Instant;
12
13
14pub mod dead_code;
15pub mod complexity;
16pub mod modules;
17
18pub use dead_code::{DeadCodeAnalyzer, DeadSymbol};
19pub use complexity::{ComplexityMetrics, RiskLevel};
20pub use modules::{ModuleAnalyzer, ModuleDependencyGraph, ModuleInfo};
21
22/// Analysis module for combined operations.
23pub struct AnalysisModule {
24    graph: GraphModule,
25    search: SearchModule,
26    cfg: CfgModule,
27    edit: EditModule,
28}
29
30/// Detailed impact analysis result for a symbol.
31#[derive(Debug, Clone)]
32pub struct ImpactData {
33    /// Symbol that was analyzed
34    pub symbol: String,
35    /// Number of references to this symbol
36    pub ref_count: usize,
37    /// Number of call sites (for functions)
38    pub call_count: usize,
39    /// All symbols that reference this one
40    pub referenced_by: Vec<Symbol>,
41    /// All symbols this one references
42    pub references: Vec<Symbol>,
43    /// Total estimated impact score
44    pub impact_score: usize,
45}
46
47/// Chain of references from one symbol to another.
48#[derive(Debug, Clone)]
49pub struct ReferenceChain {
50    /// Starting symbol
51    pub from: String,
52    /// Ending symbol
53    pub to: String,
54    /// Chain of symbols connecting from to to
55    pub chain: Vec<Symbol>,
56    /// Length of the chain
57    pub length: usize,
58}
59
60/// Call chain showing all callers to a function.
61#[derive(Debug, Clone)]
62pub struct CallChain {
63    /// Target function
64    pub target: String,
65    /// All callers (direct and indirect)
66    pub callers: Vec<Symbol>,
67    /// Maximum depth of call chain
68    pub depth: usize,
69}
70
71/// Performance benchmark results.
72#[derive(Debug, Clone)]
73pub struct BenchmarkResults {
74    /// Time to perform impact analysis
75    pub impact_analysis_ms: f64,
76    /// Time to find dead code
77    pub dead_code_ms: f64,
78    /// Time to compute reference chain
79    pub reference_chain_ms: f64,
80    /// Time to compute call chain
81    pub call_chain_ms: f64,
82    /// Total benchmark time
83    pub total_ms: f64,
84}
85
86/// Result of applying an edit operation.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum ApplyResult {
89    /// Operation was applied successfully
90    Applied,
91    /// Operation always returns an error
92    AlwaysError,
93    /// Operation is pending verification
94    Pending,
95    /// Operation failed with reason
96    Failed(String),
97}
98
99/// Diff showing changes between before and after.
100#[derive(Debug, Clone)]
101pub struct Diff {
102    /// Original content
103    pub original: String,
104    /// New content
105    pub new: String,
106    /// Changed lines
107    pub changed_lines: Vec<usize>,
108}
109
110impl Diff {
111    /// Create a new diff.
112    pub fn new(original: String, new: String) -> Self {
113        let changed_lines = compute_changed_lines(&original, &new);
114        Self { original, new, changed_lines }
115    }
116
117    /// Returns true if there are any changes.
118    pub fn has_changes(&self) -> bool {
119        !self.changed_lines.is_empty()
120    }
121}
122
123/// Compute which lines changed between two strings.
124fn compute_changed_lines(original: &str, new: &str) -> Vec<usize> {
125    let orig_lines: Vec<&str> = original.lines().collect();
126    let new_lines: Vec<&str> = new.lines().collect();
127
128    let mut changed = Vec::new();
129
130    for (i, (o, n)) in orig_lines.iter().zip(new_lines.iter()).enumerate() {
131        if o != n {
132            changed.push(i);
133        }
134    }
135
136    // Handle lines added at the end
137    if new_lines.len() > orig_lines.len() {
138        for i in orig_lines.len()..new_lines.len() {
139            changed.push(i);
140        }
141    }
142
143    changed
144}
145
146/// Edit operation trait for code transformations.
147#[async_trait::async_trait]
148pub trait EditOperation: Send + Sync {
149    /// Verify the operation can be applied.
150    async fn verify(&self, module: &AnalysisModule) -> Result<ApplyResult>;
151
152    /// Preview the changes without applying.
153    async fn preview(&self, module: &AnalysisModule) -> Result<Diff>;
154
155    /// Apply the operation.
156    async fn apply(&self, module: &mut AnalysisModule) -> Result<ApplyResult>;
157}
158
159/// Insert content at a specific location.
160#[derive(Debug, Clone)]
161pub struct InsertOperation {
162    /// Symbol to insert content after
163    pub after_symbol: String,
164    /// Content to insert
165    pub content: String,
166}
167
168#[async_trait::async_trait]
169impl EditOperation for InsertOperation {
170    async fn verify(&self, module: &AnalysisModule) -> Result<ApplyResult> {
171        // Check if the symbol exists
172        let symbols = module.graph().find_symbol(&self.after_symbol).await?;
173
174        if symbols.is_empty() {
175            return Ok(ApplyResult::Failed(format!("Symbol '{}' not found", self.after_symbol)));
176        }
177
178        Ok(ApplyResult::Pending)
179    }
180
181    async fn preview(&self, module: &AnalysisModule) -> Result<Diff> {
182        let symbols = module.graph().find_symbol(&self.after_symbol).await?;
183
184        if symbols.is_empty() {
185            return Ok(Diff::new(
186                String::from(""),
187                format!("// Would insert after: {}\n{}", self.after_symbol, self.content),
188            ));
189        }
190
191        let original = format!("// Original content at {}\n", self.after_symbol);
192        let new_content = format!("{}\n// Inserted content\n{}", original, self.content);
193
194        Ok(Diff::new(original, new_content))
195    }
196
197    async fn apply(&self, _module: &mut AnalysisModule) -> Result<ApplyResult> {
198        // v0.1: Placeholder - would actually modify code
199        tracing::info!("InsertOperation: inserting {} after {}", self.content.len(), self.after_symbol);
200        Ok(ApplyResult::Applied)
201    }
202}
203
204/// Delete a symbol by name.
205#[derive(Debug, Clone)]
206pub struct DeleteOperation {
207    /// Name of symbol to delete
208    pub symbol_name: String,
209}
210
211#[async_trait::async_trait]
212impl EditOperation for DeleteOperation {
213    async fn verify(&self, module: &AnalysisModule) -> Result<ApplyResult> {
214        let symbols = module.graph().find_symbol(&self.symbol_name).await?;
215
216        if symbols.is_empty() {
217            return Ok(ApplyResult::Failed(format!("Symbol '{}' not found", self.symbol_name)));
218        }
219
220        // Check if anything references this symbol
221        let refs = module.graph().references(&self.symbol_name).await?;
222
223        if !refs.is_empty() {
224            return Ok(ApplyResult::Failed(format!(
225                "Cannot delete '{}': still referenced by {} symbols",
226                self.symbol_name,
227                refs.len()
228            )));
229        }
230
231        Ok(ApplyResult::Pending)
232    }
233
234    async fn preview(&self, _module: &AnalysisModule) -> Result<Diff> {
235        let original = format!("fn {}() {{\n    // original implementation\n}}\n", self.symbol_name);
236        let new_content = String::from("// Symbol deleted\n");
237
238        Ok(Diff::new(original, new_content))
239    }
240
241    async fn apply(&self, _module: &mut AnalysisModule) -> Result<ApplyResult> {
242        tracing::info!("DeleteOperation: deleting symbol '{}'", self.symbol_name);
243        Ok(ApplyResult::Applied)
244    }
245}
246
247/// Rename a symbol with validation.
248#[derive(Debug, Clone)]
249pub struct RenameOperation {
250    /// Current symbol name
251    pub old_name: String,
252    /// New symbol name
253    pub new_name: String,
254}
255
256impl RenameOperation {
257    /// Create a new rename operation.
258    pub fn new(old_name: impl Into<String>, new_name: impl Into<String>) -> Self {
259        Self {
260            old_name: old_name.into(),
261            new_name: new_name.into(),
262        }
263    }
264
265    /// Validate the new name is acceptable.
266    fn validate_name(&self) -> Result<()> {
267        if self.new_name.is_empty() {
268            return Err(crate::error::ForgeError::InvalidQuery("New name cannot be empty".to_string()));
269        }
270
271        if self.new_name.chars().any(|c| c.is_whitespace()) {
272            return Err(crate::error::ForgeError::InvalidQuery("New name cannot contain spaces".to_string()));
273        }
274
275        // Check if it's a valid Rust identifier
276        if !self.new_name.chars().next().map(|c| c.is_alphabetic() || c == '_').unwrap_or(false) {
277            return Err(crate::error::ForgeError::InvalidQuery(
278                "New name must start with a letter or underscore".to_string(),
279            ));
280        }
281
282        Ok(())
283    }
284}
285
286#[async_trait::async_trait]
287impl EditOperation for RenameOperation {
288    async fn verify(&self, module: &AnalysisModule) -> Result<ApplyResult> {
289        // First validate the new name format
290        if let Err(e) = self.validate_name() {
291            return Ok(ApplyResult::Failed(e.to_string()));
292        }
293
294        // Check if old symbol exists
295        let old_symbols = module.graph().find_symbol(&self.old_name).await?;
296
297        if old_symbols.is_empty() {
298            return Ok(ApplyResult::Failed(format!("Symbol '{}' not found", self.old_name)));
299        }
300
301        // Check if new name already exists
302        let new_symbols = module.graph().find_symbol(&self.new_name).await?;
303
304        if !new_symbols.is_empty() {
305            return Ok(ApplyResult::Failed(format!(
306                "Cannot rename to '{}': symbol already exists",
307                self.new_name
308            )));
309        }
310
311        Ok(ApplyResult::Pending)
312    }
313
314    async fn preview(&self, _module: &AnalysisModule) -> Result<Diff> {
315        let original = format!("fn {}()", self.old_name);
316        let new_content = format!("fn {}()", self.new_name);
317
318        Ok(Diff::new(original, new_content))
319    }
320
321    async fn apply(&self, module: &mut AnalysisModule) -> Result<ApplyResult> {
322        // Use the edit module to perform the rename
323        let result = module.edit().rename_symbol(&self.old_name, &self.new_name).await?;
324
325        if result.success {
326            Ok(ApplyResult::Applied)
327        } else {
328            Ok(ApplyResult::Failed(result.error.unwrap_or_default()))
329        }
330    }
331}
332
333/// Error result - operation always fails.
334#[derive(Debug, Clone)]
335pub struct ErrorResult {
336    pub reason: String,
337}
338
339impl ErrorResult {
340    /// Create a new error result.
341    pub fn new(reason: impl Into<String>) -> Self {
342        Self {
343            reason: reason.into(),
344        }
345    }
346}
347
348#[async_trait::async_trait]
349impl EditOperation for ErrorResult {
350    async fn verify(&self, _module: &AnalysisModule) -> Result<ApplyResult> {
351        Ok(ApplyResult::AlwaysError)
352    }
353
354    async fn preview(&self, _module: &AnalysisModule) -> Result<Diff> {
355        Ok(Diff::new(
356            format!("// Error: {}", self.reason),
357            format!("// Error: {}", self.reason),
358        ))
359    }
360
361    async fn apply(&self, _module: &mut AnalysisModule) -> Result<ApplyResult> {
362        Ok(ApplyResult::Failed(self.reason.clone()))
363    }
364}
365
366/// Impact analysis result.
367#[derive(Debug, Clone)]
368pub struct ImpactAnalysis {
369    /// Symbols that would be affected by a change
370    pub affected_symbols: Vec<Symbol>,
371    /// Total number of call sites
372    pub call_sites: usize,
373}
374
375/// Cross-reference information for a symbol.
376#[derive(Debug, Clone)]
377pub struct CrossReferences {
378    /// Symbols that call the target
379    pub callers: Vec<Symbol>,
380    /// Symbols called by the target
381    pub callees: Vec<Symbol>,
382}
383
384/// Module dependency.
385#[derive(Debug, Clone)]
386pub struct ModuleDependency {
387    /// Source module
388    pub from: String,
389    /// Target module
390    pub to: String,
391}
392
393impl AnalysisModule {
394    /// Create a new AnalysisModule.
395    pub fn new(graph: GraphModule, cfg: CfgModule, edit: EditModule, search: SearchModule) -> Self {
396        Self { graph, search, cfg, edit }
397    }
398
399    /// Get the graph module
400    pub fn graph(&self) -> &GraphModule {
401        &self.graph
402    }
403
404    /// Get the search module
405    pub fn search(&self) -> &SearchModule {
406        &self.search
407    }
408
409    /// Get the CFG module
410    pub fn cfg(&self) -> &CfgModule {
411        &self.cfg
412    }
413
414    /// Get the edit module
415    pub fn edit(&self) -> &EditModule {
416        &self.edit
417    }
418
419    /// Analyze the impact of changing a symbol.
420    ///
421    /// Returns detailed impact data including references, calls, and impact score.
422    pub async fn impact_analysis(&self, symbol: &str) -> Result<ImpactData> {
423        let start = Instant::now();
424
425        // Get all callers
426        let callers = self.graph.callers_of(symbol).await
427            .unwrap_or_default();
428
429        // Get all references
430        let refs = self.graph.references(symbol).await
431            .unwrap_or_default();
432
433        // Also search for the symbol to get its metadata
434        let _symbol_info = self.graph.find_symbol(symbol).await
435            .unwrap_or_default()
436            .into_iter()
437            .next();
438
439        // Compute impact score based on reference and call counts
440        let ref_count = refs.len();
441        let call_count = callers.len();
442        let impact_score = ref_count + call_count * 2; // Calls weigh more
443
444        tracing::debug!("Impact analysis for '{}' completed in {:?}", symbol, start.elapsed());
445
446        Ok(ImpactData {
447            symbol: symbol.to_string(),
448            ref_count,
449            call_count,
450            referenced_by: callers.into_iter()
451                .filter_map(|_r| {
452                    // Try to resolve the symbol ID to a Symbol
453                    None // v0.1: would need symbol lookup
454                })
455                .collect(),
456            references: refs.into_iter()
457                .filter_map(|_r| None)
458                .collect(),
459            impact_score,
460        })
461    }
462
463    /// Analyze the impact of changing a symbol.
464    ///
465    /// Returns all symbols that would be affected by modifying the given symbol.
466    pub async fn analyze_impact(&self, symbol_name: &str) -> Result<ImpactAnalysis> {
467        let impact = self.impact_analysis(symbol_name).await?;
468        Ok(ImpactAnalysis {
469            affected_symbols: impact.referenced_by,
470            call_sites: impact.call_count,
471        })
472    }
473
474    /// Find dead code in the codebase.
475    ///
476    /// Returns symbols that are defined but never called/referenced.
477    pub async fn dead_code_detection(&self) -> Result<Vec<Symbol>> {
478        let start = Instant::now();
479
480        let db_path = self.graph.store().db_path().join("graph.db");
481
482        // Check if database exists first
483        if !db_path.exists() {
484            tracing::debug!("No graph database found at {:?}, returning empty dead code list", db_path);
485            return Ok(Vec::new());
486        }
487
488        let analyzer = dead_code::DeadCodeAnalyzer::new(&db_path);
489
490        match analyzer.find_dead_code() {
491            Ok(dead_symbols) => {
492                let result: Vec<Symbol> = dead_symbols.into_iter().map(Into::into).collect();
493                tracing::debug!("Dead code detection found {} symbols in {:?}", result.len(), start.elapsed());
494                Ok(result)
495            }
496            Err(e) => {
497                tracing::warn!("Dead code detection failed: {}", e);
498                // Return empty list on error rather than failing
499                Ok(Vec::new())
500            }
501        }
502    }
503
504    /// Perform deep impact analysis with k-hop traversal.
505    ///
506    /// Returns all symbols within k hops of the target symbol.
507    pub async fn deep_impact_analysis(&self, symbol_name: &str, depth: u32) -> Result<Vec<crate::graph::queries::ImpactedSymbol>> {
508        self.graph.impact_analysis(symbol_name, Some(depth)).await
509    }
510
511    /// Find dead code in the codebase.
512    ///
513    /// Returns symbols that are defined but never called/referenced.
514    pub async fn find_dead_code(&self) -> Result<Vec<Symbol>> {
515        self.dead_code_detection().await
516    }
517
518    /// Trace the reference chain from a symbol.
519    ///
520    /// Returns an ordered list showing how symbols reference each other.
521    pub async fn reference_chain(&self, symbol: &str) -> Result<Vec<Symbol>> {
522        let start = Instant::now();
523
524        // Get all symbols this one references
525        let refs = self.graph.references(symbol).await?;
526
527        // In a full implementation, we would recursively follow references
528        // For v0.1, return direct references only
529        let chain: Vec<Symbol> = refs.into_iter()
530            .filter_map(|_r| {
531                // Try to resolve to a Symbol
532                None // v0.1: would need symbol lookup
533            })
534            .collect();
535
536        tracing::debug!("Reference chain for '{}' has {} symbols, found in {:?}", symbol, chain.len(), start.elapsed());
537        Ok(chain)
538    }
539
540    /// Trace all callers to a function.
541    ///
542    /// Returns an ordered list of calling symbols.
543    pub async fn call_chain(&self, symbol: &str) -> Result<Vec<Symbol>> {
544        let start = Instant::now();
545
546        let callers = self.graph.callers_of(symbol).await?;
547
548        // For v0.1, return direct callers
549        let chain: Vec<Symbol> = callers.into_iter()
550            .filter_map(|_| None)
551            .collect();
552
553        tracing::debug!("Call chain for '{}' has {} symbols, found in {:?}", symbol, chain.len(), start.elapsed());
554        Ok(chain)
555    }
556
557    /// Run performance benchmarks for key operations.
558    ///
559    /// Returns timing data for each operation type.
560    pub async fn benchmarks(&self) -> Result<BenchmarkResults> {
561        let total_start = Instant::now();
562
563        // Benchmark impact analysis
564        let impact_start = Instant::now();
565        let _ = self.impact_analysis("test_symbol").await;
566        let impact_analysis_ms = impact_start.elapsed().as_secs_f64() * 1000.0;
567
568        // Benchmark dead code detection
569        let dead_start = Instant::now();
570        let _ = self.dead_code_detection().await;
571        let dead_code_ms = dead_start.elapsed().as_secs_f64() * 1000.0;
572
573        // Benchmark reference chain
574        let ref_start = Instant::now();
575        let _ = self.reference_chain("test_symbol").await;
576        let reference_chain_ms = ref_start.elapsed().as_secs_f64() * 1000.0;
577
578        // Benchmark call chain
579        let call_start = Instant::now();
580        let _ = self.call_chain("test_symbol").await;
581        let call_chain_ms = call_start.elapsed().as_secs_f64() * 1000.0;
582
583        let total_ms = total_start.elapsed().as_secs_f64() * 1000.0;
584
585        Ok(BenchmarkResults {
586            impact_analysis_ms,
587            dead_code_ms,
588            reference_chain_ms,
589            call_chain_ms,
590            total_ms,
591        })
592    }
593
594    /// Calculate complexity metrics for a function.
595    ///
596    /// Returns cyclomatic complexity and other metrics.
597    /// Uses source-based estimation as CFG extraction is done during indexing.
598    pub async fn complexity_metrics(&self, _symbol_name: &str) -> Result<ComplexityMetrics> {
599        // v0.1: Placeholder - real implementation would look up cached CFG
600        // from the storage and analyze it
601        Ok(ComplexityMetrics {
602            cyclomatic_complexity: 1,
603            lines_of_code: 1,
604            max_nesting_depth: 0,
605            decision_points: 0,
606        })
607    }
608
609    /// Calculate complexity from source code directly.
610    pub fn analyze_source_complexity(&self, source: &str) -> ComplexityMetrics {
611        let metrics = complexity::analyze_source_complexity(source);
612        ComplexityMetrics {
613            cyclomatic_complexity: metrics.cyclomatic_complexity,
614            lines_of_code: metrics.lines_of_code,
615            max_nesting_depth: metrics.max_nesting_depth,
616            decision_points: metrics.decision_points,
617        }
618    }
619
620    /// Get cross-references for a symbol.
621    ///
622    /// Returns both callers (incoming) and callees (outgoing).
623    pub async fn cross_references(&self, symbol_name: &str) -> Result<CrossReferences> {
624        let _caller_refs = self.graph.callers_of(symbol_name).await?;
625        let _callee_refs = self.graph.references(symbol_name).await?;
626        
627        // v0.1: We return empty symbol lists since we can't easily
628        // resolve references to symbols without additional lookups
629        Ok(CrossReferences {
630            callers: Vec::new(),
631            callees: Vec::new(),
632        })
633    }
634
635    /// Analyze module dependencies.
636    ///
637    /// Returns dependencies between modules in the codebase.
638    pub async fn module_dependencies(&self) -> Result<Vec<ModuleDependency>> {
639        let db_path = self.graph.store().db_path();
640        let analyzer = ModuleAnalyzer::new(db_path);
641        
642        let graph = analyzer.analyze_dependencies()?;
643        
644        let mut deps = Vec::new();
645        for (from, tos) in graph.dependencies {
646            for to in tos {
647                deps.push(ModuleDependency { from: from.clone(), to });
648            }
649        }
650        
651        Ok(deps)
652    }
653
654    /// Find circular dependencies between modules.
655    pub async fn find_dependency_cycles(&self) -> Result<Vec<Vec<String>>> {
656        let db_path = self.graph.store().db_path();
657        let analyzer = ModuleAnalyzer::new(db_path);
658        analyzer.find_cycles()
659    }
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665    use crate::storage::BackendKind;
666
667    #[tokio::test]
668    async fn test_impact_data_creation() {
669        let impact = ImpactData {
670            symbol: "test_function".to_string(),
671            ref_count: 5,
672            call_count: 3,
673            referenced_by: vec![],
674            references: vec![],
675            impact_score: 11,
676        };
677        assert_eq!(impact.symbol, "test_function");
678        assert_eq!(impact.ref_count, 5);
679        assert_eq!(impact.call_count, 3);
680        assert_eq!(impact.impact_score, 11);
681    }
682
683    #[tokio::test]
684    async fn test_impact_analysis() {
685        let temp_dir = tempfile::tempdir().unwrap();
686        let store = std::sync::Arc::new(
687            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
688                .await.unwrap()
689        );
690        let graph = GraphModule::new(store.clone());
691        let search = SearchModule::new(store.clone());
692        let cfg = CfgModule::new(store.clone());
693        let edit = EditModule::new(store);
694
695        let analysis = AnalysisModule::new(graph, cfg, edit, search);
696        let impact = analysis.impact_analysis("nonexistent").await.unwrap();
697
698        assert_eq!(impact.symbol, "nonexistent");
699        assert_eq!(impact.ref_count, 0);
700        assert_eq!(impact.call_count, 0);
701    }
702
703    #[tokio::test]
704    async fn test_dead_code_detection() {
705        let temp_dir = tempfile::tempdir().unwrap();
706        let store = std::sync::Arc::new(
707            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
708                .await.unwrap()
709        );
710        let graph = GraphModule::new(store.clone());
711        let search = SearchModule::new(store.clone());
712        let cfg = CfgModule::new(store.clone());
713        let edit = EditModule::new(store);
714
715        let analysis = AnalysisModule::new(graph, cfg, edit, search);
716        let dead_code = analysis.dead_code_detection().await.unwrap();
717
718        // Empty database should return no dead code
719        assert!(dead_code.is_empty());
720    }
721
722    #[tokio::test]
723    async fn test_reference_chain() {
724        let temp_dir = tempfile::tempdir().unwrap();
725        let store = std::sync::Arc::new(
726            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
727                .await.unwrap()
728        );
729        let graph = GraphModule::new(store.clone());
730        let search = SearchModule::new(store.clone());
731        let cfg = CfgModule::new(store.clone());
732        let edit = EditModule::new(store);
733
734        let analysis = AnalysisModule::new(graph, cfg, edit, search);
735        let chain = analysis.reference_chain("test_symbol").await.unwrap();
736
737        // Should return empty chain for non-existent symbol
738        assert!(chain.is_empty());
739    }
740
741    #[tokio::test]
742    async fn test_call_chain() {
743        let temp_dir = tempfile::tempdir().unwrap();
744        let store = std::sync::Arc::new(
745            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
746                .await.unwrap()
747        );
748        let graph = GraphModule::new(store.clone());
749        let search = SearchModule::new(store.clone());
750        let cfg = CfgModule::new(store.clone());
751        let edit = EditModule::new(store);
752
753        let analysis = AnalysisModule::new(graph, cfg, edit, search);
754        let chain = analysis.call_chain("test_function").await.unwrap();
755
756        // Should return empty chain for non-existent function
757        assert!(chain.is_empty());
758    }
759
760    #[tokio::test]
761    async fn test_benchmarks() {
762        let temp_dir = tempfile::tempdir().unwrap();
763        let store = std::sync::Arc::new(
764            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
765                .await.unwrap()
766        );
767        let graph = GraphModule::new(store.clone());
768        let search = SearchModule::new(store.clone());
769        let cfg = CfgModule::new(store.clone());
770        let edit = EditModule::new(store);
771
772        let analysis = AnalysisModule::new(graph, cfg, edit, search);
773        let benchmarks = analysis.benchmarks().await.unwrap();
774
775        // All timings should be non-negative
776        assert!(benchmarks.impact_analysis_ms >= 0.0);
777        assert!(benchmarks.dead_code_ms >= 0.0);
778        assert!(benchmarks.reference_chain_ms >= 0.0);
779        assert!(benchmarks.call_chain_ms >= 0.0);
780        assert!(benchmarks.total_ms >= 0.0);
781    }
782
783    #[tokio::test]
784    async fn test_analyze_impact_backward_compat() {
785        let temp_dir = tempfile::tempdir().unwrap();
786        let store = std::sync::Arc::new(
787            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
788                .await.unwrap()
789        );
790        let graph = GraphModule::new(store.clone());
791        let search = SearchModule::new(store.clone());
792        let cfg = CfgModule::new(store.clone());
793        let edit = EditModule::new(store);
794
795        let analysis = AnalysisModule::new(graph, cfg, edit, search);
796        let impact = analysis.analyze_impact("test").await.unwrap();
797
798        // Backward compatible API
799        assert_eq!(impact.call_sites, 0);
800        assert!(impact.affected_symbols.is_empty());
801    }
802
803    #[tokio::test]
804    async fn test_find_dead_code_backward_compat() {
805        let temp_dir = tempfile::tempdir().unwrap();
806        let store = std::sync::Arc::new(
807            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
808                .await.unwrap()
809        );
810        let graph = GraphModule::new(store.clone());
811        let search = SearchModule::new(store.clone());
812        let cfg = CfgModule::new(store.clone());
813        let edit = EditModule::new(store);
814
815        let analysis = AnalysisModule::new(graph, cfg, edit, search);
816        let dead_code = analysis.find_dead_code().await.unwrap();
817
818        // Empty database should return no dead code
819        assert!(dead_code.is_empty());
820    }
821
822    #[test]
823    fn test_impact_analysis_creation() {
824        let impact = ImpactAnalysis {
825            affected_symbols: vec![],
826            call_sites: 0,
827        };
828        assert!(impact.affected_symbols.is_empty());
829        assert_eq!(impact.call_sites, 0);
830    }
831
832    #[test]
833    fn test_cross_references_creation() {
834        let xrefs = CrossReferences {
835            callers: vec![],
836            callees: vec![],
837        };
838        assert!(xrefs.callers.is_empty());
839        assert!(xrefs.callees.is_empty());
840    }
841
842    #[test]
843    fn test_complexity_metrics_creation() {
844        let metrics = ComplexityMetrics {
845            cyclomatic_complexity: 5,
846            lines_of_code: 100,
847            max_nesting_depth: 3,
848            decision_points: 4,
849        };
850        assert_eq!(metrics.cyclomatic_complexity, 5);
851        assert_eq!(metrics.lines_of_code, 100);
852        assert_eq!(metrics.max_nesting_depth, 3);
853        assert_eq!(metrics.decision_points, 4);
854    }
855
856    #[test]
857    fn test_module_dependency_creation() {
858        let dep = ModuleDependency {
859            from: "mod_a".to_string(),
860            to: "mod_b".to_string(),
861        };
862        assert_eq!(dep.from, "mod_a");
863        assert_eq!(dep.to, "mod_b");
864    }
865
866    // EditOperation trait tests
867
868    #[tokio::test]
869    async fn test_insert_operation_verify() {
870        let temp_dir = tempfile::tempdir().unwrap();
871        let store = std::sync::Arc::new(
872            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
873                .await.unwrap()
874        );
875        let graph = GraphModule::new(store.clone());
876        let search = SearchModule::new(store.clone());
877        let cfg = CfgModule::new(store.clone());
878        let edit = EditModule::new(store);
879
880        let analysis = AnalysisModule::new(graph, cfg, edit, search);
881        let insert = InsertOperation {
882            after_symbol: "nonexistent".to_string(),
883            content: "// new content".to_string(),
884        };
885
886        let result = insert.verify(&analysis).await.unwrap();
887        assert!(matches!(result, ApplyResult::Failed(_)));
888    }
889
890    #[tokio::test]
891    async fn test_insert_operation_preview() {
892        let temp_dir = tempfile::tempdir().unwrap();
893        let store = std::sync::Arc::new(
894            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
895                .await.unwrap()
896        );
897        let graph = GraphModule::new(store.clone());
898        let search = SearchModule::new(store.clone());
899        let cfg = CfgModule::new(store.clone());
900        let edit = EditModule::new(store);
901
902        let analysis = AnalysisModule::new(graph, cfg, edit, search);
903        let insert = InsertOperation {
904            after_symbol: "test_symbol".to_string(),
905            content: "// new content".to_string(),
906        };
907
908        let diff = insert.preview(&analysis).await.unwrap();
909        assert!(!diff.new.is_empty());
910    }
911
912    #[tokio::test]
913    async fn test_delete_operation_verify_not_found() {
914        let temp_dir = tempfile::tempdir().unwrap();
915        let store = std::sync::Arc::new(
916            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
917                .await.unwrap()
918        );
919        let graph = GraphModule::new(store.clone());
920        let search = SearchModule::new(store.clone());
921        let cfg = CfgModule::new(store.clone());
922        let edit = EditModule::new(store);
923
924        let analysis = AnalysisModule::new(graph, cfg, edit, search);
925        let delete = DeleteOperation {
926            symbol_name: "nonexistent".to_string(),
927        };
928
929        let result = delete.verify(&analysis).await.unwrap();
930        assert!(matches!(result, ApplyResult::Failed(_)));
931    }
932
933    #[tokio::test]
934    async fn test_delete_operation_preview() {
935        let temp_dir = tempfile::tempdir().unwrap();
936        let store = std::sync::Arc::new(
937            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
938                .await.unwrap()
939        );
940        let graph = GraphModule::new(store.clone());
941        let search = SearchModule::new(store.clone());
942        let cfg = CfgModule::new(store.clone());
943        let edit = EditModule::new(store);
944
945        let analysis = AnalysisModule::new(graph, cfg, edit, search);
946        let delete = DeleteOperation {
947            symbol_name: "test_func".to_string(),
948        };
949
950        let diff = delete.preview(&analysis).await.unwrap();
951        assert!(diff.new.contains("deleted"));
952    }
953
954    #[tokio::test]
955    async fn test_rename_operation_verify_not_found() {
956        let temp_dir = tempfile::tempdir().unwrap();
957        let store = std::sync::Arc::new(
958            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
959                .await.unwrap()
960        );
961        let graph = GraphModule::new(store.clone());
962        let search = SearchModule::new(store.clone());
963        let cfg = CfgModule::new(store.clone());
964        let edit = EditModule::new(store);
965
966        let analysis = AnalysisModule::new(graph, cfg, edit, search);
967        let rename = RenameOperation::new("old_name", "new_name");
968
969        let result = rename.verify(&analysis).await.unwrap();
970        assert!(matches!(result, ApplyResult::Failed(_)));
971    }
972
973    #[tokio::test]
974    async fn test_rename_operation_validate_empty_name() {
975        let rename = RenameOperation::new("old", "");
976        let result = rename.validate_name();
977        assert!(result.is_err());
978    }
979
980    #[tokio::test]
981    async fn test_rename_operation_validate_invalid_name() {
982        let rename = RenameOperation::new("old", "123invalid");
983        let result = rename.validate_name();
984        assert!(result.is_err());
985    }
986
987    #[tokio::test]
988    async fn test_error_result_always_fails() {
989        let temp_dir = tempfile::tempdir().unwrap();
990        let store = std::sync::Arc::new(
991            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
992                .await.unwrap()
993        );
994        let graph = GraphModule::new(store.clone());
995        let search = SearchModule::new(store.clone());
996        let cfg = CfgModule::new(store.clone());
997        let edit = EditModule::new(store);
998
999        let mut analysis = AnalysisModule::new(graph, cfg, edit, search);
1000        let error = ErrorResult::new("Test error");
1001
1002        let result = error.verify(&analysis).await.unwrap();
1003        assert_eq!(result, ApplyResult::AlwaysError);
1004
1005        let apply_result = error.apply(&mut analysis).await.unwrap();
1006        assert!(matches!(apply_result, ApplyResult::Failed(_)));
1007    }
1008
1009    #[test]
1010    fn test_diff_creation() {
1011        let diff = Diff::new(
1012            "original content".to_string(),
1013            "new content".to_string(),
1014        );
1015        assert_eq!(diff.original, "original content");
1016        assert_eq!(diff.new, "new content");
1017    }
1018
1019    #[test]
1020    fn test_diff_has_changes() {
1021        let diff = Diff::new("a".to_string(), "b".to_string());
1022        assert!(diff.has_changes());
1023    }
1024
1025    #[test]
1026    fn test_diff_no_changes() {
1027        let diff = Diff::new("same".to_string(), "same".to_string());
1028        assert!(!diff.has_changes());
1029    }
1030
1031    #[test]
1032    fn test_apply_result_variants() {
1033        assert!(matches!(ApplyResult::Applied, ApplyResult::Applied));
1034        assert!(matches!(ApplyResult::AlwaysError, ApplyResult::AlwaysError));
1035        assert!(matches!(ApplyResult::Pending, ApplyResult::Pending));
1036        assert!(matches!(ApplyResult::Failed("x".to_string()), ApplyResult::Failed(_)));
1037    }
1038
1039    // End-to-end integration tests
1040
1041    #[tokio::test]
1042    async fn test_full_workflow_from_lookup_to_edit() {
1043        let temp_dir = tempfile::tempdir().unwrap();
1044        let store = std::sync::Arc::new(
1045            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
1046                .await.unwrap()
1047        );
1048        let graph = GraphModule::new(store.clone());
1049        let search = SearchModule::new(store.clone());
1050        let cfg = CfgModule::new(store.clone());
1051        let edit = EditModule::new(store);
1052
1053        let mut analysis = AnalysisModule::new(graph, cfg, edit, search);
1054
1055        // 1. Look up a symbol
1056        let symbols = analysis.graph().find_symbol("test").await.unwrap();
1057        assert!(symbols.is_empty());
1058
1059        // 2. Check impact
1060        let impact = analysis.impact_analysis("test").await.unwrap();
1061        assert_eq!(impact.symbol, "test");
1062        assert_eq!(impact.impact_score, 0);
1063
1064        // 3. Try to apply a rename operation
1065        let rename = RenameOperation::new("test", "new_name");
1066        let result = rename.verify(&analysis).await.unwrap();
1067        assert!(matches!(result, ApplyResult::Failed(_)));
1068    }
1069
1070    #[tokio::test]
1071    async fn test_cross_module_error_handling() {
1072        let temp_dir = tempfile::tempdir().unwrap();
1073        let store = std::sync::Arc::new(
1074            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
1075                .await.unwrap()
1076        );
1077        let graph = GraphModule::new(store.clone());
1078        let search = SearchModule::new(store.clone());
1079        let cfg = CfgModule::new(store.clone());
1080        let edit = EditModule::new(store);
1081
1082        let analysis = AnalysisModule::new(graph, cfg, edit, search);
1083
1084        // Test that errors from graph module propagate correctly
1085        let result = analysis.graph().find_symbol("").await;
1086        assert!(result.is_ok()); // Empty query returns empty, not error
1087
1088        // Test that search module handles valid patterns
1089        let search_result = analysis.search().pattern_search("test").await;
1090        assert!(search_result.is_ok());
1091
1092        // Test semantic search
1093        let semantic_result = analysis.search().semantic_search("test").await;
1094        assert!(semantic_result.is_ok());
1095    }
1096
1097    #[tokio::test]
1098    async fn test_deep_impact_analysis_integration() {
1099        let temp_dir = tempfile::tempdir().unwrap();
1100        let store = std::sync::Arc::new(
1101            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
1102                .await.unwrap()
1103        );
1104        let graph = GraphModule::new(store.clone());
1105        let search = SearchModule::new(store.clone());
1106        let cfg = CfgModule::new(store.clone());
1107        let edit = EditModule::new(store);
1108
1109        let analysis = AnalysisModule::new(graph, cfg, edit, search);
1110
1111        // Test k-hop impact analysis
1112        let impacted = analysis.deep_impact_analysis("test", 2).await.unwrap();
1113        assert!(impacted.is_empty()); // No symbols in empty database
1114    }
1115
1116    #[tokio::test]
1117    async fn test_module_dependencies_integration() {
1118        let temp_dir = tempfile::tempdir().unwrap();
1119        let store = std::sync::Arc::new(
1120            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
1121                .await.unwrap()
1122        );
1123        let graph = GraphModule::new(store.clone());
1124        let search = SearchModule::new(store.clone());
1125        let cfg = CfgModule::new(store.clone());
1126        let edit = EditModule::new(store);
1127
1128        let analysis = AnalysisModule::new(graph, cfg, edit, search);
1129
1130        // Test module dependency analysis
1131        let deps = analysis.module_dependencies().await.unwrap();
1132        assert!(deps.is_empty()); // No dependencies in empty database
1133
1134        // Test circular dependency detection
1135        let cycles = analysis.find_dependency_cycles().await.unwrap();
1136        assert!(cycles.is_empty());
1137    }
1138
1139    #[tokio::test]
1140    async fn test_complexity_metrics_integration() {
1141        let temp_dir = tempfile::tempdir().unwrap();
1142        let store = std::sync::Arc::new(
1143            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
1144                .await.unwrap()
1145        );
1146        let graph = GraphModule::new(store.clone());
1147        let search = SearchModule::new(store.clone());
1148        let cfg = CfgModule::new(store.clone());
1149        let edit = EditModule::new(store);
1150
1151        let analysis = AnalysisModule::new(graph, cfg, edit, search);
1152
1153        // Test complexity metrics
1154        let metrics = analysis.complexity_metrics("test_function").await.unwrap();
1155        assert_eq!(metrics.cyclomatic_complexity, 1);
1156        assert_eq!(metrics.decision_points, 0);
1157
1158        // Test source complexity analysis
1159        let source = r#"
1160            fn example(x: i32) -> i32 {
1161                if x > 0 {
1162                    return x * 2;
1163                }
1164                x
1165            }
1166        "#;
1167        let source_metrics = analysis.analyze_source_complexity(source);
1168        assert!(source_metrics.cyclomatic_complexity >= 1);
1169    }
1170
1171    #[tokio::test]
1172    async fn test_cross_references_integration() {
1173        let temp_dir = tempfile::tempdir().unwrap();
1174        let store = std::sync::Arc::new(
1175            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
1176                .await.unwrap()
1177        );
1178        let graph = GraphModule::new(store.clone());
1179        let search = SearchModule::new(store.clone());
1180        let cfg = CfgModule::new(store.clone());
1181        let edit = EditModule::new(store);
1182
1183        let analysis = AnalysisModule::new(graph, cfg, edit, search);
1184
1185        // Test cross-references
1186        let xrefs = analysis.cross_references("test").await.unwrap();
1187        assert!(xrefs.callers.is_empty());
1188        assert!(xrefs.callees.is_empty());
1189    }
1190
1191    #[tokio::test]
1192    async fn test_performance_benchmarks_integration() {
1193        let temp_dir = tempfile::tempdir().unwrap();
1194        let store = std::sync::Arc::new(
1195            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
1196                .await.unwrap()
1197        );
1198        let graph = GraphModule::new(store.clone());
1199        let search = SearchModule::new(store.clone());
1200        let cfg = CfgModule::new(store.clone());
1201        let edit = EditModule::new(store);
1202
1203        let analysis = AnalysisModule::new(graph, cfg, edit, search);
1204
1205        // Test performance benchmarks
1206        let benchmarks = analysis.benchmarks().await.unwrap();
1207
1208        // Verify all benchmarks completed
1209        assert!(benchmarks.impact_analysis_ms >= 0.0);
1210        assert!(benchmarks.dead_code_ms >= 0.0);
1211        assert!(benchmarks.reference_chain_ms >= 0.0);
1212        assert!(benchmarks.call_chain_ms >= 0.0);
1213        assert!(benchmarks.total_ms >= 0.0);
1214
1215        // Total should be sum of components (approximately)
1216        let sum = benchmarks.impact_analysis_ms + benchmarks.dead_code_ms
1217            + benchmarks.reference_chain_ms + benchmarks.call_chain_ms;
1218        assert!(benchmarks.total_ms >= sum * 0.9); // Allow for timing overhead
1219    }
1220}