morph_cli/core/ast/
cleanup.rs1use 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 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 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 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 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 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 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