ryo-source 0.1.0

High-speed Rust AST manipulation engine
Documentation
//! Rename operation.
//!
//! Mut operation to rename variables, functions, and other symbols.

use proc_macro2::Ident;
use syn::visit_mut::VisitMut;

use crate::ast::RustAST;

/// Result of a rename operation.
#[derive(Debug, Clone)]
pub struct RenameResult {
    /// Number of occurrences renamed.
    pub count: usize,
    /// Old name.
    pub old_name: String,
    /// New name.
    pub new_name: String,
}

/// Rename operation for symbols.
pub struct Rename;

impl Rename {
    /// Rename all occurrences of a symbol.
    ///
    /// This renames:
    /// - Local variable definitions and uses
    /// - Function definitions and calls
    /// - Struct/enum definitions and uses
    pub fn apply(ast: &mut RustAST, old_name: &str, new_name: &str) -> RenameResult {
        let mut renamer = SymbolRenamer::new(old_name, new_name);
        renamer.visit_file_mut(ast.file_mut());

        RenameResult {
            count: renamer.count,
            old_name: old_name.to_string(),
            new_name: new_name.to_string(),
        }
    }

    /// Rename a local variable within a specific function.
    pub fn rename_local_in_fn(
        ast: &mut RustAST,
        fn_name: &str,
        old_name: &str,
        new_name: &str,
    ) -> RenameResult {
        let mut renamer = ScopedRenamer::new(fn_name, old_name, new_name);
        renamer.visit_file_mut(ast.file_mut());

        RenameResult {
            count: renamer.count,
            old_name: old_name.to_string(),
            new_name: new_name.to_string(),
        }
    }
}

/// Visitor that renames all occurrences of a symbol.
struct SymbolRenamer {
    old_name: String,
    new_name: String,
    count: usize,
}

impl SymbolRenamer {
    fn new(old_name: &str, new_name: &str) -> Self {
        Self {
            old_name: old_name.to_string(),
            new_name: new_name.to_string(),
            count: 0,
        }
    }

    fn maybe_rename(&mut self, ident: &mut Ident) {
        if *ident == self.old_name {
            *ident = Ident::new(&self.new_name, ident.span());
            self.count += 1;
        }
    }
}

impl VisitMut for SymbolRenamer {
    fn visit_ident_mut(&mut self, ident: &mut Ident) {
        self.maybe_rename(ident);
    }

    fn visit_pat_ident_mut(&mut self, node: &mut syn::PatIdent) {
        self.maybe_rename(&mut node.ident);
        syn::visit_mut::visit_pat_ident_mut(self, node);
    }

    fn visit_expr_path_mut(&mut self, node: &mut syn::ExprPath) {
        // Rename simple paths (single segment, no type args)
        if node.path.segments.len() == 1 {
            self.maybe_rename(&mut node.path.segments[0].ident);
        }
        syn::visit_mut::visit_expr_path_mut(self, node);
    }

    fn visit_item_fn_mut(&mut self, node: &mut syn::ItemFn) {
        self.maybe_rename(&mut node.sig.ident);
        syn::visit_mut::visit_item_fn_mut(self, node);
    }

    fn visit_item_struct_mut(&mut self, node: &mut syn::ItemStruct) {
        self.maybe_rename(&mut node.ident);
        syn::visit_mut::visit_item_struct_mut(self, node);
    }

    fn visit_item_enum_mut(&mut self, node: &mut syn::ItemEnum) {
        self.maybe_rename(&mut node.ident);
        syn::visit_mut::visit_item_enum_mut(self, node);
    }

    fn visit_type_path_mut(&mut self, node: &mut syn::TypePath) {
        // Rename type references
        if node.path.segments.len() == 1 {
            self.maybe_rename(&mut node.path.segments[0].ident);
        }
        syn::visit_mut::visit_type_path_mut(self, node);
    }
}

/// Visitor that renames a local variable only within a specific function.
struct ScopedRenamer {
    target_fn: String,
    old_name: String,
    new_name: String,
    count: usize,
    in_target_fn: bool,
}

impl ScopedRenamer {
    fn new(target_fn: &str, old_name: &str, new_name: &str) -> Self {
        Self {
            target_fn: target_fn.to_string(),
            old_name: old_name.to_string(),
            new_name: new_name.to_string(),
            count: 0,
            in_target_fn: false,
        }
    }

    fn maybe_rename(&mut self, ident: &mut Ident) {
        if self.in_target_fn && *ident == self.old_name {
            *ident = Ident::new(&self.new_name, ident.span());
            self.count += 1;
        }
    }
}

impl VisitMut for ScopedRenamer {
    fn visit_item_fn_mut(&mut self, node: &mut syn::ItemFn) {
        let was_in_target = self.in_target_fn;

        if node.sig.ident == self.target_fn {
            self.in_target_fn = true;
        }

        syn::visit_mut::visit_item_fn_mut(self, node);

        self.in_target_fn = was_in_target;
    }

    fn visit_pat_ident_mut(&mut self, node: &mut syn::PatIdent) {
        self.maybe_rename(&mut node.ident);
        syn::visit_mut::visit_pat_ident_mut(self, node);
    }

    fn visit_expr_path_mut(&mut self, node: &mut syn::ExprPath) {
        if node.path.segments.len() == 1 {
            self.maybe_rename(&mut node.path.segments[0].ident);
        }
        syn::visit_mut::visit_expr_path_mut(self, node);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_rename_local_var() {
        let mut ast = RustAST::parse(
            r#"
            fn main() {
                let x = 1;
                let y = x + 1;
                println!("{}", x);
            }
            "#,
        )
        .unwrap();

        let result = Rename::apply(&mut ast, "x", "value");
        // println! macro args are not parsed by syn, so only 2 renames (1 def + 1 use in binary expr)
        assert_eq!(result.count, 2);

        let output = ast.to_string();
        assert!(output.contains("let value"));
        assert!(!output.contains("let x ="));
    }

    #[test]
    fn test_rename_function() {
        let mut ast = RustAST::parse(
            r#"
            fn foo() {}
            fn main() {
                foo();
            }
            "#,
        )
        .unwrap();

        let result = Rename::apply(&mut ast, "foo", "bar");
        assert_eq!(result.count, 2); // 1 def + 1 call

        let output = ast.to_string();
        assert!(output.contains("fn bar"));
        assert!(output.contains("bar ()") || output.contains("bar()"));
        assert!(!output.contains("foo"));
    }

    #[test]
    fn test_rename_struct() {
        let mut ast = RustAST::parse(
            r#"
            struct Point { x: i32, y: i32 }
            fn main() {
                let p: Point = Point { x: 0, y: 0 };
            }
            "#,
        )
        .unwrap();

        let result = Rename::apply(&mut ast, "Point", "Vec2");
        assert!(result.count >= 2); // def + uses

        let output = ast.to_string();
        assert!(output.contains("struct Vec2"));
        assert!(!output.contains("Point"));
    }

    #[test]
    fn test_scoped_rename() {
        let mut ast = RustAST::parse(
            r#"
            fn foo() {
                let x = 1;
            }
            fn bar() {
                let x = 2;
            }
            "#,
        )
        .unwrap();

        let result = Rename::rename_local_in_fn(&mut ast, "foo", "x", "renamed");
        assert_eq!(result.count, 1);

        let output = ast.to_string();
        assert!(output.contains("let renamed"));
        // bar's x should be unchanged
        assert!(output.contains("let x = 2"));
    }
}