Skip to main content

ryo_source/ops/
remove_unused_imports.rs

1//! Remove unused imports operation.
2//!
3//! This is the first "Surgeon" operation - clean and simple with syn.
4
5use crate::ast::{RustAST, UnusedImport};
6use crate::visitor::{extract_use_names, use_tree_to_path};
7
8/// Operation to detect and remove unused imports.
9pub struct RemoveUnusedImports;
10
11impl RemoveUnusedImports {
12    /// Detect unused imports without removing them.
13    pub fn detect(ast: &RustAST) -> Vec<UnusedImport> {
14        let used_idents = ast.collect_used_identifiers();
15        let imports = ast.collect_imports();
16
17        let mut unused = Vec::new();
18
19        for import in imports {
20            let names = extract_use_names(&import.tree);
21            let path = use_tree_to_path(&import.tree);
22
23            for name in names {
24                if !used_idents.contains(&name) {
25                    unused.push(UnusedImport {
26                        path: path.clone(),
27                        name,
28                    });
29                }
30            }
31        }
32
33        unused
34    }
35
36    /// Remove all unused imports. Returns the removed imports.
37    pub fn apply(ast: &mut RustAST) -> Vec<UnusedImport> {
38        let used_idents = ast.collect_used_identifiers();
39        let mut removed = Vec::new();
40
41        // Collect indices of items to remove
42        let items_to_remove: Vec<usize> = ast
43            .file()
44            .items
45            .iter()
46            .enumerate()
47            .filter_map(|(i, item)| {
48                if let syn::Item::Use(use_item) = item {
49                    let names = extract_use_names(&use_item.tree);
50                    let path = use_tree_to_path(&use_item.tree);
51
52                    // Check if ALL names from this import are unused
53                    let all_unused = names.iter().all(|name| !used_idents.contains(name));
54
55                    if all_unused && !names.is_empty() {
56                        for name in names {
57                            removed.push(UnusedImport {
58                                path: path.clone(),
59                                name,
60                            });
61                        }
62                        return Some(i);
63                    }
64                }
65                None
66            })
67            .collect();
68
69        // Remove items in reverse order to preserve indices
70        for i in items_to_remove.into_iter().rev() {
71            ast.items_mut().remove(i);
72        }
73
74        removed
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn test_detect_unused_simple() {
84        let ast = RustAST::parse("use std::io;\n\nfn main() {}").unwrap();
85        let unused = RemoveUnusedImports::detect(&ast);
86
87        assert_eq!(unused.len(), 1);
88        assert_eq!(unused[0].name, "io");
89    }
90
91    #[test]
92    fn test_detect_used_import() {
93        let ast = RustAST::parse(
94            r#"
95            use std::io;
96
97            fn main() {
98                let _ = io::stdin();
99            }
100            "#,
101        )
102        .unwrap();
103
104        let unused = RemoveUnusedImports::detect(&ast);
105        assert!(unused.is_empty());
106    }
107
108    #[test]
109    fn test_detect_multiple_imports() {
110        let ast = RustAST::parse(
111            r#"
112            use std::io;
113            use std::fs;
114
115            fn main() {
116                let _ = fs::read_dir(".");
117            }
118            "#,
119        )
120        .unwrap();
121
122        let unused = RemoveUnusedImports::detect(&ast);
123        assert_eq!(unused.len(), 1);
124        assert_eq!(unused[0].name, "io");
125    }
126
127    #[test]
128    fn test_remove_unused() {
129        let mut ast = RustAST::parse(
130            r#"
131            use std::io;
132            use std::fs;
133
134            fn main() {
135                let _ = fs::read_dir(".");
136            }
137            "#,
138        )
139        .unwrap();
140
141        let removed = RemoveUnusedImports::apply(&mut ast);
142        assert_eq!(removed.len(), 1);
143        assert_eq!(removed[0].name, "io");
144
145        let output = ast.to_string();
146        assert!(!output.contains("std :: io"), "should not contain std::io");
147        assert!(
148            output.contains("std :: fs") || output.contains("std::fs"),
149            "should contain std::fs: {}",
150            output
151        );
152    }
153
154    #[test]
155    fn test_renamed_import_used() {
156        let ast = RustAST::parse(
157            r#"
158            use std::io as stdio;
159
160            fn main() {
161                let _ = stdio::stdin();
162            }
163            "#,
164        )
165        .unwrap();
166
167        let unused = RemoveUnusedImports::detect(&ast);
168        assert!(unused.is_empty());
169    }
170
171    #[test]
172    fn test_renamed_import_unused() {
173        let ast = RustAST::parse(
174            r#"
175            use std::io as stdio;
176
177            fn main() {}
178            "#,
179        )
180        .unwrap();
181
182        let unused = RemoveUnusedImports::detect(&ast);
183        assert_eq!(unused.len(), 1);
184        assert_eq!(unused[0].name, "stdio");
185    }
186
187    #[test]
188    fn test_group_import_partial() {
189        let ast = RustAST::parse(
190            r#"
191            use std::{io, fs};
192
193            fn main() {
194                let _ = fs::read_dir(".");
195            }
196            "#,
197        )
198        .unwrap();
199
200        let unused = RemoveUnusedImports::detect(&ast);
201        // Currently we detect individual names, but don't remove partial groups
202        assert_eq!(unused.len(), 1);
203        assert_eq!(unused[0].name, "io");
204    }
205}