1use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, HashSet};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DeadCodeAnalysis {
16 pub unused_functions: Vec<UnusedItem>,
18 pub unused_types: Vec<UnusedItem>,
20 pub unused_imports: Vec<String>,
22 pub unused_dependencies: Vec<String>,
24 pub potential_size_reduction: u64,
26}
27
28impl DeadCodeAnalysis {
29 pub fn total_unused_items(&self) -> usize {
31 self.unused_functions.len()
32 + self.unused_types.len()
33 + self.unused_imports.len()
34 + self.unused_dependencies.len()
35 }
36
37 pub fn generate_report(&self) -> String {
39 let mut report = String::new();
40
41 report.push_str("=== Dead Code Analysis ===\n\n");
42
43 if !self.unused_functions.is_empty() {
44 report.push_str(&format!("Unused Functions ({}): \n", self.unused_functions.len()));
45 for item in self.unused_functions.iter().take(10) {
46 report.push_str(&format!(" - {} (line {}) - ~{} bytes\n",
47 item.name, item.line, item.estimated_size));
48 }
49 if self.unused_functions.len() > 10 {
50 report.push_str(&format!(" ... and {} more\n", self.unused_functions.len() - 10));
51 }
52 report.push('\n');
53 }
54
55 if !self.unused_types.is_empty() {
56 report.push_str(&format!("Unused Types ({}): \n", self.unused_types.len()));
57 for item in self.unused_types.iter().take(10) {
58 report.push_str(&format!(" - {} (line {}) - ~{} bytes\n",
59 item.name, item.line, item.estimated_size));
60 }
61 if self.unused_types.len() > 10 {
62 report.push_str(&format!(" ... and {} more\n", self.unused_types.len() - 10));
63 }
64 report.push('\n');
65 }
66
67 if !self.unused_imports.is_empty() {
68 report.push_str(&format!("Unused Imports ({}): \n", self.unused_imports.len()));
69 for import in self.unused_imports.iter().take(10) {
70 report.push_str(&format!(" - {}\n", import));
71 }
72 if self.unused_imports.len() > 10 {
73 report.push_str(&format!(" ... and {} more\n", self.unused_imports.len() - 10));
74 }
75 report.push('\n');
76 }
77
78 if !self.unused_dependencies.is_empty() {
79 report.push_str(&format!("Unused Dependencies ({}): \n", self.unused_dependencies.len()));
80 for dep in &self.unused_dependencies {
81 report.push_str(&format!(" - {}\n", dep));
82 }
83 report.push('\n');
84 }
85
86 report.push_str(&format!("Total Items: {}\n", self.total_unused_items()));
87 report.push_str(&format!("Potential Size Reduction: {:.2} KB\n",
88 self.potential_size_reduction as f64 / 1024.0));
89
90 report
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct UnusedItem {
97 pub name: String,
99 pub item_type: String,
101 pub file: String,
103 pub line: usize,
105 pub estimated_size: u64,
107 pub reason: String,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct WasmOptPass {
114 pub name: String,
116 pub description: String,
118 pub impact: f64,
120 pub default: bool,
122}
123
124impl WasmOptPass {
125 pub fn all_passes() -> Vec<Self> {
127 vec![
128 Self {
130 name: "dce".to_string(),
131 description: "Dead Code Elimination - removes unreachable code".to_string(),
132 impact: 0.25,
133 default: true,
134 },
135 Self {
136 name: "remove-unused-brs".to_string(),
137 description: "Remove unused branch targets".to_string(),
138 impact: 0.05,
139 default: true,
140 },
141 Self {
142 name: "remove-unused-names".to_string(),
143 description: "Remove unused function/global names".to_string(),
144 impact: 0.10,
145 default: true,
146 },
147 Self {
148 name: "remove-unused-module-elements".to_string(),
149 description: "Remove unused module elements".to_string(),
150 impact: 0.15,
151 default: true,
152 },
153 Self {
155 name: "inlining".to_string(),
156 description: "Inline small functions".to_string(),
157 impact: 0.20,
158 default: true,
159 },
160 Self {
161 name: "inline-main".to_string(),
162 description: "Inline the main function".to_string(),
163 impact: 0.05,
164 default: false,
165 },
166 Self {
168 name: "simplify-locals".to_string(),
169 description: "Simplify local variable usage".to_string(),
170 impact: 0.15,
171 default: true,
172 },
173 Self {
174 name: "coalesce-locals".to_string(),
175 description: "Coalesce local variables".to_string(),
176 impact: 0.10,
177 default: true,
178 },
179 Self {
180 name: "merge-blocks".to_string(),
181 description: "Merge basic blocks".to_string(),
182 impact: 0.08,
183 default: true,
184 },
185 Self {
186 name: "optimize-instructions".to_string(),
187 description: "Optimize individual instructions".to_string(),
188 impact: 0.12,
189 default: true,
190 },
191 Self {
193 name: "strip".to_string(),
194 description: "Strip debug information".to_string(),
195 impact: 0.30,
196 default: true,
197 },
198 Self {
199 name: "strip-debug".to_string(),
200 description: "Strip debug information (alias)".to_string(),
201 impact: 0.30,
202 default: true,
203 },
204 Self {
205 name: "strip-producers".to_string(),
206 description: "Strip producer metadata".to_string(),
207 impact: 0.05,
208 default: true,
209 },
210 Self {
211 name: "strip-target-features".to_string(),
212 description: "Strip target features section".to_string(),
213 impact: 0.03,
214 default: true,
215 },
216 Self {
218 name: "precompute".to_string(),
219 description: "Precompute constant expressions".to_string(),
220 impact: 0.10,
221 default: true,
222 },
223 Self {
224 name: "vacuum".to_string(),
225 description: "Remove duplicate/unused code after other optimizations".to_string(),
226 impact: 0.15,
227 default: true,
228 },
229 Self {
230 name: "duplicate-function-elimination".to_string(),
231 description: "Eliminate duplicate functions".to_string(),
232 impact: 0.12,
233 default: false,
234 },
235 ]
236 }
237
238 pub fn size_optimization_passes() -> Vec<String> {
240 vec![
241 "dce".to_string(),
242 "remove-unused-brs".to_string(),
243 "remove-unused-names".to_string(),
244 "remove-unused-module-elements".to_string(),
245 "strip-debug".to_string(),
246 "strip-producers".to_string(),
247 "strip-target-features".to_string(),
248 "vacuum".to_string(),
249 "inlining".to_string(),
250 "simplify-locals".to_string(),
251 "coalesce-locals".to_string(),
252 "merge-blocks".to_string(),
253 "optimize-instructions".to_string(),
254 "precompute".to_string(),
255 "duplicate-function-elimination".to_string(),
256 ]
257 }
258
259 pub fn performance_optimization_passes() -> Vec<String> {
261 vec![
262 "inlining".to_string(),
263 "inline-main".to_string(),
264 "optimize-instructions".to_string(),
265 "precompute".to_string(),
266 "simplify-locals".to_string(),
267 "merge-blocks".to_string(),
268 ]
269 }
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct TreeShakingAnalysis {
275 pub total_dependencies: usize,
277 pub used_dependencies: Vec<String>,
279 pub unused_dependencies: Vec<String>,
281 pub partially_used: Vec<PartiallyUsedDependency>,
283 pub estimated_savings: u64,
285}
286
287impl TreeShakingAnalysis {
288 pub fn generate_report(&self) -> String {
290 let mut report = String::new();
291
292 report.push_str("=== Tree-Shaking Analysis ===\n\n");
293 report.push_str(&format!("Total Dependencies: {}\n", self.total_dependencies));
294 report.push_str(&format!("Fully Used: {}\n", self.used_dependencies.len()));
295 report.push_str(&format!("Unused: {}\n", self.unused_dependencies.len()));
296 report.push_str(&format!("Partially Used: {}\n\n", self.partially_used.len()));
297
298 if !self.unused_dependencies.is_empty() {
299 report.push_str("Unused Dependencies (can be removed):\n");
300 for dep in &self.unused_dependencies {
301 report.push_str(&format!(" ❌ {}\n", dep));
302 }
303 report.push('\n');
304 }
305
306 if !self.partially_used.is_empty() {
307 report.push_str("Partially Used Dependencies:\n");
308 for item in &self.partially_used {
309 report.push_str(&format!(" ⚠️ {} - using {}/{} features\n",
310 item.dependency, item.used_features.len(), item.total_features));
311 }
312 report.push('\n');
313 }
314
315 report.push_str(&format!("Estimated Savings: {:.2} KB\n",
316 self.estimated_savings as f64 / 1024.0));
317
318 report
319 }
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct PartiallyUsedDependency {
325 pub dependency: String,
327 pub used_features: Vec<String>,
329 pub total_features: usize,
331 pub unused_features: Vec<String>,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct CallGraph {
338 pub nodes: HashMap<String, CallGraphNode>,
340 pub edges: HashMap<String, Vec<String>>,
342 pub entry_points: HashSet<String>,
344}
345
346impl CallGraph {
347 pub fn new() -> Self {
349 Self {
350 nodes: HashMap::new(),
351 edges: HashMap::new(),
352 entry_points: HashSet::new(),
353 }
354 }
355
356 pub fn add_function(&mut self, name: String, node: CallGraphNode) {
358 self.nodes.insert(name.clone(), node);
359 self.edges.entry(name).or_insert_with(Vec::new);
360 }
361
362 pub fn add_call(&mut self, from: String, to: String) {
364 self.edges.entry(from).or_insert_with(Vec::new).push(to);
365 }
366
367 pub fn add_entry_point(&mut self, name: String) {
369 self.entry_points.insert(name);
370 }
371
372 pub fn compute_reachable(&self) -> HashSet<String> {
374 let mut reachable = HashSet::new();
375 let mut stack: Vec<String> = self.entry_points.iter().cloned().collect();
376
377 while let Some(func) = stack.pop() {
378 if reachable.insert(func.clone()) {
379 if let Some(callees) = self.edges.get(&func) {
380 for callee in callees {
381 if !reachable.contains(callee) {
382 stack.push(callee.clone());
383 }
384 }
385 }
386 }
387 }
388
389 reachable
390 }
391
392 pub fn find_unreachable(&self) -> Vec<String> {
394 let reachable = self.compute_reachable();
395 self.nodes
396 .keys()
397 .filter(|name| !reachable.contains(*name))
398 .cloned()
399 .collect()
400 }
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct CallGraphNode {
406 pub name: String,
408 pub visibility: Visibility,
410 pub is_generic: bool,
412 pub is_async: bool,
414 pub is_inline: bool,
416 pub line: usize,
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
422pub enum Visibility {
423 Public,
424 PublicCrate,
425 Private,
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
430pub enum OptimizationStrategy {
431 Conservative,
433 Moderate,
435 Aggressive,
437}
438
439impl OptimizationStrategy {
440 pub fn remove_private_unused(&self) -> bool {
442 matches!(self, OptimizationStrategy::Moderate | OptimizationStrategy::Aggressive)
443 }
444
445 pub fn remove_public_unused(&self) -> bool {
447 matches!(self, OptimizationStrategy::Aggressive)
448 }
449
450 pub fn inline_small_functions(&self) -> bool {
452 matches!(self, OptimizationStrategy::Moderate | OptimizationStrategy::Aggressive)
453 }
454}
455
456pub struct DeadCodeEliminator {
458 pub strategy: OptimizationStrategy,
460 pub preserve_exports: bool,
462}
463
464impl Default for DeadCodeEliminator {
465 fn default() -> Self {
466 Self::new()
467 }
468}
469
470impl DeadCodeEliminator {
471 pub fn new() -> Self {
473 Self {
474 strategy: OptimizationStrategy::Moderate,
475 preserve_exports: true,
476 }
477 }
478
479 pub fn with_strategy(strategy: OptimizationStrategy) -> Self {
481 Self {
482 strategy,
483 preserve_exports: true,
484 }
485 }
486
487 pub fn build_call_graph(&self, code: &str) -> CallGraph {
489 let mut graph = CallGraph::new();
490 let lines: Vec<&str> = code.lines().collect();
491
492 let mut current_function: Option<String> = None;
494
495 for (idx, line) in lines.iter().enumerate() {
496 let trimmed = line.trim();
497
498 if trimmed.starts_with("//") || trimmed.starts_with("/*") {
500 continue;
501 }
502
503 if trimmed.starts_with("fn ") || trimmed.starts_with("pub fn ") ||
505 trimmed.starts_with("pub(crate) fn ") || trimmed.starts_with("async fn ") {
506 if let Some(name) = Self::extract_function_name(line) {
507 let visibility = if trimmed.starts_with("pub fn ") {
508 Visibility::Public
509 } else if trimmed.starts_with("pub(crate) fn ") {
510 Visibility::PublicCrate
511 } else {
512 Visibility::Private
513 };
514
515 let node = CallGraphNode {
516 name: name.clone(),
517 visibility: visibility.clone(),
518 is_generic: line.contains('<') && line.contains('>'),
519 is_async: line.contains("async"),
520 is_inline: Self::has_inline_attribute(&lines, idx),
521 line: idx + 1,
522 };
523
524 graph.add_function(name.clone(), node);
525 current_function = Some(name.clone());
526
527 if name == "main" || visibility == Visibility::Public ||
529 Self::has_wasm_bindgen_attribute(&lines, idx) {
530 graph.add_entry_point(name);
531 }
532 }
533 }
534
535 if let Some(ref from_func) = current_function {
537 for to_func in Self::extract_function_calls(line) {
538 graph.add_call(from_func.clone(), to_func);
539 }
540 }
541
542 if trimmed == "}" && current_function.is_some() {
544 current_function = None;
545 }
546 }
547
548 graph
549 }
550
551 fn extract_function_calls(line: &str) -> Vec<String> {
553 let mut calls = Vec::new();
554 let line = line.trim();
555
556 let mut chars = line.chars().peekable();
558 let mut current_word = String::new();
559
560 while let Some(ch) = chars.next() {
561 if ch.is_alphanumeric() || ch == '_' {
562 current_word.push(ch);
563 } else if ch == '(' && !current_word.is_empty() {
564 if !Self::is_rust_keyword(¤t_word) {
566 calls.push(current_word.clone());
567 }
568 current_word.clear();
569 } else {
570 current_word.clear();
571 }
572 }
573
574 calls
575 }
576
577 fn is_rust_keyword(word: &str) -> bool {
579 matches!(word, "if" | "else" | "match" | "for" | "while" | "loop" |
580 "fn" | "let" | "mut" | "const" | "static" | "struct" |
581 "enum" | "impl" | "trait" | "mod" | "use" | "pub")
582 }
583
584 fn has_inline_attribute(lines: &[&str], idx: usize) -> bool {
586 if idx > 0 {
587 lines[idx - 1].trim().contains("#[inline")
588 } else {
589 false
590 }
591 }
592
593 fn has_wasm_bindgen_attribute(lines: &[&str], idx: usize) -> bool {
595 if idx > 0 {
596 lines[idx - 1].trim().contains("#[wasm_bindgen")
597 } else {
598 false
599 }
600 }
601
602 pub fn analyze_with_call_graph(&self, code: &str) -> DeadCodeAnalysis {
604 let graph = self.build_call_graph(code);
605 let unreachable = graph.find_unreachable();
606 let mut unused_functions = Vec::new();
607
608 for func_name in unreachable {
609 if let Some(node) = graph.nodes.get(&func_name) {
610 let should_remove = match node.visibility {
612 Visibility::Private => self.strategy.remove_private_unused(),
613 Visibility::PublicCrate => self.strategy.remove_private_unused(),
614 Visibility::Public => self.strategy.remove_public_unused(),
615 };
616
617 if should_remove && (!self.preserve_exports || node.visibility == Visibility::Private) {
618 unused_functions.push(UnusedItem {
619 name: func_name.clone(),
620 item_type: "function".to_string(),
621 file: "generated.rs".to_string(),
622 line: node.line,
623 estimated_size: 150,
624 reason: "Unreachable from entry points".to_string(),
625 });
626 }
627 }
628 }
629
630 let unused_types = Vec::new();
631 let unused_imports = self.find_unused_imports(code);
632 let unused_dependencies = Vec::new();
633
634 let potential_reduction =
635 (unused_functions.len() * 150) as u64 +
636 (unused_types.len() * 50) as u64 +
637 (unused_imports.len() * 20) as u64;
638
639 DeadCodeAnalysis {
640 unused_functions,
641 unused_types,
642 unused_imports,
643 unused_dependencies,
644 potential_size_reduction: potential_reduction,
645 }
646 }
647
648 fn find_unused_imports(&self, code: &str) -> Vec<String> {
650 let mut unused = Vec::new();
651 let lines: Vec<&str> = code.lines().collect();
652
653 for line in lines {
654 if line.trim().starts_with("use ") {
655 if let Some(import) = Self::extract_import(line) {
656 let item_name = if let Some(pos) = import.rfind("::") {
658 &import[pos + 2..]
659 } else {
660 &import
661 };
662
663 let usage_count = code.matches(item_name).count();
665 if usage_count <= 1 { unused.push(import);
667 }
668 }
669 }
670 }
671
672 unused
673 }
674
675 pub fn analyze_rust_code(code: &str) -> DeadCodeAnalysis {
677 let eliminator = Self::new();
678 eliminator.analyze_with_call_graph(code)
679 }
680
681 fn extract_function_name(line: &str) -> Option<String> {
683 let line = line.trim();
684 if let Some(start) = line.find("fn ") {
685 let after_fn = &line[start + 3..];
686 if let Some(end) = after_fn.find('(') {
687 let name = after_fn[..end].trim();
688 if !name.is_empty() {
689 return Some(name.to_string());
690 }
691 }
692 }
693 None
694 }
695
696 fn extract_import(line: &str) -> Option<String> {
698 let line = line.trim();
699 if line.starts_with("use ") {
700 let import = line.trim_start_matches("use ").trim_end_matches(';').trim();
701 Some(import.to_string())
702 } else {
703 None
704 }
705 }
706
707 pub fn generate_wasm_opt_command(
709 input: &str,
710 output: &str,
711 optimization_level: u8,
712 ) -> String {
713 let passes = WasmOptPass::size_optimization_passes();
714
715 let mut cmd = format!("wasm-opt -O{}", optimization_level);
716
717 for pass in passes {
718 cmd.push_str(&format!(" --{}", pass));
719 }
720
721 cmd.push_str(&format!(" {} -o {}", input, output));
722 cmd
723 }
724
725 pub fn analyze_tree_shaking(
727 dependencies: &HashMap<String, Vec<String>>,
728 used_items: &HashSet<String>,
729 ) -> TreeShakingAnalysis {
730 let total_dependencies = dependencies.len();
731 let mut used_dependencies = Vec::new();
732 let mut unused_dependencies = Vec::new();
733 let mut partially_used = Vec::new();
734
735 for (dep_name, features) in dependencies {
736 let used_features: Vec<String> = features
737 .iter()
738 .filter(|f| used_items.contains(*f))
739 .cloned()
740 .collect();
741
742 if used_features.is_empty() {
743 unused_dependencies.push(dep_name.clone());
744 } else if used_features.len() == features.len() {
745 used_dependencies.push(dep_name.clone());
746 } else {
747 let unused_features: Vec<String> = features
748 .iter()
749 .filter(|f| !used_items.contains(*f))
750 .cloned()
751 .collect();
752
753 partially_used.push(PartiallyUsedDependency {
754 dependency: dep_name.clone(),
755 used_features: used_features.clone(),
756 total_features: features.len(),
757 unused_features,
758 });
759 }
760 }
761
762 let estimated_savings =
763 (unused_dependencies.len() * 20000) as u64 + (partially_used.len() * 5000) as u64; TreeShakingAnalysis {
767 total_dependencies,
768 used_dependencies,
769 unused_dependencies,
770 partially_used,
771 estimated_savings,
772 }
773 }
774
775 pub fn get_recommendations(analysis: &DeadCodeAnalysis) -> Vec<String> {
777 let mut recommendations = Vec::new();
778
779 if !analysis.unused_functions.is_empty() {
780 recommendations.push(format!(
781 "Remove {} unused functions to save ~{:.1} KB",
782 analysis.unused_functions.len(),
783 (analysis.unused_functions.len() * 100) as f64 / 1024.0
784 ));
785 }
786
787 if !analysis.unused_imports.is_empty() {
788 recommendations.push(format!(
789 "Remove {} unused imports",
790 analysis.unused_imports.len()
791 ));
792 }
793
794 if !analysis.unused_dependencies.is_empty() {
795 recommendations.push(format!(
796 "Remove {} unused dependencies from Cargo.toml",
797 analysis.unused_dependencies.len()
798 ));
799 }
800
801 if analysis.potential_size_reduction > 1024 {
802 recommendations.push(format!(
803 "Total potential size reduction: {:.2} KB",
804 analysis.potential_size_reduction as f64 / 1024.0
805 ));
806 }
807
808 recommendations.push(
809 "Run wasm-opt with dead code elimination passes for maximum optimization".to_string()
810 );
811
812 recommendations
813 }
814}
815
816#[cfg(test)]
817mod tests {
818 use super::*;
819
820 #[test]
821 fn test_dead_code_analysis() {
822 let code = r#"
823fn used_function() {
824 println!("I am used");
825}
826
827fn unused_function() {
828 println!("I am never called");
829}
830
831fn main() {
832 used_function();
833}
834"#;
835
836 let analysis = DeadCodeEliminator::analyze_rust_code(code);
837
838 assert!(analysis.unused_functions.len() > 0);
839 assert!(analysis.unused_functions.iter().any(|f| f.name == "unused_function"));
840 }
841
842 #[test]
843 fn test_wasm_opt_passes() {
844 let passes = WasmOptPass::all_passes();
845 assert!(passes.len() > 10);
846
847 let dce = passes.iter().find(|p| p.name == "dce");
848 assert!(dce.is_some());
849 }
850
851 #[test]
852 fn test_wasm_opt_command_generation() {
853 let cmd = DeadCodeEliminator::generate_wasm_opt_command(
854 "input.wasm",
855 "output.wasm",
856 4,
857 );
858
859 assert!(cmd.contains("wasm-opt"));
860 assert!(cmd.contains("-O4"));
861 assert!(cmd.contains("--dce"));
862 assert!(cmd.contains("input.wasm"));
863 }
864
865 #[test]
866 fn test_tree_shaking_analysis() {
867 let mut deps = HashMap::new();
868 deps.insert("serde".to_string(), vec!["Serialize".to_string(), "Deserialize".to_string()]);
869 deps.insert("unused_crate".to_string(), vec!["Function1".to_string()]);
870
871 let mut used_items = HashSet::new();
872 used_items.insert("Serialize".to_string());
873
874 let analysis = DeadCodeEliminator::analyze_tree_shaking(&deps, &used_items);
875
876 assert_eq!(analysis.total_dependencies, 2);
877 assert!(analysis.unused_dependencies.contains(&"unused_crate".to_string()));
878 assert_eq!(analysis.partially_used.len(), 1);
879 }
880
881 #[test]
882 fn test_size_optimization_passes() {
883 let passes = WasmOptPass::size_optimization_passes();
884
885 assert!(passes.contains(&"dce".to_string()));
886 assert!(passes.contains(&"strip-debug".to_string()));
887 assert!(passes.contains(&"vacuum".to_string()));
888 }
889}