ryo_executor/engine/impls/
create_mod.rs1use 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
17pub 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 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 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 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 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 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 ctx.ast_registry
110 .set_module_items(mod_id, parsed.items.clone());
111
112 let mut added_items = 0;
115
116 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 match super::utils::register_impl_block(ctx, &mod_path, i) {
131 Ok(result) => {
132 added_items += 1 + result.methods_added; }
134 Err(_) => {
135 }
137 }
138 continue;
139 }
140 PureItem::Use(_) | PureItem::Macro(_) | PureItem::Other(_) => {
141 continue;
143 }
144 };
145
146 let item_path = match mod_path.child(&name) {
148 Ok(p) => p,
149 Err(_) => continue,
150 };
151
152 if ctx
154 .register_with_ast(item_path.clone(), kind, item)
155 .is_some()
156 {
157 added_items += 1;
158 }
159 }
160
161 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, description: format!(
175 "Created {}mod {} with {} items",
176 if is_pub { "pub " } else { "" },
177 mod_name,
178 added_items
179 ),
180 }
181}
182
183impl ASTRegApply for CreateModMutation {
188 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
189 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 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 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 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 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 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 assert!(result.result.changes >= 4, "Expected at least 4 changes");
322
323 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 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 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 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}