use ryo_analysis::{AnalysisContext, SymbolPath};
use ryo_mutations::{CreateModMutation, MutationResult};
use ryo_source::pure::{PureFile, PureItem};
use ryo_symbol::{SymbolKind, Visibility};
use crate::engine::{ASTMutationContext, ASTRegApply, ExecutionResult, MutationEvent};
pub fn create_mod_v2(
ctx: &mut AnalysisContext,
parent: &SymbolPath,
mod_name: &str,
content: &str,
is_pub: bool,
) -> ExecutionResult {
let mut mutation_ctx = ASTMutationContext::new(&mut ctx.ast_registry, &mut ctx.registry);
let result = create_mod_impl(&mut mutation_ctx, parent, mod_name, content, is_pub);
let events = mutation_ctx.into_events();
ExecutionResult::new(result, events)
}
fn create_mod_impl(
ctx: &mut ASTMutationContext,
parent: &SymbolPath,
mod_name: &str,
content: &str,
is_pub: bool,
) -> MutationResult {
let mod_path = match parent.child(mod_name) {
Ok(p) => p,
Err(e) => {
return MutationResult {
mutation_type: "CreateMod".to_string(),
changes: 0,
description: format!("Failed to build module path: {:?}", e),
};
}
};
if ctx.symbol_registry.lookup(&mod_path).is_some() {
return MutationResult {
mutation_type: "CreateMod".to_string(),
changes: 0,
description: format!("Module '{}' already exists", mod_name),
};
}
let file_content = if content.is_empty() {
format!("//! {} module\n", mod_name)
} else {
content.to_string()
};
let parsed = match PureFile::from_source(&file_content) {
Ok(file) => file,
Err(e) => {
return MutationResult {
mutation_type: "CreateMod".to_string(),
changes: 0,
description: format!("Failed to parse module content: {}", e),
};
}
};
let mod_id = match ctx
.symbol_registry
.register(mod_path.clone(), SymbolKind::Mod)
{
Ok(id) => id,
Err(e) => {
return MutationResult {
mutation_type: "CreateMod".to_string(),
changes: 0,
description: format!("Failed to register module: {:?}", e),
};
}
};
let vis = if is_pub {
Visibility::Public
} else {
Visibility::Private
};
let _ = ctx.symbol_registry.set_visibility(mod_id, vis);
ctx.ast_registry
.set_module_items(mod_id, parsed.items.clone());
let mut added_items = 0;
for item in parsed.items {
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, &mod_path, i) {
Ok(result) => {
added_items += 1 + result.methods_added; }
Err(_) => {
}
}
continue;
}
PureItem::Use(_) | PureItem::Macro(_) | PureItem::Other(_) => {
continue;
}
};
let item_path = match mod_path.child(&name) {
Ok(p) => p,
Err(_) => continue,
};
if ctx
.register_with_ast(item_path.clone(), kind, item)
.is_some()
{
added_items += 1;
}
}
ctx.emit(MutationEvent::SymbolAdded {
path: mod_path.clone(),
kind: SymbolKind::Mod,
});
MutationResult {
mutation_type: "CreateMod".to_string(),
changes: added_items + 1, description: format!(
"Created {}mod {} with {} items",
if is_pub { "pub " } else { "" },
mod_name,
added_items
),
}
}
impl ASTRegApply for CreateModMutation {
fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
let parent = match ctx.symbol_registry.path(self.parent) {
Some(p) => p.clone(),
None => {
return MutationResult {
mutation_type: "CreateMod".to_string(),
changes: 0,
description: format!("Parent module {:?} not found in registry", self.parent),
};
}
};
if ctx.symbol_registry.kind(self.parent) != Some(SymbolKind::Mod) {
return MutationResult {
mutation_type: "CreateMod".to_string(),
changes: 0,
description: format!("Target symbol {:?} is not a module", self.parent),
};
}
create_mod_impl(ctx, &parent, &self.name, &self.content, self.is_pub)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ryo_analysis::testing::ContextBuilder;
#[test]
fn test_create_mod_basic() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "// lib.rs\n")
.build();
let parent = SymbolPath::parse("test_crate").unwrap();
let result = create_mod_v2(&mut ctx, &parent, "utils", "pub fn helper() {}\n", false);
assert!(result.has_changes(), "Expected changes: {:?}", result);
assert!(result.result.changes >= 1, "Expected at least 1 change");
let mod_path = SymbolPath::parse("test_crate::utils").unwrap();
assert!(
ctx.registry.lookup(&mod_path).is_some(),
"Module should be registered"
);
let fn_path = SymbolPath::parse("test_crate::utils::helper").unwrap();
assert!(
ctx.registry.lookup(&fn_path).is_some(),
"Function should be registered"
);
}
#[test]
fn test_create_mod_empty_content() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "// lib.rs\n")
.build();
let parent = SymbolPath::parse("test_crate").unwrap();
let result = create_mod_v2(&mut ctx, &parent, "config", "", true);
assert!(result.has_changes(), "Expected changes: {:?}", result);
let mod_path = SymbolPath::parse("test_crate::config").unwrap();
assert!(
ctx.registry.lookup(&mod_path).is_some(),
"Module should be registered"
);
}
#[test]
fn test_create_mod_already_exists() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "mod utils;\n")
.with_file("src/utils.rs", "// utils\n")
.build();
let parent = SymbolPath::parse("test_crate").unwrap();
let result = create_mod_v2(&mut ctx, &parent, "utils", "pub fn helper() {}\n", false);
assert_eq!(result.result.changes, 0, "Should not add duplicate module");
assert!(
result.result.description.contains("already exists"),
"Should mention already exists"
);
}
#[test]
fn test_create_pub_mod() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "// lib.rs\n")
.build();
let parent = SymbolPath::parse("test_crate").unwrap();
let result = create_mod_v2(&mut ctx, &parent, "api", "pub fn endpoint() {}\n", true);
assert!(result.has_changes(), "Expected changes: {:?}", result);
assert!(
result.result.description.contains("pub mod"),
"Should mention pub mod"
);
}
#[test]
fn test_create_mod_with_multiple_items() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "// lib.rs\n")
.build();
let parent = SymbolPath::parse("test_crate").unwrap();
let content = r#"
pub struct Config {
pub name: String,
}
pub fn load() -> Config {
Config { name: String::new() }
}
const VERSION: &str = "1.0";
"#;
let result = create_mod_v2(&mut ctx, &parent, "config", content, true);
assert!(result.has_changes(), "Expected changes: {:?}", result);
assert!(result.result.changes >= 4, "Expected at least 4 changes");
assert!(ctx
.registry
.lookup(&SymbolPath::parse("test_crate::config").unwrap())
.is_some());
assert!(ctx
.registry
.lookup(&SymbolPath::parse("test_crate::config::Config").unwrap())
.is_some());
assert!(ctx
.registry
.lookup(&SymbolPath::parse("test_crate::config::load").unwrap())
.is_some());
assert!(ctx
.registry
.lookup(&SymbolPath::parse("test_crate::config::VERSION").unwrap())
.is_some());
}
#[test]
fn test_create_nested_mod() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "pub mod common;\n")
.with_file("src/common/mod.rs", "pub mod types;\n")
.with_file("src/common/types.rs", "pub struct MyType;\n")
.build();
let parent = SymbolPath::parse("test_crate::common").unwrap();
let result = create_mod_v2(
&mut ctx,
&parent,
"traits",
"pub trait Identifiable {}\n",
true,
);
assert!(result.has_changes(), "Expected changes: {:?}", result);
let mod_path = SymbolPath::parse("test_crate::common::traits").unwrap();
assert!(
ctx.registry.lookup(&mod_path).is_some(),
"Module should be registered under common"
);
let trait_path = SymbolPath::parse("test_crate::common::traits::Identifiable").unwrap();
assert!(
ctx.registry.lookup(&trait_path).is_some(),
"Trait should be registered"
);
}
}