use ryo_analysis::AnalysisContext;
use ryo_mutations::{
AddItemMutation, AddPureItemsMutation, MoveItemMutation, MutationResult, RemoveItemMutation,
};
use ryo_source::pure::{PureFile, PureItem, PureUse, PureUseTree, PureVis};
use ryo_source::ItemKind;
use ryo_symbol::{SymbolKind, SymbolPath, Visibility};
use crate::engine::{ASTMutationContext, ASTRegApply, ExecutionResult};
pub fn add_item_v2(
ctx: &mut AnalysisContext,
target: &SymbolPath,
content: &str,
) -> ExecutionResult {
let mut mutation_ctx = ASTMutationContext::new(&mut ctx.ast_registry, &mut ctx.registry);
let result = add_item_impl(&mut mutation_ctx, target, content);
let events = mutation_ctx.into_events();
ExecutionResult::new(result, events)
}
fn add_item_impl(
ctx: &mut ASTMutationContext,
target: &SymbolPath,
content: &str,
) -> MutationResult {
let parsed = match PureFile::from_source(content.trim()) {
Ok(file) => file,
Err(e) => {
return MutationResult {
mutation_type: "AddItem".to_string(),
changes: 0,
description: format!("Failed to parse content: {}", e),
};
}
};
let items = parsed.items;
if items.is_empty() {
return MutationResult {
mutation_type: "AddItem".to_string(),
changes: 0,
description: "No items found in content".to_string(),
};
}
add_pure_items_impl(ctx, target, items, "AddItem")
}
fn add_pure_items_impl(
ctx: &mut ASTMutationContext,
target: &SymbolPath,
items: Vec<PureItem>,
mutation_type: &'static str,
) -> MutationResult {
let mut added = 0;
let mut descriptions = Vec::new();
let mut skipped = Vec::new();
for item in items {
let mod_visibility = if let PureItem::Mod(m) = &item {
Some(pure_vis_to_visibility(&m.vis))
} else {
None
};
let (name, kind) = match &item {
PureItem::Fn(f) => (f.name.clone(), SymbolKind::Function),
PureItem::Struct(s) => (s.name.clone(), SymbolKind::Struct),
PureItem::Enum(e) => (e.name.clone(), SymbolKind::Enum),
PureItem::Const(c) => (c.name.clone(), SymbolKind::Const),
PureItem::Static(s) => (s.name.clone(), SymbolKind::Static),
PureItem::Type(t) => (t.name.clone(), SymbolKind::TypeAlias),
PureItem::Trait(t) => (t.name.clone(), SymbolKind::Trait),
PureItem::Mod(m) => (m.name.clone(), SymbolKind::Mod),
PureItem::Impl(i) => {
match super::utils::register_impl_block(ctx, target, i) {
Ok(result) => {
added += result.methods_added;
descriptions.push(result.description);
}
Err(e) => {
skipped.push(format!("Impl block for '{}': {}", i.self_ty, e));
}
}
continue;
}
PureItem::Use(u) => {
let target_str = target.to_string();
if let Some(module_id) = ctx.symbol_registry.lookup(target) {
let mut items = ctx
.ast_registry
.get_module_items(module_id)
.cloned()
.unwrap_or_default();
let insert_pos = items
.iter()
.position(|i| !matches!(i, PureItem::Use(_)))
.unwrap_or(items.len());
items.insert(insert_pos, PureItem::Use(u.clone()));
ctx.ast_registry.set_module_items(module_id, items);
ctx.emit_modified(
module_id,
crate::engine::events::ModificationType::Other(
"use statement added".to_string(),
),
);
added += 1;
descriptions.push(format!("Added use statement to '{}'", target_str));
}
continue;
}
PureItem::Macro(_) | PureItem::Other(_) => {
continue;
}
};
let target_str = target.to_string();
let full_path = if target_str == "crate" {
SymbolPath::parse(&format!("crate::{}", name))
} else {
SymbolPath::parse(&format!("{}::{}", target_str, name))
};
let path = match full_path {
Ok(p) => p,
Err(e) => {
skipped.push(format!(
"{} '{}': invalid path ({})",
kind.display_name(),
name,
e
));
continue;
}
};
match ctx.register_with_ast(path.clone(), kind, item.clone()) {
Some(id) => {
if let Some(vis) = mod_visibility {
let _ = ctx.symbol_registry.set_visibility(id, vis);
}
if let PureItem::Mod(m) = &item {
if !m.items.is_empty() {
ctx.ast_registry.mark_inline_module(id);
}
}
if let Some(parent_id) = ctx.symbol_registry.lookup(target) {
let mut items = ctx
.ast_registry
.get_module_items(parent_id)
.cloned()
.unwrap_or_default();
items.push(item);
ctx.ast_registry.set_module_items(parent_id, items);
}
added += 1;
descriptions.push(format!("Added {} '{}'", kind.display_name(), name));
}
None => {
skipped.push(format!(
"{} '{}': registration failed (symbol may already exist with different kind)",
kind.display_name(),
name
));
}
}
}
let description = match (descriptions.is_empty(), skipped.is_empty()) {
(true, true) => "No items added".to_string(),
(true, false) => format!("No items added. Skipped: {}", skipped.join("; ")),
(false, true) => descriptions.join(", "),
(false, false) => format!(
"{}. Skipped: {}",
descriptions.join(", "),
skipped.join("; ")
),
};
MutationResult {
mutation_type: mutation_type.to_string(),
changes: added,
description,
}
}
pub fn remove_item_v2(
ctx: &mut AnalysisContext,
symbol_id: ryo_symbol::SymbolId,
item_kind: &crate::ItemKind,
) -> ExecutionResult {
let mut mutation_ctx = ASTMutationContext::new(&mut ctx.ast_registry, &mut ctx.registry);
let result = remove_item_impl(&mut mutation_ctx, symbol_id, item_kind);
let events = mutation_ctx.into_events();
ExecutionResult::new(result, events)
}
fn remove_item_impl(
ctx: &mut ASTMutationContext,
symbol_id: ryo_symbol::SymbolId,
item_kind: &crate::ItemKind,
) -> MutationResult {
ctx.remove_symbol(symbol_id);
MutationResult {
mutation_type: "RemoveItem".to_string(),
changes: 1,
description: format!("Removed {:?} ({:?})", item_kind, symbol_id),
}
}
impl ASTRegApply for AddItemMutation {
fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
let module_id = self.parent;
if ctx.symbol_registry.kind(module_id) != Some(SymbolKind::Mod) {
return MutationResult {
mutation_type: "AddItem".to_string(),
changes: 0,
description: format!("Target symbol {} is not a module", module_id),
};
}
let target = match ctx.symbol_registry.path(module_id) {
Some(p) => p.clone(),
None => {
return MutationResult {
mutation_type: "AddItem".to_string(),
changes: 0,
description: format!("Module {} not found in registry", module_id),
};
}
};
add_item_impl(ctx, &target, &self.content)
}
}
impl ASTRegApply for AddPureItemsMutation {
fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
let module_id = self.parent;
if ctx.symbol_registry.kind(module_id) != Some(SymbolKind::Mod) {
return MutationResult {
mutation_type: "AddPureItems".to_string(),
changes: 0,
description: format!("Target symbol {} is not a module", module_id),
};
}
let target = match ctx.symbol_registry.path(module_id) {
Some(p) => p.clone(),
None => {
return MutationResult {
mutation_type: "AddPureItems".to_string(),
changes: 0,
description: format!("Module {} not found in registry", module_id),
};
}
};
add_pure_items_impl(ctx, &target, self.items.clone(), "AddPureItems")
}
}
impl ASTRegApply for RemoveItemMutation {
fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
remove_item_impl(ctx, self.symbol_id, &self.item_kind)
}
}
impl ASTRegApply for MoveItemMutation {
fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
move_item_impl(
ctx,
&self.source,
&self.target,
&self.item_name,
&self.item_kind,
self.add_use,
)
}
}
fn move_item_impl(
ctx: &mut ASTMutationContext,
source: &SymbolPath,
target: &SymbolPath,
item_name: &str,
item_kind: &ItemKind,
add_use: bool,
) -> MutationResult {
let expected_kind = match item_kind {
ItemKind::Struct => Some(SymbolKind::Struct),
ItemKind::Enum => Some(SymbolKind::Enum),
ItemKind::Function => Some(SymbolKind::Function),
ItemKind::Trait => Some(SymbolKind::Trait),
ItemKind::Impl => Some(SymbolKind::Impl),
ItemKind::TypeAlias => Some(SymbolKind::TypeAlias),
ItemKind::Const => Some(SymbolKind::Const),
ItemKind::Static => Some(SymbolKind::Static),
ItemKind::Mod => Some(SymbolKind::Mod),
_ => None,
};
let source_id = ctx
.symbol_registry
.iter()
.find(|(id, path)| {
let path_matches = path.name() == item_name && path.parent() == Some(source.clone());
let kind_matches = expected_kind
.map(|k| ctx.symbol_registry.kind(*id) == Some(k))
.unwrap_or(true);
path_matches && kind_matches
})
.map(|(id, _)| id);
let source_id = match source_id {
Some(id) => id,
None => {
return MutationResult {
mutation_type: "MoveItem".to_string(),
changes: 0,
description: format!("Item '{}' not found in {}", item_name, source),
};
}
};
let ast = match ctx.get_ast(source_id).cloned() {
Some(ast) => ast,
None => {
return MutationResult {
mutation_type: "MoveItem".to_string(),
changes: 0,
description: format!("No AST found for '{}'", item_name),
};
}
};
let kind = ctx.kind(source_id).unwrap_or(SymbolKind::Struct);
ctx.remove_symbol(source_id);
let target_path = match target.child(item_name) {
Ok(p) => p,
Err(_) => {
return MutationResult {
mutation_type: "MoveItem".to_string(),
changes: 0,
description: format!("Invalid target path: {}::{}", target, item_name),
};
}
};
if ctx
.register_with_ast(target_path.clone(), kind, ast)
.is_none()
{
return MutationResult {
mutation_type: "MoveItem".to_string(),
changes: 0,
description: format!("Failed to register at new path: {}", target_path),
};
}
if add_use {
let use_path_str = format!("{}::{}", target, item_name);
let parts: Vec<&str> = use_path_str.split("::").collect();
let tree = parts
.iter()
.rev()
.fold(None, |acc: Option<PureUseTree>, part| {
Some(match acc {
None => PureUseTree::Name(part.to_string()),
Some(subtree) => PureUseTree::Path {
path: part.to_string(),
tree: Box::new(subtree),
},
})
})
.unwrap_or(PureUseTree::Name(item_name.to_string()));
let use_item = PureItem::Use(PureUse {
vis: PureVis::Private,
tree,
});
if let Some(source_mod_id) = ctx.lookup(source) {
let mut items = ctx
.ast_registry
.get_module_items(source_mod_id)
.cloned()
.unwrap_or_default();
let insert_pos = items
.iter()
.position(|i| !matches!(i, PureItem::Use(_)))
.unwrap_or(items.len());
items.insert(insert_pos, use_item);
ctx.ast_registry.set_module_items(source_mod_id, items);
}
}
MutationResult {
mutation_type: "MoveItem".to_string(),
changes: 1,
description: format!(
"Moved {} '{}' from {} to {}",
kind.display_name(),
item_name,
source,
target
),
}
}
fn pure_vis_to_visibility(vis: &PureVis) -> Visibility {
match vis {
PureVis::Public => Visibility::Public,
PureVis::Crate => Visibility::Crate,
PureVis::Super => Visibility::Super,
PureVis::Private => Visibility::Private,
PureVis::In(path) => {
ryo_symbol::SymbolPath::parse(path)
.map(|p| Visibility::Restricted(Box::new(p)))
.unwrap_or(Visibility::Private)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ryo_analysis::{ASTRegistry, SymbolRegistry};
#[test]
fn test_add_impl_block_registers_methods_on_parent_type() {
let mut ast_registry = ASTRegistry::new();
let mut symbol_registry = SymbolRegistry::new();
let mut ctx = ASTMutationContext::new(&mut ast_registry, &mut symbol_registry);
let struct_path = SymbolPath::parse("test_crate::TodoList").unwrap();
ctx.register(struct_path.clone(), SymbolKind::Struct);
let target = SymbolPath::parse("test_crate").unwrap();
let impl_code = r#"
impl TodoList {
pub fn new() -> Self {
Self { items: vec![] }
}
}
"#;
let result = add_item_impl(&mut ctx, &target, impl_code);
assert_eq!(result.changes, 1);
let method_path = SymbolPath::parse("test_crate::TodoList::new").unwrap();
let method_id = ctx.lookup(&method_path);
assert!(
method_id.is_some(),
"Method should be registered as TodoList::new"
);
assert_eq!(ctx.kind(method_id.unwrap()), Some(SymbolKind::Method));
}
#[test]
fn test_multiple_impl_blocks_methods_merged() {
let mut ast_registry = ASTRegistry::new();
let mut symbol_registry = SymbolRegistry::new();
let mut ctx = ASTMutationContext::new(&mut ast_registry, &mut symbol_registry);
let struct_path = SymbolPath::parse("test_crate::TodoList").unwrap();
ctx.register(struct_path.clone(), SymbolKind::Struct);
let target = SymbolPath::parse("test_crate").unwrap();
let impl1 = r#"
impl TodoList {
pub fn new() -> Self {
Self { items: vec![] }
}
}
"#;
add_item_impl(&mut ctx, &target, impl1);
let impl2 = r#"
impl TodoList {
pub fn add(&mut self, item: String) {
self.items.push(item);
}
}
"#;
add_item_impl(&mut ctx, &target, impl2);
let new_path = SymbolPath::parse("test_crate::TodoList::new").unwrap();
let add_path = SymbolPath::parse("test_crate::TodoList::add").unwrap();
assert!(
ctx.lookup(&new_path).is_some(),
"TodoList::new should exist"
);
assert!(
ctx.lookup(&add_path).is_some(),
"TodoList::add should exist"
);
assert_eq!(
ctx.kind(ctx.lookup(&new_path).unwrap()),
Some(SymbolKind::Method)
);
assert_eq!(
ctx.kind(ctx.lookup(&add_path).unwrap()),
Some(SymbolKind::Method)
);
}
}