Skip to main content

morph_cli/core/ast/
cleanup.rs

1use std::collections::{HashMap, HashSet};
2use swc_common::DUMMY_SP;
3use swc_ecma_ast::*;
4
5use crate::core::ast::semantic::SemanticModel;
6
7pub fn cleanup_imports_exports(module: &mut Module) {
8    let semantic = SemanticModel::new(module);
9    let unused_imports: HashSet<String> = semantic.get_unused_imports().into_iter().collect();
10
11    let mut import_groups: HashMap<String, Vec<ImportDecl>> = HashMap::new();
12    let mut sources_order: Vec<String> = Vec::new();
13    let mut new_body = Vec::new();
14
15    // Pass 1: Extract and filter imports
16    for item in std::mem::take(&mut module.body) {
17        if let ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) = item {
18            let mut used_specifiers = Vec::new();
19            for spec in import_decl.specifiers {
20                let local_name = match &spec {
21                    ImportSpecifier::Named(named) => named.local.sym.to_string(),
22                    ImportSpecifier::Default(def) => def.local.sym.to_string(),
23                    ImportSpecifier::Namespace(ns) => ns.local.sym.to_string(),
24                };
25                if !unused_imports.contains(&local_name) {
26                    used_specifiers.push(spec);
27                }
28            }
29
30            if !used_specifiers.is_empty() {
31                let src = import_decl.src.value.to_string();
32                if !import_groups.contains_key(&src) {
33                    sources_order.push(src.clone());
34                }
35                let decl = ImportDecl {
36                    specifiers: used_specifiers,
37                    ..import_decl
38                };
39                import_groups.entry(src).or_default().push(decl);
40            }
41        } else {
42            new_body.push(item);
43        }
44    }
45
46    // Pass 2: Merge imports per source in insertion order
47    let mut merged_imports = Vec::new();
48    for src in sources_order {
49        let decls = import_groups.remove(&src).unwrap_or_default();
50        if decls.is_empty() {
51            continue;
52        }
53
54        let mut default_spec = None;
55        let mut ns_spec = None;
56        let mut named_specs = HashMap::new();
57
58        for decl in decls {
59            for spec in decl.specifiers {
60                match spec {
61                    ImportSpecifier::Default(def) => {
62                        if default_spec.is_none() {
63                            default_spec = Some(def);
64                        }
65                    }
66                    ImportSpecifier::Namespace(ns) => {
67                        if ns_spec.is_none() {
68                            ns_spec = Some(ns);
69                        }
70                    }
71                    ImportSpecifier::Named(named) => {
72                        named_specs.insert(named.local.sym.clone(), named);
73                    }
74                }
75            }
76        }
77
78        let mut final_specifiers = Vec::new();
79        if let Some(def) = default_spec {
80            final_specifiers.push(ImportSpecifier::Default(def));
81        }
82        if let Some(ns) = ns_spec {
83            final_specifiers.push(ImportSpecifier::Namespace(ns));
84        }
85        if !named_specs.is_empty() {
86            let mut specs: Vec<_> = named_specs.into_values().collect();
87            specs.sort_by(|a, b| a.local.sym.cmp(&b.local.sym));
88            final_specifiers.extend(specs.into_iter().map(ImportSpecifier::Named));
89        }
90
91        merged_imports.push(ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
92            span: DUMMY_SP,
93            specifiers: final_specifiers,
94            src: Box::new(Str {
95                span: DUMMY_SP,
96                value: src.into(),
97                raw: None,
98            }),
99            type_only: false,
100            with: None,
101            phase: Default::default(),
102        })));
103    }
104
105    let mut final_body = merged_imports;
106    final_body.extend(new_body);
107
108    // Pass 3: Normalize bare exports
109    let mut named_exports: Vec<ExportSpecifier> = Vec::new();
110    let mut other_items = Vec::new();
111    for item in final_body {
112        if let ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(ref export)) = item {
113            if export.src.is_none() && export.with.is_none() && !export.type_only {
114                named_exports.extend(export.specifiers.clone());
115                continue;
116            }
117        }
118        other_items.push(item);
119    }
120
121    if !named_exports.is_empty() {
122        // Dedup exports based on local name
123        let mut unique_exports = Vec::new();
124        let mut seen = HashSet::new();
125        for spec in named_exports {
126            if let ExportSpecifier::Named(named) = &spec {
127                let name = match &named.orig {
128                    ModuleExportName::Ident(id) => id.sym.to_string(),
129                    ModuleExportName::Str(s) => s.value.to_string(),
130                };
131                if seen.insert(name) {
132                    unique_exports.push(spec);
133                }
134            } else {
135                unique_exports.push(spec);
136            }
137        }
138
139        // Sort unique exports deterministically to avoid churn on repeated executions
140        unique_exports.sort_by(|a, b| {
141            let a_name = match a {
142                ExportSpecifier::Named(n) => match &n.orig {
143                    ModuleExportName::Ident(id) => id.sym.to_string(),
144                    ModuleExportName::Str(s) => s.value.to_string(),
145                },
146                _ => "".to_string(),
147            };
148            let b_name = match b {
149                ExportSpecifier::Named(n) => match &n.orig {
150                    ModuleExportName::Ident(id) => id.sym.to_string(),
151                    ModuleExportName::Str(s) => s.value.to_string(),
152                },
153                _ => "".to_string(),
154            };
155            a_name.cmp(&b_name)
156        });
157
158        other_items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(
159            NamedExport {
160                span: DUMMY_SP,
161                specifiers: unique_exports,
162                src: None,
163                type_only: false,
164                with: None,
165            },
166        )));
167    }
168
169    module.body = other_items;
170}
171
172pub fn run_autofix(path: &std::path::Path) -> anyhow::Result<()> {
173    use crate::core::ast::parser::parse_file;
174    use crate::core::ast::printer::print_module;
175    use crate::core::format::{FormatOptions, FormatPipeline};
176
177    let original_content = std::fs::read_to_string(path)?;
178    let mut parsed = parse_file(path).map_err(|e| anyhow::anyhow!(e))?;
179    cleanup_imports_exports(&mut parsed.module);
180
181    let mut output = print_module(&parsed, &parsed.module).map_err(|e| anyhow::anyhow!(e))?;
182
183    // Apply formatting preservation for autofix mode
184    let format_opts = FormatOptions {
185        enabled: true,
186        use_prettier: false,
187        preserve_indent: true,
188        preserve_quotes: true,
189        preserve_semicolons: true,
190        normalize_newlines: true,
191    };
192    let mut pipeline = FormatPipeline::new(format_opts);
193    output = pipeline.format(&output, Some(&original_content), path);
194    output = crate::core::format::normalize::insert_newline_after_imports(&output);
195
196    if output != original_content {
197        std::fs::write(path, output)?;
198    }
199    Ok(())
200}
201