Skip to main content

ryo_executor/engine/impls/
item.rs

1//! ASTRegApply implementations for AddItemMutation, AddPureItemsMutation, RemoveItemMutation, and MoveItemMutation
2
3use ryo_analysis::AnalysisContext;
4use ryo_mutations::{
5    AddItemMutation, AddPureItemsMutation, MoveItemMutation, MutationResult, RemoveItemMutation,
6};
7use ryo_source::pure::{PureFile, PureItem, PureUse, PureUseTree, PureVis};
8use ryo_source::ItemKind;
9use ryo_symbol::{SymbolKind, SymbolPath, Visibility};
10
11use crate::engine::{ASTMutationContext, ASTRegApply, ExecutionResult};
12
13/// Add an item from raw source code content
14pub fn add_item_v2(
15    ctx: &mut AnalysisContext,
16    target: &SymbolPath,
17    content: &str,
18) -> ExecutionResult {
19    let mut mutation_ctx = ASTMutationContext::new(&mut ctx.ast_registry, &mut ctx.registry);
20
21    let result = add_item_impl(&mut mutation_ctx, target, content);
22    let events = mutation_ctx.into_events();
23
24    ExecutionResult::new(result, events)
25}
26
27fn add_item_impl(
28    ctx: &mut ASTMutationContext,
29    target: &SymbolPath,
30    content: &str,
31) -> MutationResult {
32    // Parse the content to get PureItem(s)
33    let parsed = match PureFile::from_source(content.trim()) {
34        Ok(file) => file,
35        Err(e) => {
36            return MutationResult {
37                mutation_type: "AddItem".to_string(),
38                changes: 0,
39                description: format!("Failed to parse content: {}", e),
40            };
41        }
42    };
43
44    let items = parsed.items;
45    if items.is_empty() {
46        return MutationResult {
47            mutation_type: "AddItem".to_string(),
48            changes: 0,
49            description: "No items found in content".to_string(),
50        };
51    }
52
53    add_pure_items_impl(ctx, target, items, "AddItem")
54}
55
56/// Add pre-built PureItems to a module (shared logic for AddItem and AddPureItems)
57fn add_pure_items_impl(
58    ctx: &mut ASTMutationContext,
59    target: &SymbolPath,
60    items: Vec<PureItem>,
61    mutation_type: &'static str,
62) -> MutationResult {
63    let mut added = 0;
64    let mut descriptions = Vec::new();
65    let mut skipped = Vec::new();
66
67    for item in items {
68        // For Mod items, we need to preserve visibility for SymbolRegistry
69        let mod_visibility = if let PureItem::Mod(m) = &item {
70            Some(pure_vis_to_visibility(&m.vis))
71        } else {
72            None
73        };
74
75        let (name, kind) = match &item {
76            PureItem::Fn(f) => (f.name.clone(), SymbolKind::Function),
77            PureItem::Struct(s) => (s.name.clone(), SymbolKind::Struct),
78            PureItem::Enum(e) => (e.name.clone(), SymbolKind::Enum),
79            PureItem::Const(c) => (c.name.clone(), SymbolKind::Const),
80            PureItem::Static(s) => (s.name.clone(), SymbolKind::Static),
81            PureItem::Type(t) => (t.name.clone(), SymbolKind::TypeAlias),
82            PureItem::Trait(t) => (t.name.clone(), SymbolKind::Trait),
83            PureItem::Mod(m) => (m.name.clone(), SymbolKind::Mod),
84            PureItem::Impl(i) => {
85                // Use common impl block registration logic
86                match super::utils::register_impl_block(ctx, target, i) {
87                    Ok(result) => {
88                        // Count methods added (impl block itself may be merged)
89                        added += result.methods_added;
90                        descriptions.push(result.description);
91                    }
92                    Err(e) => {
93                        skipped.push(format!("Impl block for '{}': {}", i.self_ty, e));
94                    }
95                }
96                continue;
97            }
98            PureItem::Use(u) => {
99                // Use statements are added to module_items without symbol registration
100                // They don't have a "name" in the symbol registry sense
101
102                // Find the target module ID
103                let target_str = target.to_string();
104                if let Some(module_id) = ctx.symbol_registry.lookup(target) {
105                    // Get current module items and add the use statement at the beginning
106                    let mut items = ctx
107                        .ast_registry
108                        .get_module_items(module_id)
109                        .cloned()
110                        .unwrap_or_default();
111
112                    // Insert use statement at the beginning (after any existing use statements)
113                    let insert_pos = items
114                        .iter()
115                        .position(|i| !matches!(i, PureItem::Use(_)))
116                        .unwrap_or(items.len());
117                    items.insert(insert_pos, PureItem::Use(u.clone()));
118
119                    ctx.ast_registry.set_module_items(module_id, items);
120
121                    // Emit event so sync_files_and_rebuild regenerates this module's file
122                    ctx.emit_modified(
123                        module_id,
124                        crate::engine::events::ModificationType::Other(
125                            "use statement added".to_string(),
126                        ),
127                    );
128
129                    added += 1;
130                    descriptions.push(format!("Added use statement to '{}'", target_str));
131                }
132                continue;
133            }
134            PureItem::Macro(_) | PureItem::Other(_) | PureItem::Verbatim(_) => {
135                // Skip these for now
136                continue;
137            }
138        };
139
140        // Build the full path
141        let target_str = target.to_string();
142        let full_path = if target_str == "crate" {
143            SymbolPath::parse(&format!("crate::{}", name))
144        } else {
145            SymbolPath::parse(&format!("{}::{}", target_str, name))
146        };
147
148        let path = match full_path {
149            Ok(p) => p,
150            Err(e) => {
151                skipped.push(format!(
152                    "{} '{}': invalid path ({})",
153                    kind.display_name(),
154                    name,
155                    e
156                ));
157                continue;
158            }
159        };
160
161        // Register the new symbol with its AST
162        match ctx.register_with_ast(path.clone(), kind, item.clone()) {
163            Some(id) => {
164                // For Mod items, set visibility in SymbolRegistry
165                // This is needed for RegistryGenerator to determine mod visibility
166                if let Some(vis) = mod_visibility {
167                    let _ = ctx.symbol_registry.set_visibility(id, vis);
168                }
169
170                // Mark inline modules: modules with non-empty items should be marked
171                // as inline so RegistryGenerator keeps them in the parent file
172                // instead of creating separate files for them.
173                // This is important for operations like DuplicateModTree.
174                if let PureItem::Mod(m) = &item {
175                    if !m.items.is_empty() {
176                        ctx.ast_registry.mark_inline_module(id);
177                    }
178                }
179
180                // Also add to parent module's module_items for inline module preservation
181                // This ensures RegistryGenerator sees the item in the parent's PureMod.items
182                if let Some(parent_id) = ctx.symbol_registry.lookup(target) {
183                    let mut items = ctx
184                        .ast_registry
185                        .get_module_items(parent_id)
186                        .cloned()
187                        .unwrap_or_default();
188                    items.push(item);
189                    ctx.ast_registry.set_module_items(parent_id, items);
190                }
191
192                added += 1;
193                descriptions.push(format!("Added {} '{}'", kind.display_name(), name));
194            }
195            None => {
196                skipped.push(format!(
197                    "{} '{}': registration failed (symbol may already exist with different kind)",
198                    kind.display_name(),
199                    name
200                ));
201            }
202        }
203    }
204
205    // Build description with both successes and failures
206    let description = match (descriptions.is_empty(), skipped.is_empty()) {
207        (true, true) => "No items added".to_string(),
208        (true, false) => format!("No items added. Skipped: {}", skipped.join("; ")),
209        (false, true) => descriptions.join(", "),
210        (false, false) => format!(
211            "{}. Skipped: {}",
212            descriptions.join(", "),
213            skipped.join("; ")
214        ),
215    };
216
217    MutationResult {
218        mutation_type: mutation_type.to_string(),
219        changes: added,
220        description,
221    }
222}
223
224/// Remove an item by SymbolId
225pub fn remove_item_v2(
226    ctx: &mut AnalysisContext,
227    symbol_id: ryo_symbol::SymbolId,
228    item_kind: &crate::ItemKind,
229) -> ExecutionResult {
230    let mut mutation_ctx = ASTMutationContext::new(&mut ctx.ast_registry, &mut ctx.registry);
231
232    let result = remove_item_impl(&mut mutation_ctx, symbol_id, item_kind);
233    let events = mutation_ctx.into_events();
234
235    ExecutionResult::new(result, events)
236}
237
238fn remove_item_impl(
239    ctx: &mut ASTMutationContext,
240    symbol_id: ryo_symbol::SymbolId,
241    item_kind: &crate::ItemKind,
242) -> MutationResult {
243    // Remove the symbol directly via O(1) lookup
244    ctx.remove_symbol(symbol_id);
245
246    MutationResult {
247        mutation_type: "RemoveItem".to_string(),
248        changes: 1,
249        description: format!("Removed {:?} ({:?})", item_kind, symbol_id),
250    }
251}
252
253// ============================================================================
254// ASTRegApply implementations
255// ============================================================================
256
257impl ASTRegApply for AddItemMutation {
258    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
259        // Use the provided symbol_id as the parent module
260        let module_id = self.parent;
261
262        // Verify the target is a module
263        if ctx.symbol_registry.kind(module_id) != Some(SymbolKind::Mod) {
264            return MutationResult {
265                mutation_type: "AddItem".to_string(),
266                changes: 0,
267                description: format!("Target symbol {} is not a module", module_id),
268            };
269        }
270
271        // Get the module path
272        let target = match ctx.symbol_registry.path(module_id) {
273            Some(p) => p.clone(),
274            None => {
275                return MutationResult {
276                    mutation_type: "AddItem".to_string(),
277                    changes: 0,
278                    description: format!("Module {} not found in registry", module_id),
279                };
280            }
281        };
282
283        add_item_impl(ctx, &target, &self.content)
284    }
285}
286
287impl ASTRegApply for AddPureItemsMutation {
288    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
289        // Use the provided symbol_id as the parent module
290        let module_id = self.parent;
291
292        // Verify the target is a module
293        if ctx.symbol_registry.kind(module_id) != Some(SymbolKind::Mod) {
294            return MutationResult {
295                mutation_type: "AddPureItems".to_string(),
296                changes: 0,
297                description: format!("Target symbol {} is not a module", module_id),
298            };
299        }
300
301        // Get the module path
302        let target = match ctx.symbol_registry.path(module_id) {
303            Some(p) => p.clone(),
304            None => {
305                return MutationResult {
306                    mutation_type: "AddPureItems".to_string(),
307                    changes: 0,
308                    description: format!("Module {} not found in registry", module_id),
309                };
310            }
311        };
312
313        add_pure_items_impl(ctx, &target, self.items.clone(), "AddPureItems")
314    }
315}
316
317impl ASTRegApply for RemoveItemMutation {
318    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
319        remove_item_impl(ctx, self.symbol_id, &self.item_kind)
320    }
321}
322
323impl ASTRegApply for ryo_mutations::ReplaceCodeMutation {
324    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
325        use crate::engine::ModificationType;
326        use ryo_source::pure::PureItem;
327
328        // Confirm the symbol exists in the AST registry; otherwise there is
329        // no item to swap out and we report 0 changes.
330        if ctx.ast_registry.get(self.symbol_id).is_none() {
331            return MutationResult {
332                mutation_type: "ReplaceCode".to_string(),
333                changes: 0,
334                description: format!(
335                    "ReplaceCode target {:?} not present in ASTRegistry",
336                    self.symbol_id
337                ),
338            };
339        }
340
341        // Substitute the item with a Verbatim carrying the raw bytes. The
342        // file-rendering path (PureFile::to_source) lays a sentinel stub in
343        // its place and splices the raw bytes back over it after
344        // prettyplease formats the surrounding items.
345        ctx.ast_registry
346            .set(self.symbol_id, PureItem::Verbatim(self.raw.clone()));
347
348        // Emit a body-modified event so the downstream sync_files_and_rebuild
349        // step regenerates the affected file. We use BodyModified because the
350        // item identity (and its location in the parent module's child list)
351        // is unchanged — only its content text differs.
352        ctx.emit_modified(self.symbol_id, ModificationType::BodyModified);
353
354        MutationResult {
355            mutation_type: "ReplaceCode".to_string(),
356            changes: 1,
357            description: format!(
358                "Replaced {:?} with {} verbatim bytes",
359                self.symbol_id,
360                self.raw.len()
361            ),
362        }
363    }
364}
365
366impl ASTRegApply for MoveItemMutation {
367    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
368        move_item_impl(
369            ctx,
370            &self.source,
371            &self.target,
372            &self.item_name,
373            &self.item_kind,
374            self.add_use,
375        )
376    }
377}
378
379/// Move an item from one module to another
380fn move_item_impl(
381    ctx: &mut ASTMutationContext,
382    source: &SymbolPath,
383    target: &SymbolPath,
384    item_name: &str,
385    item_kind: &ItemKind,
386    add_use: bool,
387) -> MutationResult {
388    // Convert ItemKind to SymbolKind for lookup
389    let expected_kind = match item_kind {
390        ItemKind::Struct => Some(SymbolKind::Struct),
391        ItemKind::Enum => Some(SymbolKind::Enum),
392        ItemKind::Function => Some(SymbolKind::Function),
393        ItemKind::Trait => Some(SymbolKind::Trait),
394        ItemKind::Impl => Some(SymbolKind::Impl),
395        ItemKind::TypeAlias => Some(SymbolKind::TypeAlias),
396        ItemKind::Const => Some(SymbolKind::Const),
397        ItemKind::Static => Some(SymbolKind::Static),
398        ItemKind::Mod => Some(SymbolKind::Mod),
399        _ => None,
400    };
401
402    // 1. Find source symbol by iterating registry (to match kind)
403    let source_id = ctx
404        .symbol_registry
405        .iter()
406        .find(|(id, path)| {
407            let path_matches = path.name() == item_name && path.parent() == Some(source.clone());
408            let kind_matches = expected_kind
409                .map(|k| ctx.symbol_registry.kind(*id) == Some(k))
410                .unwrap_or(true);
411            path_matches && kind_matches
412        })
413        .map(|(id, _)| id);
414
415    let source_id = match source_id {
416        Some(id) => id,
417        None => {
418            return MutationResult {
419                mutation_type: "MoveItem".to_string(),
420                changes: 0,
421                description: format!("Item '{}' not found in {}", item_name, source),
422            };
423        }
424    };
425
426    // 2. Get AST and kind before removing
427    let ast = match ctx.get_ast(source_id).cloned() {
428        Some(ast) => ast,
429        None => {
430            return MutationResult {
431                mutation_type: "MoveItem".to_string(),
432                changes: 0,
433                description: format!("No AST found for '{}'", item_name),
434            };
435        }
436    };
437    let kind = ctx.kind(source_id).unwrap_or(SymbolKind::Struct);
438
439    // 3. Remove from old location
440    ctx.remove_symbol(source_id);
441
442    // 4. Build target path
443    let target_path = match target.child(item_name) {
444        Ok(p) => p,
445        Err(_) => {
446            return MutationResult {
447                mutation_type: "MoveItem".to_string(),
448                changes: 0,
449                description: format!("Invalid target path: {}::{}", target, item_name),
450            };
451        }
452    };
453
454    // 5. Register at new location
455    if ctx
456        .register_with_ast(target_path.clone(), kind, ast)
457        .is_none()
458    {
459        return MutationResult {
460            mutation_type: "MoveItem".to_string(),
461            changes: 0,
462            description: format!("Failed to register at new path: {}", target_path),
463        };
464    }
465
466    // 6. Optionally add use statement in source module
467    if add_use {
468        // Build use path: target::item_name
469        let use_path_str = format!("{}::{}", target, item_name);
470
471        // Parse path into PureUseTree
472        // e.g., "crate::core::Task" -> Path("crate", Path("core", Name("Task")))
473        let parts: Vec<&str> = use_path_str.split("::").collect();
474        let tree = parts
475            .iter()
476            .rev()
477            .fold(None, |acc: Option<PureUseTree>, part| {
478                Some(match acc {
479                    None => PureUseTree::Name(part.to_string()),
480                    Some(subtree) => PureUseTree::Path {
481                        path: part.to_string(),
482                        tree: Box::new(subtree),
483                    },
484                })
485            })
486            .unwrap_or(PureUseTree::Name(item_name.to_string()));
487
488        let use_item = PureItem::Use(PureUse {
489            vis: PureVis::Private,
490            tree,
491        });
492
493        // Find source module and add use to its module_items
494        if let Some(source_mod_id) = ctx.lookup(source) {
495            let mut items = ctx
496                .ast_registry
497                .get_module_items(source_mod_id)
498                .cloned()
499                .unwrap_or_default();
500
501            // Insert after existing use statements
502            let insert_pos = items
503                .iter()
504                .position(|i| !matches!(i, PureItem::Use(_)))
505                .unwrap_or(items.len());
506            items.insert(insert_pos, use_item);
507
508            ctx.ast_registry.set_module_items(source_mod_id, items);
509        }
510    }
511
512    MutationResult {
513        mutation_type: "MoveItem".to_string(),
514        changes: 1,
515        description: format!(
516            "Moved {} '{}' from {} to {}",
517            kind.display_name(),
518            item_name,
519            source,
520            target
521        ),
522    }
523}
524
525/// Convert PureVis to ryo_symbol::Visibility
526fn pure_vis_to_visibility(vis: &PureVis) -> Visibility {
527    match vis {
528        PureVis::Public => Visibility::Public,
529        PureVis::Crate => Visibility::Crate,
530        PureVis::Super => Visibility::Super,
531        PureVis::Private => Visibility::Private,
532        PureVis::In(path) => {
533            // Try to parse as SymbolPath, fallback to Private if invalid
534            ryo_symbol::SymbolPath::parse(path)
535                .map(|p| Visibility::Restricted(Box::new(p)))
536                .unwrap_or(Visibility::Private)
537        }
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544    use ryo_analysis::testing::ContextBuilder;
545    use ryo_source::pure::PureItem;
546
547    #[test]
548    fn test_v2_replace_code_substitutes_item_with_verbatim() {
549        // Boundary: ReplaceCode resolves the target SymbolId, swaps the
550        // stored PureItem for a PureItem::Verbatim carrying the raw bytes,
551        // and emits a BodyModified event. The follow-on rendering path
552        // (PureFile::to_source) is exercised separately by
553        // `test_verbatim_item_preserves_raw_bytes` in ryo-source.
554        let mut ctx = ContextBuilder::new()
555            .with_file(
556                "src/lib.rs",
557                r#"
558pub struct Foo {
559    pub x: i32,
560}
561"#,
562            )
563            .build();
564
565        let foo_id = ctx
566            .registry
567            .iter()
568            .find(|(id, path)| {
569                matches!(ctx.registry.kind(*id), Some(SymbolKind::Struct)) && path.name() == "Foo"
570            })
571            .map(|(id, _)| id)
572            .expect("Foo struct must be registered");
573
574        let raw = "// preserved verbatim comment\npub struct Foo {\n    pub x: i32, // inline doc preserved\n}".to_string();
575
576        let mutation = ryo_mutations::ReplaceCodeMutation::new(foo_id, raw.clone());
577        let result = crate::engine::ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
578
579        assert_eq!(
580            result.result.changes, 1,
581            "ReplaceCode should report 1 change"
582        );
583        match ctx.ast_registry.get(foo_id) {
584            Some(PureItem::Verbatim(stored)) => assert_eq!(stored, &raw),
585            other => panic!(
586                "Expected PureItem::Verbatim after ReplaceCode, got: {:?}",
587                other
588            ),
589        }
590    }
591
592    #[test]
593    fn test_v2_replace_code_missing_target_reports_zero() {
594        // Boundary: when the symbol id is unknown (e.g., resolution drift
595        // between Suggest detection and apply), ReplaceCode should report
596        // 0 changes rather than panic or mutate an unrelated item.
597        let mut ctx = ContextBuilder::new()
598            .with_file("src/lib.rs", "pub struct Foo { pub x: i32 }")
599            .build();
600        // Choose a SymbolId that almost certainly does not exist in the
601        // registry by parsing a large index/version pair.
602        let bogus = ryo_symbol::SymbolId::parse("99999v1").expect("parseable SymbolId");
603
604        let mutation = ryo_mutations::ReplaceCodeMutation::new(bogus, "// noop".to_string());
605        let result = crate::engine::ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
606
607        assert_eq!(result.result.changes, 0);
608    }
609}
610
611#[cfg(test)]
612mod legacy_tests {
613    use super::*;
614    use ryo_analysis::{ASTRegistry, SymbolRegistry};
615
616    /// Test: Impl block methods are registered directly on parent type
617    ///
618    /// New design: impl blocks are file-level construct.
619    /// Methods are registered as Struct::method, not <impl Struct>::N::method.
620    #[test]
621    fn test_add_impl_block_registers_methods_on_parent_type() {
622        let mut ast_registry = ASTRegistry::new();
623        let mut symbol_registry = SymbolRegistry::new();
624        let mut ctx = ASTMutationContext::new(&mut ast_registry, &mut symbol_registry);
625
626        // Register the parent struct first
627        let struct_path = SymbolPath::parse("test_crate::TodoList").unwrap();
628        ctx.register(struct_path.clone(), SymbolKind::Struct);
629
630        let target = SymbolPath::parse("test_crate").unwrap();
631
632        // Add impl block with method
633        let impl_code = r#"
634            impl TodoList {
635                pub fn new() -> Self {
636                    Self { items: vec![] }
637                }
638            }
639        "#;
640        let result = add_item_impl(&mut ctx, &target, impl_code);
641        assert_eq!(result.changes, 1);
642
643        // Verify method is registered as TodoList::new (not <impl TodoList>::N::new)
644        let method_path = SymbolPath::parse("test_crate::TodoList::new").unwrap();
645        let method_id = ctx.lookup(&method_path);
646        assert!(
647            method_id.is_some(),
648            "Method should be registered as TodoList::new"
649        );
650        assert_eq!(ctx.kind(method_id.unwrap()), Some(SymbolKind::Method));
651    }
652
653    /// Test: Multiple impl blocks merge methods on parent type
654    ///
655    /// New design: All methods from different impl blocks are registered
656    /// under the same parent type path.
657    #[test]
658    fn test_multiple_impl_blocks_methods_merged() {
659        let mut ast_registry = ASTRegistry::new();
660        let mut symbol_registry = SymbolRegistry::new();
661        let mut ctx = ASTMutationContext::new(&mut ast_registry, &mut symbol_registry);
662
663        // Register the parent struct first
664        let struct_path = SymbolPath::parse("test_crate::TodoList").unwrap();
665        ctx.register(struct_path.clone(), SymbolKind::Struct);
666
667        let target = SymbolPath::parse("test_crate").unwrap();
668
669        // Add first impl block
670        let impl1 = r#"
671            impl TodoList {
672                pub fn new() -> Self {
673                    Self { items: vec![] }
674                }
675            }
676        "#;
677        add_item_impl(&mut ctx, &target, impl1);
678
679        // Add second impl block
680        let impl2 = r#"
681            impl TodoList {
682                pub fn add(&mut self, item: String) {
683                    self.items.push(item);
684                }
685            }
686        "#;
687        add_item_impl(&mut ctx, &target, impl2);
688
689        // Verify both methods are under TodoList::
690        let new_path = SymbolPath::parse("test_crate::TodoList::new").unwrap();
691        let add_path = SymbolPath::parse("test_crate::TodoList::add").unwrap();
692
693        assert!(
694            ctx.lookup(&new_path).is_some(),
695            "TodoList::new should exist"
696        );
697        assert!(
698            ctx.lookup(&add_path).is_some(),
699            "TodoList::add should exist"
700        );
701
702        // Verify both are methods
703        assert_eq!(
704            ctx.kind(ctx.lookup(&new_path).unwrap()),
705            Some(SymbolKind::Method)
706        );
707        assert_eq!(
708            ctx.kind(ctx.lookup(&add_path).unwrap()),
709            Some(SymbolKind::Method)
710        );
711    }
712}