Skip to main content

ryo_executor/engine/impls/
create_mod.rs

1//! ASTRegApply implementation for CreateModMutation
2//!
3//! Creates a new module with content by:
4//! 1. Parsing the content to extract items
5//! 2. Registering each item under the new module path
6//!
7//! Note: mod declarations (mod xxx;) are NOT stored here.
8//! RegistryGenerator automatically generates them from module hierarchy.
9
10use ryo_analysis::{AnalysisContext, SymbolPath};
11use ryo_mutations::{CreateModMutation, MutationResult};
12use ryo_source::pure::{PureFile, PureItem};
13use ryo_symbol::{SymbolKind, Visibility};
14
15use crate::engine::{ASTMutationContext, ASTRegApply, ExecutionResult, MutationEvent};
16
17/// Create a new module with content (V2 ASTRegistry-based)
18///
19/// This operation:
20/// 1. Parses the content to extract items
21/// 2. Registers each item under `parent::mod_name::item_name`
22/// 3. Adds a mod declaration (`mod xxx;` or `pub mod xxx;`) to the parent
23pub fn create_mod_v2(
24    ctx: &mut AnalysisContext,
25    parent: &SymbolPath,
26    mod_name: &str,
27    content: &str,
28    is_pub: bool,
29) -> ExecutionResult {
30    let mut mutation_ctx = ASTMutationContext::new(&mut ctx.ast_registry, &mut ctx.registry);
31
32    let result = create_mod_impl(&mut mutation_ctx, parent, mod_name, content, is_pub);
33    let events = mutation_ctx.into_events();
34
35    ExecutionResult::new(result, events)
36}
37
38fn create_mod_impl(
39    ctx: &mut ASTMutationContext,
40    parent: &SymbolPath,
41    mod_name: &str,
42    content: &str,
43    is_pub: bool,
44) -> MutationResult {
45    // Build the module path
46    let mod_path = match parent.child(mod_name) {
47        Ok(p) => p,
48        Err(e) => {
49            return MutationResult {
50                mutation_type: "CreateMod".to_string(),
51                changes: 0,
52                description: format!("Failed to build module path: {:?}", e),
53            };
54        }
55    };
56
57    // Check if module already exists
58    if ctx.symbol_registry.lookup(&mod_path).is_some() {
59        return MutationResult {
60            mutation_type: "CreateMod".to_string(),
61            changes: 0,
62            description: format!("Module '{}' already exists", mod_name),
63        };
64    }
65
66    // Parse the content (or use default if empty)
67    let file_content = if content.is_empty() {
68        format!("//! {} module\n", mod_name)
69    } else {
70        content.to_string()
71    };
72
73    let parsed = match PureFile::from_source(&file_content) {
74        Ok(file) => file,
75        Err(e) => {
76            return MutationResult {
77                mutation_type: "CreateMod".to_string(),
78                changes: 0,
79                description: format!("Failed to parse module content: {}", e),
80            };
81        }
82    };
83
84    // Register the module itself
85    let mod_id = match ctx
86        .symbol_registry
87        .register(mod_path.clone(), SymbolKind::Mod)
88    {
89        Ok(id) => id,
90        Err(e) => {
91            return MutationResult {
92                mutation_type: "CreateMod".to_string(),
93                changes: 0,
94                description: format!("Failed to register module: {:?}", e),
95            };
96        }
97    };
98
99    // Set visibility for the module (used by RegistryGenerator for mod declaration)
100    let vis = if is_pub {
101        Visibility::Public
102    } else {
103        Visibility::Private
104    };
105    let _ = ctx.symbol_registry.set_visibility(mod_id, vis);
106
107    // Store module_items for FileDumper to preserve use statements and file-level items.
108    // This is critical for proper output generation.
109    ctx.ast_registry
110        .set_module_items(mod_id, parsed.items.clone());
111
112    // The module declaration (mod xxx;) will be added to parent
113    // But the content items belong to the new module file
114    let mut added_items = 0;
115
116    // Register and store each item from the parsed content
117    for item in parsed.items {
118        let (name, kind) = match &item {
119            PureItem::Fn(f) => (f.name.clone(), SymbolKind::Function),
120            PureItem::Struct(s) => (s.name.clone(), SymbolKind::Struct),
121            PureItem::Enum(e) => (e.name.clone(), SymbolKind::Enum),
122            PureItem::Const(c) => (c.name.clone(), SymbolKind::Const),
123            PureItem::Static(s) => (s.name.clone(), SymbolKind::Static),
124            PureItem::Type(t) => (t.name.clone(), SymbolKind::TypeAlias),
125            PureItem::Trait(t) => (t.name.clone(), SymbolKind::Trait),
126            PureItem::Mod(m) => (m.name.clone(), SymbolKind::Mod),
127            PureItem::Impl(i) => {
128                // Use common impl block registration logic
129                // This handles both impl block and method registration
130                match super::utils::register_impl_block(ctx, &mod_path, i) {
131                    Ok(result) => {
132                        added_items += 1 + result.methods_added; // impl block + methods
133                    }
134                    Err(_) => {
135                        // Skip on error, but continue processing other items
136                    }
137                }
138                continue;
139            }
140            PureItem::Use(_) | PureItem::Macro(_) | PureItem::Other(_) => {
141                // These don't get registered as symbols
142                continue;
143            }
144        };
145
146        // Build item path under the new module
147        let item_path = match mod_path.child(&name) {
148            Ok(p) => p,
149            Err(_) => continue,
150        };
151
152        // Register and store the item
153        if ctx
154            .register_with_ast(item_path.clone(), kind, item)
155            .is_some()
156        {
157            added_items += 1;
158        }
159    }
160
161    // Note: mod declaration (mod xxx;) is NOT stored in ASTRegistry.
162    // RegistryGenerator automatically generates mod declarations from
163    // the module hierarchy derived from child item paths.
164
165    // Emit event
166    ctx.emit(MutationEvent::SymbolAdded {
167        path: mod_path.clone(),
168        kind: SymbolKind::Mod,
169    });
170
171    MutationResult {
172        mutation_type: "CreateMod".to_string(),
173        changes: added_items + 1, // +1 for the module itself
174        description: format!(
175            "Created {}mod {} with {} items",
176            if is_pub { "pub " } else { "" },
177            mod_name,
178            added_items
179        ),
180    }
181}
182
183// ============================================================================
184// ASTRegApply implementation
185// ============================================================================
186
187impl ASTRegApply for CreateModMutation {
188    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
189        // Use the provided symbol_id to get the parent path
190        let parent = match ctx.symbol_registry.path(self.parent) {
191            Some(p) => p.clone(),
192            None => {
193                return MutationResult {
194                    mutation_type: "CreateMod".to_string(),
195                    changes: 0,
196                    description: format!("Parent module {:?} not found in registry", self.parent),
197                };
198            }
199        };
200
201        // Verify the target is a module
202        if ctx.symbol_registry.kind(self.parent) != Some(SymbolKind::Mod) {
203            return MutationResult {
204                mutation_type: "CreateMod".to_string(),
205                changes: 0,
206                description: format!("Target symbol {:?} is not a module", self.parent),
207            };
208        }
209
210        create_mod_impl(ctx, &parent, &self.name, &self.content, self.is_pub)
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use ryo_analysis::testing::ContextBuilder;
218
219    #[test]
220    fn test_create_mod_basic() {
221        let mut ctx = ContextBuilder::new()
222            .with_file("src/lib.rs", "// lib.rs\n")
223            .build();
224
225        let parent = SymbolPath::parse("test_crate").unwrap();
226        let result = create_mod_v2(&mut ctx, &parent, "utils", "pub fn helper() {}\n", false);
227
228        assert!(result.has_changes(), "Expected changes: {:?}", result);
229        assert!(result.result.changes >= 1, "Expected at least 1 change");
230
231        // Verify the module was registered
232        let mod_path = SymbolPath::parse("test_crate::utils").unwrap();
233        assert!(
234            ctx.registry.lookup(&mod_path).is_some(),
235            "Module should be registered"
236        );
237
238        // Verify the helper function was registered
239        let fn_path = SymbolPath::parse("test_crate::utils::helper").unwrap();
240        assert!(
241            ctx.registry.lookup(&fn_path).is_some(),
242            "Function should be registered"
243        );
244    }
245
246    #[test]
247    fn test_create_mod_empty_content() {
248        let mut ctx = ContextBuilder::new()
249            .with_file("src/lib.rs", "// lib.rs\n")
250            .build();
251
252        let parent = SymbolPath::parse("test_crate").unwrap();
253        let result = create_mod_v2(&mut ctx, &parent, "config", "", true);
254
255        assert!(result.has_changes(), "Expected changes: {:?}", result);
256
257        // Verify the module was registered
258        let mod_path = SymbolPath::parse("test_crate::config").unwrap();
259        assert!(
260            ctx.registry.lookup(&mod_path).is_some(),
261            "Module should be registered"
262        );
263    }
264
265    #[test]
266    fn test_create_mod_already_exists() {
267        let mut ctx = ContextBuilder::new()
268            .with_file("src/lib.rs", "mod utils;\n")
269            .with_file("src/utils.rs", "// utils\n")
270            .build();
271
272        let parent = SymbolPath::parse("test_crate").unwrap();
273        let result = create_mod_v2(&mut ctx, &parent, "utils", "pub fn helper() {}\n", false);
274
275        // Should not create duplicate
276        assert_eq!(result.result.changes, 0, "Should not add duplicate module");
277        assert!(
278            result.result.description.contains("already exists"),
279            "Should mention already exists"
280        );
281    }
282
283    #[test]
284    fn test_create_pub_mod() {
285        let mut ctx = ContextBuilder::new()
286            .with_file("src/lib.rs", "// lib.rs\n")
287            .build();
288
289        let parent = SymbolPath::parse("test_crate").unwrap();
290        let result = create_mod_v2(&mut ctx, &parent, "api", "pub fn endpoint() {}\n", true);
291
292        assert!(result.has_changes(), "Expected changes: {:?}", result);
293        assert!(
294            result.result.description.contains("pub mod"),
295            "Should mention pub mod"
296        );
297    }
298
299    #[test]
300    fn test_create_mod_with_multiple_items() {
301        let mut ctx = ContextBuilder::new()
302            .with_file("src/lib.rs", "// lib.rs\n")
303            .build();
304
305        let parent = SymbolPath::parse("test_crate").unwrap();
306        let content = r#"
307pub struct Config {
308    pub name: String,
309}
310
311pub fn load() -> Config {
312    Config { name: String::new() }
313}
314
315const VERSION: &str = "1.0";
316"#;
317        let result = create_mod_v2(&mut ctx, &parent, "config", content, true);
318
319        assert!(result.has_changes(), "Expected changes: {:?}", result);
320        // 1 for module + 3 for items (struct, fn, const)
321        assert!(result.result.changes >= 4, "Expected at least 4 changes");
322
323        // Verify all items were registered
324        assert!(ctx
325            .registry
326            .lookup(&SymbolPath::parse("test_crate::config").unwrap())
327            .is_some());
328        assert!(ctx
329            .registry
330            .lookup(&SymbolPath::parse("test_crate::config::Config").unwrap())
331            .is_some());
332        assert!(ctx
333            .registry
334            .lookup(&SymbolPath::parse("test_crate::config::load").unwrap())
335            .is_some());
336        assert!(ctx
337            .registry
338            .lookup(&SymbolPath::parse("test_crate::config::VERSION").unwrap())
339            .is_some());
340    }
341
342    #[test]
343    fn test_create_nested_mod() {
344        let mut ctx = ContextBuilder::new()
345            .with_file("src/lib.rs", "pub mod common;\n")
346            .with_file("src/common/mod.rs", "pub mod types;\n")
347            .with_file("src/common/types.rs", "pub struct MyType;\n")
348            .build();
349
350        // Create a new module inside common
351        let parent = SymbolPath::parse("test_crate::common").unwrap();
352        let result = create_mod_v2(
353            &mut ctx,
354            &parent,
355            "traits",
356            "pub trait Identifiable {}\n",
357            true,
358        );
359
360        assert!(result.has_changes(), "Expected changes: {:?}", result);
361
362        // Verify the module was registered under common
363        let mod_path = SymbolPath::parse("test_crate::common::traits").unwrap();
364        assert!(
365            ctx.registry.lookup(&mod_path).is_some(),
366            "Module should be registered under common"
367        );
368
369        // Verify the trait was registered
370        let trait_path = SymbolPath::parse("test_crate::common::traits::Identifiable").unwrap();
371        assert!(
372            ctx.registry.lookup(&trait_path).is_some(),
373            "Trait should be registered"
374        );
375    }
376}