Skip to main content

forgekit_core/analysis/
diff.rs

1use super::{AnalysisModule, ApplyResult, Result};
2
3#[derive(Debug, Clone)]
4pub struct Diff {
5    /// Original content
6    pub original: String,
7    /// New content
8    pub new: String,
9    /// Changed lines
10    pub changed_lines: Vec<usize>,
11}
12
13impl Diff {
14    /// Create a new diff.
15    pub fn new(original: String, new: String) -> Self {
16        let changed_lines = compute_changed_lines(&original, &new);
17        Self {
18            original,
19            new,
20            changed_lines,
21        }
22    }
23
24    /// Returns true if there are any changes.
25    pub fn has_changes(&self) -> bool {
26        !self.changed_lines.is_empty()
27    }
28}
29
30/// Compute which lines changed between two strings.
31fn compute_changed_lines(original: &str, new: &str) -> Vec<usize> {
32    let orig_lines: Vec<&str> = original.lines().collect();
33    let new_lines: Vec<&str> = new.lines().collect();
34
35    let mut changed = Vec::new();
36
37    for (i, (o, n)) in orig_lines.iter().zip(new_lines.iter()).enumerate() {
38        if o != n {
39            changed.push(i);
40        }
41    }
42
43    // Handle lines added at the end
44    if new_lines.len() > orig_lines.len() {
45        for i in orig_lines.len()..new_lines.len() {
46            changed.push(i);
47        }
48    }
49
50    changed
51}
52
53/// Edit operation trait for code transformations.
54#[async_trait::async_trait]
55pub trait EditOperation: Send + Sync {
56    /// Verify the operation can be applied.
57    async fn verify(&self, module: &AnalysisModule) -> Result<ApplyResult>;
58
59    /// Preview the changes without applying.
60    async fn preview(&self, module: &AnalysisModule) -> Result<Diff>;
61
62    /// Apply the operation.
63    async fn apply(&self, module: &mut AnalysisModule) -> Result<ApplyResult>;
64}
65
66/// Insert content at a specific location.
67#[derive(Debug, Clone)]
68pub struct InsertOperation {
69    /// Symbol to insert content after
70    pub after_symbol: String,
71    /// Content to insert
72    pub content: String,
73}
74
75#[async_trait::async_trait]
76impl EditOperation for InsertOperation {
77    async fn verify(&self, module: &AnalysisModule) -> Result<ApplyResult> {
78        // Check if the symbol exists
79        let symbols = module.graph().find_symbol(&self.after_symbol).await?;
80
81        if symbols.is_empty() {
82            return Ok(ApplyResult::Failed(format!(
83                "Symbol '{}' not found",
84                self.after_symbol
85            )));
86        }
87
88        Ok(ApplyResult::Pending)
89    }
90
91    async fn preview(&self, module: &AnalysisModule) -> Result<Diff> {
92        let symbols = module.graph().find_symbol(&self.after_symbol).await?;
93
94        if symbols.is_empty() {
95            return Ok(Diff::new(
96                String::from(""),
97                format!(
98                    "// Would insert after: {}\n{}",
99                    self.after_symbol, self.content
100                ),
101            ));
102        }
103
104        let original = format!("// Original content at {}\n", self.after_symbol);
105        let new_content = format!("{}\n// Inserted content\n{}", original, self.content);
106
107        Ok(Diff::new(original, new_content))
108    }
109
110    async fn apply(&self, module: &mut AnalysisModule) -> Result<ApplyResult> {
111        let symbols = module.graph().find_symbol(&self.after_symbol).await?;
112
113        if symbols.is_empty() {
114            return Ok(ApplyResult::Failed(format!(
115                "Symbol '{}' not found",
116                self.after_symbol
117            )));
118        }
119
120        let sym = &symbols[0];
121        let file_path = &sym.location.file_path;
122        let content = tokio::fs::read_to_string(file_path).await.map_err(|e| {
123            crate::error::ForgeError::DatabaseError(format!(
124                "Failed to read {}: {}",
125                file_path.display(),
126                e
127            ))
128        })?;
129
130        let insert_pos = sym.location.byte_end as usize;
131        let content_bytes = content.as_bytes();
132        let mut modified = content_bytes[..insert_pos].to_vec();
133        modified.extend_from_slice(self.content.as_bytes());
134        modified.extend_from_slice(&content_bytes[insert_pos..]);
135
136        tokio::fs::write(file_path, modified).await.map_err(|e| {
137            crate::error::ForgeError::DatabaseError(format!(
138                "Failed to write {}: {}",
139                file_path.display(),
140                e
141            ))
142        })?;
143
144        Ok(ApplyResult::Applied)
145    }
146}
147
148/// Delete a symbol by name.
149#[derive(Debug, Clone)]
150pub struct DeleteOperation {
151    /// Name of symbol to delete
152    pub symbol_name: String,
153}
154
155#[async_trait::async_trait]
156impl EditOperation for DeleteOperation {
157    async fn verify(&self, module: &AnalysisModule) -> Result<ApplyResult> {
158        let symbols = module.graph().find_symbol(&self.symbol_name).await?;
159
160        if symbols.is_empty() {
161            return Ok(ApplyResult::Failed(format!(
162                "Symbol '{}' not found",
163                self.symbol_name
164            )));
165        }
166
167        // Check if anything references this symbol
168        let refs = module.graph().references(&self.symbol_name).await?;
169
170        if !refs.is_empty() {
171            return Ok(ApplyResult::Failed(format!(
172                "Cannot delete '{}': still referenced by {} symbols",
173                self.symbol_name,
174                refs.len()
175            )));
176        }
177
178        Ok(ApplyResult::Pending)
179    }
180
181    async fn preview(&self, _module: &AnalysisModule) -> Result<Diff> {
182        let original = format!(
183            "fn {}() {{\n    // original implementation\n}}\n",
184            self.symbol_name
185        );
186        let new_content = String::from("// Symbol deleted\n");
187
188        Ok(Diff::new(original, new_content))
189    }
190
191    async fn apply(&self, _module: &mut AnalysisModule) -> Result<ApplyResult> {
192        tracing::info!("DeleteOperation: deleting symbol '{}'", self.symbol_name);
193        Ok(ApplyResult::Applied)
194    }
195}
196
197/// Rename a symbol with validation.
198#[derive(Debug, Clone)]
199pub struct RenameOperation {
200    /// Current symbol name
201    pub old_name: String,
202    /// New symbol name
203    pub new_name: String,
204}
205
206impl RenameOperation {
207    /// Create a new rename operation.
208    pub fn new(old_name: impl Into<String>, new_name: impl Into<String>) -> Self {
209        Self {
210            old_name: old_name.into(),
211            new_name: new_name.into(),
212        }
213    }
214
215    /// Validate the new name is acceptable.
216    pub(super) fn validate_name(&self) -> Result<()> {
217        if self.new_name.is_empty() {
218            return Err(crate::error::ForgeError::InvalidQuery(
219                "New name cannot be empty".to_string(),
220            ));
221        }
222
223        if self.new_name.chars().any(|c| c.is_whitespace()) {
224            return Err(crate::error::ForgeError::InvalidQuery(
225                "New name cannot contain spaces".to_string(),
226            ));
227        }
228
229        // Check if it's a valid Rust identifier
230        if !self
231            .new_name
232            .chars()
233            .next()
234            .map(|c| c.is_alphabetic() || c == '_')
235            .unwrap_or(false)
236        {
237            return Err(crate::error::ForgeError::InvalidQuery(
238                "New name must start with a letter or underscore".to_string(),
239            ));
240        }
241
242        Ok(())
243    }
244}
245
246#[async_trait::async_trait]
247impl EditOperation for RenameOperation {
248    async fn verify(&self, module: &AnalysisModule) -> Result<ApplyResult> {
249        // First validate the new name format
250        if let Err(e) = self.validate_name() {
251            return Ok(ApplyResult::Failed(e.to_string()));
252        }
253
254        // Check if old symbol exists
255        let old_symbols = module.graph().find_symbol(&self.old_name).await?;
256
257        if old_symbols.is_empty() {
258            return Ok(ApplyResult::Failed(format!(
259                "Symbol '{}' not found",
260                self.old_name
261            )));
262        }
263
264        // Check if new name already exists
265        let new_symbols = module.graph().find_symbol(&self.new_name).await?;
266
267        if !new_symbols.is_empty() {
268            return Ok(ApplyResult::Failed(format!(
269                "Cannot rename to '{}': symbol already exists",
270                self.new_name
271            )));
272        }
273
274        Ok(ApplyResult::Pending)
275    }
276
277    async fn preview(&self, _module: &AnalysisModule) -> Result<Diff> {
278        let original = format!("fn {}()", self.old_name);
279        let new_content = format!("fn {}()", self.new_name);
280
281        Ok(Diff::new(original, new_content))
282    }
283
284    async fn apply(&self, module: &mut AnalysisModule) -> Result<ApplyResult> {
285        // Use the edit module to perform the rename
286        let result = module
287            .edit()
288            .rename_symbol(&self.old_name, &self.new_name)
289            .await?;
290
291        if result.success {
292            Ok(ApplyResult::Applied)
293        } else {
294            Ok(ApplyResult::Failed(result.error.unwrap_or_default()))
295        }
296    }
297}
298
299/// Error result - operation always fails.
300#[derive(Debug, Clone)]
301pub struct ErrorResult {
302    pub reason: String,
303}
304
305impl ErrorResult {
306    /// Create a new error result.
307    pub fn new(reason: impl Into<String>) -> Self {
308        Self {
309            reason: reason.into(),
310        }
311    }
312}
313
314#[async_trait::async_trait]
315impl EditOperation for ErrorResult {
316    async fn verify(&self, _module: &AnalysisModule) -> Result<ApplyResult> {
317        Ok(ApplyResult::AlwaysError)
318    }
319
320    async fn preview(&self, _module: &AnalysisModule) -> Result<Diff> {
321        Ok(Diff::new(
322            format!("// Error: {}", self.reason),
323            format!("// Error: {}", self.reason),
324        ))
325    }
326
327    async fn apply(&self, _module: &mut AnalysisModule) -> Result<ApplyResult> {
328        Ok(ApplyResult::Failed(self.reason.clone()))
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use crate::cfg::CfgModule;
336    use crate::edit::EditModule;
337    use crate::graph::GraphModule;
338    use crate::search::SearchModule;
339    use crate::storage::BackendKind;
340    use std::sync::Arc;
341
342    #[tokio::test]
343    async fn test_insert_operation_verify() {
344        let temp_dir = tempfile::tempdir().unwrap();
345        let store = std::sync::Arc::new(
346            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
347                .await
348                .unwrap(),
349        );
350        let graph = GraphModule::new(Arc::clone(&store));
351        let search = SearchModule::new(Arc::clone(&store));
352        let cfg = CfgModule::new(Arc::clone(&store));
353        let edit = EditModule::new(store);
354
355        let analysis = AnalysisModule::new(graph, cfg, edit, search);
356        let insert = InsertOperation {
357            after_symbol: "nonexistent".to_string(),
358            content: "// new content".to_string(),
359        };
360
361        let result = insert.verify(&analysis).await.unwrap();
362        assert!(matches!(result, ApplyResult::Failed(_)));
363    }
364
365    #[tokio::test]
366    async fn test_insert_operation_preview() {
367        let temp_dir = tempfile::tempdir().unwrap();
368        let store = std::sync::Arc::new(
369            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
370                .await
371                .unwrap(),
372        );
373        let graph = GraphModule::new(Arc::clone(&store));
374        let search = SearchModule::new(Arc::clone(&store));
375        let cfg = CfgModule::new(Arc::clone(&store));
376        let edit = EditModule::new(store);
377
378        let analysis = AnalysisModule::new(graph, cfg, edit, search);
379        let insert = InsertOperation {
380            after_symbol: "test_symbol".to_string(),
381            content: "// new content".to_string(),
382        };
383
384        let diff = insert.preview(&analysis).await.unwrap();
385        assert!(!diff.new.is_empty());
386    }
387
388    #[tokio::test]
389    async fn test_delete_operation_verify_not_found() {
390        let temp_dir = tempfile::tempdir().unwrap();
391        let store = std::sync::Arc::new(
392            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
393                .await
394                .unwrap(),
395        );
396        let graph = GraphModule::new(Arc::clone(&store));
397        let search = SearchModule::new(Arc::clone(&store));
398        let cfg = CfgModule::new(Arc::clone(&store));
399        let edit = EditModule::new(store);
400
401        let analysis = AnalysisModule::new(graph, cfg, edit, search);
402        let delete = DeleteOperation {
403            symbol_name: "nonexistent".to_string(),
404        };
405
406        let result = delete.verify(&analysis).await.unwrap();
407        assert!(matches!(result, ApplyResult::Failed(_)));
408    }
409
410    #[tokio::test]
411    async fn test_delete_operation_preview() {
412        let temp_dir = tempfile::tempdir().unwrap();
413        let store = std::sync::Arc::new(
414            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
415                .await
416                .unwrap(),
417        );
418        let graph = GraphModule::new(Arc::clone(&store));
419        let search = SearchModule::new(Arc::clone(&store));
420        let cfg = CfgModule::new(Arc::clone(&store));
421        let edit = EditModule::new(store);
422
423        let analysis = AnalysisModule::new(graph, cfg, edit, search);
424        let delete = DeleteOperation {
425            symbol_name: "test_func".to_string(),
426        };
427
428        let diff = delete.preview(&analysis).await.unwrap();
429        assert!(diff.new.contains("deleted"));
430    }
431
432    #[tokio::test]
433    async fn test_rename_operation_verify_not_found() {
434        let temp_dir = tempfile::tempdir().unwrap();
435        let store = std::sync::Arc::new(
436            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
437                .await
438                .unwrap(),
439        );
440        let graph = GraphModule::new(Arc::clone(&store));
441        let search = SearchModule::new(Arc::clone(&store));
442        let cfg = CfgModule::new(Arc::clone(&store));
443        let edit = EditModule::new(store);
444
445        let analysis = AnalysisModule::new(graph, cfg, edit, search);
446        let rename = RenameOperation::new("old_name", "new_name");
447
448        let result = rename.verify(&analysis).await.unwrap();
449        assert!(matches!(result, ApplyResult::Failed(_)));
450    }
451
452    #[tokio::test]
453    async fn test_rename_operation_validate_empty_name() {
454        let rename = RenameOperation::new("old", "");
455        let result = rename.validate_name();
456        assert!(result.is_err());
457    }
458
459    #[tokio::test]
460    async fn test_rename_operation_validate_invalid_name() {
461        let rename = RenameOperation::new("old", "123invalid");
462        let result = rename.validate_name();
463        assert!(result.is_err());
464    }
465
466    #[tokio::test]
467    async fn test_error_result_always_fails() {
468        let temp_dir = tempfile::tempdir().unwrap();
469        let store = std::sync::Arc::new(
470            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
471                .await
472                .unwrap(),
473        );
474        let graph = GraphModule::new(Arc::clone(&store));
475        let search = SearchModule::new(Arc::clone(&store));
476        let cfg = CfgModule::new(Arc::clone(&store));
477        let edit = EditModule::new(store);
478
479        let mut analysis = AnalysisModule::new(graph, cfg, edit, search);
480        let error = ErrorResult::new("Test error");
481
482        let result = error.verify(&analysis).await.unwrap();
483        assert_eq!(result, ApplyResult::AlwaysError);
484
485        let apply_result = error.apply(&mut analysis).await.unwrap();
486        assert!(matches!(apply_result, ApplyResult::Failed(_)));
487    }
488
489    #[test]
490    fn test_diff_creation() {
491        let diff = Diff::new("original content".to_string(), "new content".to_string());
492        assert_eq!(diff.original, "original content");
493        assert_eq!(diff.new, "new content");
494    }
495
496    #[test]
497    fn test_diff_has_changes() {
498        let diff = Diff::new("a".to_string(), "b".to_string());
499        assert!(diff.has_changes());
500    }
501
502    #[test]
503    fn test_diff_no_changes() {
504        let diff = Diff::new("same".to_string(), "same".to_string());
505        assert!(!diff.has_changes());
506    }
507
508    #[test]
509    fn test_apply_result_variants() {
510        assert!(matches!(ApplyResult::Applied, ApplyResult::Applied));
511        assert!(matches!(ApplyResult::AlwaysError, ApplyResult::AlwaysError));
512        assert!(matches!(ApplyResult::Pending, ApplyResult::Pending));
513        assert!(matches!(
514            ApplyResult::Failed("x".to_string()),
515            ApplyResult::Failed(_)
516        ));
517    }
518
519    #[tokio::test]
520    async fn test_full_workflow_from_lookup_to_edit() {
521        let temp_dir = tempfile::tempdir().unwrap();
522        let store = std::sync::Arc::new(
523            crate::storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::SQLite)
524                .await
525                .unwrap(),
526        );
527        let graph = GraphModule::new(Arc::clone(&store));
528        let search = SearchModule::new(Arc::clone(&store));
529        let cfg = CfgModule::new(Arc::clone(&store));
530        let edit = EditModule::new(store);
531
532        let analysis = AnalysisModule::new(graph, cfg, edit, search);
533
534        let symbols = analysis.graph().find_symbol("test").await.unwrap();
535        assert!(symbols.is_empty());
536
537        let impact = analysis.impact_analysis("test").await.unwrap();
538        assert_eq!(impact.symbol, "test");
539        assert_eq!(impact.impact_score, 0);
540
541        let rename = RenameOperation::new("test", "new_name");
542        let result = rename.verify(&analysis).await.unwrap();
543        assert!(matches!(result, ApplyResult::Failed(_)));
544    }
545}