ryo-executor 0.1.0

[experimental] Mutation execution engine for RYO - parallel execution, conflict detection, workspace management
Documentation
//! ASTRegApply implementation for CreateModMutation
//!
//! Creates a new module with content by:
//! 1. Parsing the content to extract items
//! 2. Registering each item under the new module path
//!
//! Note: mod declarations (mod xxx;) are NOT stored here.
//! RegistryGenerator automatically generates them from module hierarchy.

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};

/// Create a new module with content (V2 ASTRegistry-based)
///
/// This operation:
/// 1. Parses the content to extract items
/// 2. Registers each item under `parent::mod_name::item_name`
/// 3. Adds a mod declaration (`mod xxx;` or `pub mod xxx;`) to the parent
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 {
    // Build the module path
    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),
            };
        }
    };

    // Check if module already exists
    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),
        };
    }

    // Parse the content (or use default if empty)
    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),
            };
        }
    };

    // Register the module itself
    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),
            };
        }
    };

    // Set visibility for the module (used by RegistryGenerator for mod declaration)
    let vis = if is_pub {
        Visibility::Public
    } else {
        Visibility::Private
    };
    let _ = ctx.symbol_registry.set_visibility(mod_id, vis);

    // Store module_items for FileDumper to preserve use statements and file-level items.
    // This is critical for proper output generation.
    ctx.ast_registry
        .set_module_items(mod_id, parsed.items.clone());

    // The module declaration (mod xxx;) will be added to parent
    // But the content items belong to the new module file
    let mut added_items = 0;

    // Register and store each item from the parsed content
    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) => {
                // Use common impl block registration logic
                // This handles both impl block and method registration
                match super::utils::register_impl_block(ctx, &mod_path, i) {
                    Ok(result) => {
                        added_items += 1 + result.methods_added; // impl block + methods
                    }
                    Err(_) => {
                        // Skip on error, but continue processing other items
                    }
                }
                continue;
            }
            PureItem::Use(_) | PureItem::Macro(_) | PureItem::Other(_) => {
                // These don't get registered as symbols
                continue;
            }
        };

        // Build item path under the new module
        let item_path = match mod_path.child(&name) {
            Ok(p) => p,
            Err(_) => continue,
        };

        // Register and store the item
        if ctx
            .register_with_ast(item_path.clone(), kind, item)
            .is_some()
        {
            added_items += 1;
        }
    }

    // Note: mod declaration (mod xxx;) is NOT stored in ASTRegistry.
    // RegistryGenerator automatically generates mod declarations from
    // the module hierarchy derived from child item paths.

    // Emit event
    ctx.emit(MutationEvent::SymbolAdded {
        path: mod_path.clone(),
        kind: SymbolKind::Mod,
    });

    MutationResult {
        mutation_type: "CreateMod".to_string(),
        changes: added_items + 1, // +1 for the module itself
        description: format!(
            "Created {}mod {} with {} items",
            if is_pub { "pub " } else { "" },
            mod_name,
            added_items
        ),
    }
}

// ============================================================================
// ASTRegApply implementation
// ============================================================================

impl ASTRegApply for CreateModMutation {
    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
        // Use the provided symbol_id to get the parent path
        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),
                };
            }
        };

        // Verify the target is a module
        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");

        // Verify the module was registered
        let mod_path = SymbolPath::parse("test_crate::utils").unwrap();
        assert!(
            ctx.registry.lookup(&mod_path).is_some(),
            "Module should be registered"
        );

        // Verify the helper function was 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);

        // Verify the module was registered
        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);

        // Should not create duplicate
        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);
        // 1 for module + 3 for items (struct, fn, const)
        assert!(result.result.changes >= 4, "Expected at least 4 changes");

        // Verify all items were registered
        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();

        // Create a new module inside common
        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);

        // Verify the module was registered under common
        let mod_path = SymbolPath::parse("test_crate::common::traits").unwrap();
        assert!(
            ctx.registry.lookup(&mod_path).is_some(),
            "Module should be registered under common"
        );

        // Verify the trait was registered
        let trait_path = SymbolPath::parse("test_crate::common::traits::Identifiable").unwrap();
        assert!(
            ctx.registry.lookup(&trait_path).is_some(),
            "Trait should be registered"
        );
    }
}