1use 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
13pub 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 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
56fn 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 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 match super::utils::register_impl_block(ctx, target, i) {
87 Ok(result) => {
88 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 let target_str = target.to_string();
104 if let Some(module_id) = ctx.symbol_registry.lookup(target) {
105 let mut items = ctx
107 .ast_registry
108 .get_module_items(module_id)
109 .cloned()
110 .unwrap_or_default();
111
112 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 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 continue;
137 }
138 };
139
140 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 match ctx.register_with_ast(path.clone(), kind, item.clone()) {
163 Some(id) => {
164 if let Some(vis) = mod_visibility {
167 let _ = ctx.symbol_registry.set_visibility(id, vis);
168 }
169
170 if let PureItem::Mod(m) = &item {
175 if !m.items.is_empty() {
176 ctx.ast_registry.mark_inline_module(id);
177 }
178 }
179
180 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 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
224pub 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 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
253impl ASTRegApply for AddItemMutation {
258 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
259 let module_id = self.parent;
261
262 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 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 let module_id = self.parent;
291
292 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 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
336fn 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 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 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 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 ctx.remove_symbol(source_id);
398
399 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 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 if add_use {
425 let use_path_str = format!("{}::{}", target, item_name);
427
428 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 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 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
482fn 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 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]
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 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 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 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]
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 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 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 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 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 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}