ryo-source 0.1.0

High-speed Rust AST manipulation engine
Documentation
//! Remove unused imports operation.
//!
//! This is the first "Surgeon" operation - clean and simple with syn.

use crate::ast::{RustAST, UnusedImport};
use crate::visitor::{extract_use_names, use_tree_to_path};

/// Operation to detect and remove unused imports.
pub struct RemoveUnusedImports;

impl RemoveUnusedImports {
    /// Detect unused imports without removing them.
    pub fn detect(ast: &RustAST) -> Vec<UnusedImport> {
        let used_idents = ast.collect_used_identifiers();
        let imports = ast.collect_imports();

        let mut unused = Vec::new();

        for import in imports {
            let names = extract_use_names(&import.tree);
            let path = use_tree_to_path(&import.tree);

            for name in names {
                if !used_idents.contains(&name) {
                    unused.push(UnusedImport {
                        path: path.clone(),
                        name,
                    });
                }
            }
        }

        unused
    }

    /// Remove all unused imports. Returns the removed imports.
    pub fn apply(ast: &mut RustAST) -> Vec<UnusedImport> {
        let used_idents = ast.collect_used_identifiers();
        let mut removed = Vec::new();

        // Collect indices of items to remove
        let items_to_remove: Vec<usize> = ast
            .file()
            .items
            .iter()
            .enumerate()
            .filter_map(|(i, item)| {
                if let syn::Item::Use(use_item) = item {
                    let names = extract_use_names(&use_item.tree);
                    let path = use_tree_to_path(&use_item.tree);

                    // Check if ALL names from this import are unused
                    let all_unused = names.iter().all(|name| !used_idents.contains(name));

                    if all_unused && !names.is_empty() {
                        for name in names {
                            removed.push(UnusedImport {
                                path: path.clone(),
                                name,
                            });
                        }
                        return Some(i);
                    }
                }
                None
            })
            .collect();

        // Remove items in reverse order to preserve indices
        for i in items_to_remove.into_iter().rev() {
            ast.items_mut().remove(i);
        }

        removed
    }
}

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

    #[test]
    fn test_detect_unused_simple() {
        let ast = RustAST::parse("use std::io;\n\nfn main() {}").unwrap();
        let unused = RemoveUnusedImports::detect(&ast);

        assert_eq!(unused.len(), 1);
        assert_eq!(unused[0].name, "io");
    }

    #[test]
    fn test_detect_used_import() {
        let ast = RustAST::parse(
            r#"
            use std::io;

            fn main() {
                let _ = io::stdin();
            }
            "#,
        )
        .unwrap();

        let unused = RemoveUnusedImports::detect(&ast);
        assert!(unused.is_empty());
    }

    #[test]
    fn test_detect_multiple_imports() {
        let ast = RustAST::parse(
            r#"
            use std::io;
            use std::fs;

            fn main() {
                let _ = fs::read_dir(".");
            }
            "#,
        )
        .unwrap();

        let unused = RemoveUnusedImports::detect(&ast);
        assert_eq!(unused.len(), 1);
        assert_eq!(unused[0].name, "io");
    }

    #[test]
    fn test_remove_unused() {
        let mut ast = RustAST::parse(
            r#"
            use std::io;
            use std::fs;

            fn main() {
                let _ = fs::read_dir(".");
            }
            "#,
        )
        .unwrap();

        let removed = RemoveUnusedImports::apply(&mut ast);
        assert_eq!(removed.len(), 1);
        assert_eq!(removed[0].name, "io");

        let output = ast.to_string();
        assert!(!output.contains("std :: io"), "should not contain std::io");
        assert!(
            output.contains("std :: fs") || output.contains("std::fs"),
            "should contain std::fs: {}",
            output
        );
    }

    #[test]
    fn test_renamed_import_used() {
        let ast = RustAST::parse(
            r#"
            use std::io as stdio;

            fn main() {
                let _ = stdio::stdin();
            }
            "#,
        )
        .unwrap();

        let unused = RemoveUnusedImports::detect(&ast);
        assert!(unused.is_empty());
    }

    #[test]
    fn test_renamed_import_unused() {
        let ast = RustAST::parse(
            r#"
            use std::io as stdio;

            fn main() {}
            "#,
        )
        .unwrap();

        let unused = RemoveUnusedImports::detect(&ast);
        assert_eq!(unused.len(), 1);
        assert_eq!(unused[0].name, "stdio");
    }

    #[test]
    fn test_group_import_partial() {
        let ast = RustAST::parse(
            r#"
            use std::{io, fs};

            fn main() {
                let _ = fs::read_dir(".");
            }
            "#,
        )
        .unwrap();

        let unused = RemoveUnusedImports::detect(&ast);
        // Currently we detect individual names, but don't remove partial groups
        assert_eq!(unused.len(), 1);
        assert_eq!(unused[0].name, "io");
    }
}