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(_) => {
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 MoveItemMutation {
324    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
325        move_item_impl(
326            ctx,
327            &self.source,
328            &self.target,
329            &self.item_name,
330            &self.item_kind,
331            self.add_use,
332        )
333    }
334}
335
336/// Move an item from one module to another
337fn move_item_impl(
338    ctx: &mut ASTMutationContext,
339    source: &SymbolPath,
340    target: &SymbolPath,
341    item_name: &str,
342    item_kind: &ItemKind,
343    add_use: bool,
344) -> MutationResult {
345    // Convert ItemKind to SymbolKind for lookup
346    let expected_kind = match item_kind {
347        ItemKind::Struct => Some(SymbolKind::Struct),
348        ItemKind::Enum => Some(SymbolKind::Enum),
349        ItemKind::Function => Some(SymbolKind::Function),
350        ItemKind::Trait => Some(SymbolKind::Trait),
351        ItemKind::Impl => Some(SymbolKind::Impl),
352        ItemKind::TypeAlias => Some(SymbolKind::TypeAlias),
353        ItemKind::Const => Some(SymbolKind::Const),
354        ItemKind::Static => Some(SymbolKind::Static),
355        ItemKind::Mod => Some(SymbolKind::Mod),
356        _ => None,
357    };
358
359    // 1. Find source symbol by iterating registry (to match kind)
360    let source_id = ctx
361        .symbol_registry
362        .iter()
363        .find(|(id, path)| {
364            let path_matches = path.name() == item_name && path.parent() == Some(source.clone());
365            let kind_matches = expected_kind
366                .map(|k| ctx.symbol_registry.kind(*id) == Some(k))
367                .unwrap_or(true);
368            path_matches && kind_matches
369        })
370        .map(|(id, _)| id);
371
372    let source_id = match source_id {
373        Some(id) => id,
374        None => {
375            return MutationResult {
376                mutation_type: "MoveItem".to_string(),
377                changes: 0,
378                description: format!("Item '{}' not found in {}", item_name, source),
379            };
380        }
381    };
382
383    // 2. Get AST and kind before removing
384    let ast = match ctx.get_ast(source_id).cloned() {
385        Some(ast) => ast,
386        None => {
387            return MutationResult {
388                mutation_type: "MoveItem".to_string(),
389                changes: 0,
390                description: format!("No AST found for '{}'", item_name),
391            };
392        }
393    };
394    let kind = ctx.kind(source_id).unwrap_or(SymbolKind::Struct);
395
396    // 3. Remove from old location
397    ctx.remove_symbol(source_id);
398
399    // 4. Build target path
400    let target_path = match target.child(item_name) {
401        Ok(p) => p,
402        Err(_) => {
403            return MutationResult {
404                mutation_type: "MoveItem".to_string(),
405                changes: 0,
406                description: format!("Invalid target path: {}::{}", target, item_name),
407            };
408        }
409    };
410
411    // 5. Register at new location
412    if ctx
413        .register_with_ast(target_path.clone(), kind, ast)
414        .is_none()
415    {
416        return MutationResult {
417            mutation_type: "MoveItem".to_string(),
418            changes: 0,
419            description: format!("Failed to register at new path: {}", target_path),
420        };
421    }
422
423    // 6. Optionally add use statement in source module
424    if add_use {
425        // Build use path: target::item_name
426        let use_path_str = format!("{}::{}", target, item_name);
427
428        // Parse path into PureUseTree
429        // e.g., "crate::core::Task" -> Path("crate", Path("core", Name("Task")))
430        let parts: Vec<&str> = use_path_str.split("::").collect();
431        let tree = parts
432            .iter()
433            .rev()
434            .fold(None, |acc: Option<PureUseTree>, part| {
435                Some(match acc {
436                    None => PureUseTree::Name(part.to_string()),
437                    Some(subtree) => PureUseTree::Path {
438                        path: part.to_string(),
439                        tree: Box::new(subtree),
440                    },
441                })
442            })
443            .unwrap_or(PureUseTree::Name(item_name.to_string()));
444
445        let use_item = PureItem::Use(PureUse {
446            vis: PureVis::Private,
447            tree,
448        });
449
450        // Find source module and add use to its module_items
451        if let Some(source_mod_id) = ctx.lookup(source) {
452            let mut items = ctx
453                .ast_registry
454                .get_module_items(source_mod_id)
455                .cloned()
456                .unwrap_or_default();
457
458            // Insert after existing use statements
459            let insert_pos = items
460                .iter()
461                .position(|i| !matches!(i, PureItem::Use(_)))
462                .unwrap_or(items.len());
463            items.insert(insert_pos, use_item);
464
465            ctx.ast_registry.set_module_items(source_mod_id, items);
466        }
467    }
468
469    MutationResult {
470        mutation_type: "MoveItem".to_string(),
471        changes: 1,
472        description: format!(
473            "Moved {} '{}' from {} to {}",
474            kind.display_name(),
475            item_name,
476            source,
477            target
478        ),
479    }
480}
481
482/// Convert PureVis to ryo_symbol::Visibility
483fn pure_vis_to_visibility(vis: &PureVis) -> Visibility {
484    match vis {
485        PureVis::Public => Visibility::Public,
486        PureVis::Crate => Visibility::Crate,
487        PureVis::Super => Visibility::Super,
488        PureVis::Private => Visibility::Private,
489        PureVis::In(path) => {
490            // Try to parse as SymbolPath, fallback to Private if invalid
491            ryo_symbol::SymbolPath::parse(path)
492                .map(|p| Visibility::Restricted(Box::new(p)))
493                .unwrap_or(Visibility::Private)
494        }
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501    use ryo_analysis::{ASTRegistry, SymbolRegistry};
502
503    /// Test: Impl block methods are registered directly on parent type
504    ///
505    /// New design: impl blocks are file-level construct.
506    /// Methods are registered as Struct::method, not <impl Struct>::N::method.
507    #[test]
508    fn test_add_impl_block_registers_methods_on_parent_type() {
509        let mut ast_registry = ASTRegistry::new();
510        let mut symbol_registry = SymbolRegistry::new();
511        let mut ctx = ASTMutationContext::new(&mut ast_registry, &mut symbol_registry);
512
513        // Register the parent struct first
514        let struct_path = SymbolPath::parse("test_crate::TodoList").unwrap();
515        ctx.register(struct_path.clone(), SymbolKind::Struct);
516
517        let target = SymbolPath::parse("test_crate").unwrap();
518
519        // Add impl block with method
520        let impl_code = r#"
521            impl TodoList {
522                pub fn new() -> Self {
523                    Self { items: vec![] }
524                }
525            }
526        "#;
527        let result = add_item_impl(&mut ctx, &target, impl_code);
528        assert_eq!(result.changes, 1);
529
530        // Verify method is registered as TodoList::new (not <impl TodoList>::N::new)
531        let method_path = SymbolPath::parse("test_crate::TodoList::new").unwrap();
532        let method_id = ctx.lookup(&method_path);
533        assert!(
534            method_id.is_some(),
535            "Method should be registered as TodoList::new"
536        );
537        assert_eq!(ctx.kind(method_id.unwrap()), Some(SymbolKind::Method));
538    }
539
540    /// Test: Multiple impl blocks merge methods on parent type
541    ///
542    /// New design: All methods from different impl blocks are registered
543    /// under the same parent type path.
544    #[test]
545    fn test_multiple_impl_blocks_methods_merged() {
546        let mut ast_registry = ASTRegistry::new();
547        let mut symbol_registry = SymbolRegistry::new();
548        let mut ctx = ASTMutationContext::new(&mut ast_registry, &mut symbol_registry);
549
550        // Register the parent struct first
551        let struct_path = SymbolPath::parse("test_crate::TodoList").unwrap();
552        ctx.register(struct_path.clone(), SymbolKind::Struct);
553
554        let target = SymbolPath::parse("test_crate").unwrap();
555
556        // Add first impl block
557        let impl1 = r#"
558            impl TodoList {
559                pub fn new() -> Self {
560                    Self { items: vec![] }
561                }
562            }
563        "#;
564        add_item_impl(&mut ctx, &target, impl1);
565
566        // Add second impl block
567        let impl2 = r#"
568            impl TodoList {
569                pub fn add(&mut self, item: String) {
570                    self.items.push(item);
571                }
572            }
573        "#;
574        add_item_impl(&mut ctx, &target, impl2);
575
576        // Verify both methods are under TodoList::
577        let new_path = SymbolPath::parse("test_crate::TodoList::new").unwrap();
578        let add_path = SymbolPath::parse("test_crate::TodoList::add").unwrap();
579
580        assert!(
581            ctx.lookup(&new_path).is_some(),
582            "TodoList::new should exist"
583        );
584        assert!(
585            ctx.lookup(&add_path).is_some(),
586            "TodoList::add should exist"
587        );
588
589        // Verify both are methods
590        assert_eq!(
591            ctx.kind(ctx.lookup(&new_path).unwrap()),
592            Some(SymbolKind::Method)
593        );
594        assert_eq!(
595            ctx.kind(ctx.lookup(&add_path).unwrap()),
596            Some(SymbolKind::Method)
597        );
598    }
599}