Skip to main content

portalis_transpiler/
dead_code_eliminator.rs

1//! Dead Code Eliminator - Analyzes and removes unused code for WASM optimization
2//!
3//! This module provides:
4//! 1. Dead code detection in generated Rust code
5//! 2. wasm-opt advanced optimization passes
6//! 3. Tree-shaking analysis for dependencies
7//! 4. Unused function and type detection
8//! 5. Optimization reporting and recommendations
9
10use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, HashSet};
12
13/// Dead code analysis result
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct DeadCodeAnalysis {
16    /// Unused functions
17    pub unused_functions: Vec<UnusedItem>,
18    /// Unused types/structs
19    pub unused_types: Vec<UnusedItem>,
20    /// Unused imports
21    pub unused_imports: Vec<String>,
22    /// Unused dependencies
23    pub unused_dependencies: Vec<String>,
24    /// Total potential size reduction (bytes)
25    pub potential_size_reduction: u64,
26}
27
28impl DeadCodeAnalysis {
29    /// Calculate total items found
30    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    /// Generate detailed report
38    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/// Represents an unused code item
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct UnusedItem {
97    /// Item name
98    pub name: String,
99    /// Item type (function, struct, etc.)
100    pub item_type: String,
101    /// Source file
102    pub file: String,
103    /// Line number
104    pub line: usize,
105    /// Estimated size in bytes
106    pub estimated_size: u64,
107    /// Reason for being unused
108    pub reason: String,
109}
110
111/// wasm-opt optimization pass
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct WasmOptPass {
114    /// Pass name
115    pub name: String,
116    /// Description
117    pub description: String,
118    /// Estimated impact (0.0-1.0)
119    pub impact: f64,
120    /// Enabled by default
121    pub default: bool,
122}
123
124impl WasmOptPass {
125    /// Get all available optimization passes
126    pub fn all_passes() -> Vec<Self> {
127        vec![
128            // Dead code elimination
129            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            // Inlining
154            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            // Code optimization
167            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            // Size reduction
192            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            // Advanced optimization
217            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    /// Get passes for aggressive size optimization
239    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    /// Get passes for performance optimization
260    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/// Tree-shaking analysis for dependencies
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct TreeShakingAnalysis {
275    /// Total dependencies
276    pub total_dependencies: usize,
277    /// Used dependencies
278    pub used_dependencies: Vec<String>,
279    /// Unused dependencies (can be removed)
280    pub unused_dependencies: Vec<String>,
281    /// Partially used dependencies
282    pub partially_used: Vec<PartiallyUsedDependency>,
283    /// Estimated size savings from tree-shaking
284    pub estimated_savings: u64,
285}
286
287impl TreeShakingAnalysis {
288    /// Generate report
289    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/// Partially used dependency information
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct PartiallyUsedDependency {
325    /// Dependency name
326    pub dependency: String,
327    /// Used features
328    pub used_features: Vec<String>,
329    /// Total available features
330    pub total_features: usize,
331    /// Unused feature names
332    pub unused_features: Vec<String>,
333}
334
335/// Call graph for reachability analysis
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct CallGraph {
338    /// Function nodes
339    pub nodes: HashMap<String, CallGraphNode>,
340    /// Edges (caller -> callees)
341    pub edges: HashMap<String, Vec<String>>,
342    /// Entry points (main, public functions, exported)
343    pub entry_points: HashSet<String>,
344}
345
346impl CallGraph {
347    /// Create new call graph
348    pub fn new() -> Self {
349        Self {
350            nodes: HashMap::new(),
351            edges: HashMap::new(),
352            entry_points: HashSet::new(),
353        }
354    }
355
356    /// Add function node
357    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    /// Add function call edge
363    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    /// Mark function as entry point
368    pub fn add_entry_point(&mut self, name: String) {
369        self.entry_points.insert(name);
370    }
371
372    /// Compute reachable functions from entry points
373    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    /// Find unreachable functions
393    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/// Call graph node
404#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct CallGraphNode {
406    /// Function name
407    pub name: String,
408    /// Visibility (pub, pub(crate), private)
409    pub visibility: Visibility,
410    /// Is generic function
411    pub is_generic: bool,
412    /// Is async function
413    pub is_async: bool,
414    /// Has #[inline] attribute
415    pub is_inline: bool,
416    /// Line number
417    pub line: usize,
418}
419
420/// Function visibility
421#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
422pub enum Visibility {
423    Public,
424    PublicCrate,
425    Private,
426}
427
428/// Optimization strategy for dead code elimination
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub enum OptimizationStrategy {
431    /// Conservative - only remove clearly unused code
432    Conservative,
433    /// Moderate - remove unused code with high confidence
434    Moderate,
435    /// Aggressive - remove all unreachable code
436    Aggressive,
437}
438
439impl OptimizationStrategy {
440    /// Should remove private unused functions
441    pub fn remove_private_unused(&self) -> bool {
442        matches!(self, OptimizationStrategy::Moderate | OptimizationStrategy::Aggressive)
443    }
444
445    /// Should remove public unused functions (requires whole-program analysis)
446    pub fn remove_public_unused(&self) -> bool {
447        matches!(self, OptimizationStrategy::Aggressive)
448    }
449
450    /// Should inline small functions
451    pub fn inline_small_functions(&self) -> bool {
452        matches!(self, OptimizationStrategy::Moderate | OptimizationStrategy::Aggressive)
453    }
454}
455
456/// Dead code eliminator
457pub struct DeadCodeEliminator {
458    /// Optimization strategy
459    pub strategy: OptimizationStrategy,
460    /// Preserve exported functions
461    pub preserve_exports: bool,
462}
463
464impl Default for DeadCodeEliminator {
465    fn default() -> Self {
466        Self::new()
467    }
468}
469
470impl DeadCodeEliminator {
471    /// Create new eliminator with default strategy
472    pub fn new() -> Self {
473        Self {
474            strategy: OptimizationStrategy::Moderate,
475            preserve_exports: true,
476        }
477    }
478
479    /// Create with specific strategy
480    pub fn with_strategy(strategy: OptimizationStrategy) -> Self {
481        Self {
482            strategy,
483            preserve_exports: true,
484        }
485    }
486
487    /// Build call graph from Rust code
488    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        // Track current function for call edges
493        let mut current_function: Option<String> = None;
494
495        for (idx, line) in lines.iter().enumerate() {
496            let trimmed = line.trim();
497
498            // Skip comments
499            if trimmed.starts_with("//") || trimmed.starts_with("/*") {
500                continue;
501            }
502
503            // Find function definitions
504            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                    // Entry points: main, public functions, #[wasm_bindgen]
528                    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            // Find function calls
536            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            // End of function body
543            if trimmed == "}" && current_function.is_some() {
544                current_function = None;
545            }
546        }
547
548        graph
549    }
550
551    /// Extract function calls from a line
552    fn extract_function_calls(line: &str) -> Vec<String> {
553        let mut calls = Vec::new();
554        let line = line.trim();
555
556        // Simple pattern: identifier followed by (
557        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                // Check if it's not a keyword
565                if !Self::is_rust_keyword(&current_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    /// Check if word is a Rust keyword
578    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    /// Check for #[inline] attribute
585    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    /// Check for #[wasm_bindgen] attribute
594    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    /// Analyze Rust code for dead code using call graph
603    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                // Apply strategy
611                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    /// Find unused imports
649    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                    // Extract the actual item name
657                    let item_name = if let Some(pos) = import.rfind("::") {
658                        &import[pos + 2..]
659                    } else {
660                        &import
661                    };
662
663                    // Check if used (simple heuristic)
664                    let usage_count = code.matches(item_name).count();
665                    if usage_count <= 1 { // Only the import line
666                        unused.push(import);
667                    }
668                }
669            }
670        }
671
672        unused
673    }
674
675    /// Analyze Rust code for dead code (simplified static method)
676    pub fn analyze_rust_code(code: &str) -> DeadCodeAnalysis {
677        let eliminator = Self::new();
678        eliminator.analyze_with_call_graph(code)
679    }
680
681    /// Extract function name from definition line
682    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    /// Extract import name
697    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    /// Generate wasm-opt command with dead code elimination
708    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    /// Analyze tree-shaking opportunities
726    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 + // 20KB per unused dep
764            (partially_used.len() * 5000) as u64;        // 5KB per partially used
765
766        TreeShakingAnalysis {
767            total_dependencies,
768            used_dependencies,
769            unused_dependencies,
770            partially_used,
771            estimated_savings,
772        }
773    }
774
775    /// Get optimization recommendations
776    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}