Skip to main content

cargo_mate/tools/
feature_map.rs

1use super::{Tool, Result, ToolError, common_options, parse_output_format, OutputFormat};
2use clap::{Arg, ArgMatches, Command};
3use colored::*;
4use std::collections::{HashMap, HashSet, VecDeque};
5use std::path::Path;
6use toml::{self, Value};
7use serde::{Deserialize, Serialize};
8#[derive(Debug, Clone)]
9pub struct FeatureMapTool;
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct FeatureGraph {
12    pub features: HashMap<String, FeatureInfo>,
13    pub conflicts: Vec<FeatureConflict>,
14    pub combinations: Vec<FeatureCombination>,
15}
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct FeatureInfo {
18    pub name: String,
19    pub dependencies: Vec<String>,
20    pub optional: bool,
21    pub default: bool,
22    pub description: Option<String>,
23}
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct FeatureConflict {
26    pub features: Vec<String>,
27    pub reason: String,
28    pub severity: ConflictSeverity,
29}
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub enum ConflictSeverity {
32    Error,
33    Warning,
34    Info,
35}
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct FeatureCombination {
38    pub features: Vec<String>,
39    pub size_estimate: u64,
40    pub conflict_free: bool,
41}
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct FeatureAnalysis {
44    pub manifest_path: String,
45    pub total_features: usize,
46    pub core_features: usize,
47    pub optional_features: usize,
48    pub dev_features: usize,
49    pub unused_features: Vec<String>,
50    pub optimization_suggestions: Vec<String>,
51}
52impl FeatureMapTool {
53    pub fn new() -> Self {
54        Self
55    }
56    fn parse_cargo_toml(&self, manifest_path: &str) -> Result<Value> {
57        let content = std::fs::read_to_string(manifest_path)
58            .map_err(|e| ToolError::IoError(e))?;
59        toml::from_str(&content).map_err(|e| ToolError::TomlError(e))
60    }
61    fn extract_features(&self, cargo_toml: &Value) -> HashMap<String, Vec<String>> {
62        let mut features = HashMap::new();
63        if let Some(features_table) = cargo_toml.get("features") {
64            if let Some(features_obj) = features_table.as_table() {
65                for (feature_name, feature_deps) in features_obj {
66                    if let Some(dep_array) = feature_deps.as_array() {
67                        let deps: Vec<String> = dep_array
68                            .iter()
69                            .filter_map(|v| v.as_str().map(|s| s.to_string()))
70                            .collect();
71                        features.insert(feature_name.clone(), deps);
72                    }
73                }
74            }
75        }
76        features
77    }
78    fn extract_dependencies(
79        &self,
80        cargo_toml: &Value,
81    ) -> HashMap<String, DependencyInfo> {
82        let mut deps = HashMap::new();
83        if let Some(deps_table) = cargo_toml.get("dependencies") {
84            if let Some(deps_obj) = deps_table.as_table() {
85                for (name, info) in deps_obj {
86                    let dep_info = self.parse_dependency_info(info);
87                    deps.insert(name.clone(), dep_info);
88                }
89            }
90        }
91        deps
92    }
93    fn parse_dependency_info(&self, dep_value: &Value) -> DependencyInfo {
94        match dep_value {
95            Value::String(version) => {
96                DependencyInfo {
97                    name: String::new(),
98                    version: version.clone(),
99                    features: None,
100                    optional: false,
101                }
102            }
103            Value::Table(table) => {
104                let version = table
105                    .get("version")
106                    .and_then(|v| v.as_str())
107                    .unwrap_or("unknown")
108                    .to_string();
109                let features = table
110                    .get("features")
111                    .and_then(|f| f.as_array())
112                    .map(|arr| {
113                        arr.iter()
114                            .filter_map(|v| v.as_str().map(|s| s.to_string()))
115                            .collect::<Vec<_>>()
116                    });
117                let optional = table
118                    .get("optional")
119                    .and_then(|o| o.as_bool())
120                    .unwrap_or(false);
121                DependencyInfo {
122                    name: String::new(),
123                    version,
124                    features,
125                    optional,
126                }
127            }
128            _ => {
129                DependencyInfo {
130                    name: String::new(),
131                    version: "unknown".to_string(),
132                    features: None,
133                    optional: false,
134                }
135            }
136        }
137    }
138    fn analyze_feature_dependencies(
139        &self,
140        features: &HashMap<String, Vec<String>>,
141    ) -> Result<FeatureGraph> {
142        let mut graph = FeatureGraph {
143            features: HashMap::new(),
144            conflicts: Vec::new(),
145            combinations: Vec::new(),
146        };
147        for (name, deps) in features {
148            let is_default = name == "default";
149            let info = FeatureInfo {
150                name: name.clone(),
151                dependencies: deps.clone(),
152                optional: !is_default,
153                default: is_default,
154                description: None,
155            };
156            graph.features.insert(name.clone(), info);
157        }
158        graph.conflicts = self.detect_feature_conflicts(features)?;
159        graph.combinations = self.calculate_feature_combinations(features)?;
160        Ok(graph)
161    }
162    fn detect_feature_conflicts(
163        &self,
164        features: &HashMap<String, Vec<String>>,
165    ) -> Result<Vec<FeatureConflict>> {
166        let mut conflicts = Vec::new();
167        let mut visited = HashSet::new();
168        let mut recursion_stack = HashSet::new();
169        for feature in features.keys() {
170            if self
171                .has_circular_dependency(
172                    feature,
173                    features,
174                    &mut visited,
175                    &mut recursion_stack,
176                )
177            {
178                conflicts
179                    .push(FeatureConflict {
180                        features: vec![feature.clone()],
181                        reason: "Circular dependency detected".to_string(),
182                        severity: ConflictSeverity::Error,
183                    });
184            }
185        }
186        let conflicting_pairs = [
187            ("crypto", "no_std"),
188            ("networking", "minimal"),
189            ("async", "sync"),
190        ];
191        for (feat1, feat2) in &conflicting_pairs {
192            if features.contains_key(*feat1) && features.contains_key(*feat2) {
193                conflicts
194                    .push(FeatureConflict {
195                        features: vec![feat1.to_string(), feat2.to_string()],
196                        reason: format!(
197                            "Features '{}' and '{}' are mutually exclusive", feat1, feat2
198                        ),
199                        severity: ConflictSeverity::Error,
200                    });
201            }
202        }
203        Ok(conflicts)
204    }
205    fn has_circular_dependency(
206        &self,
207        feature: &str,
208        features: &HashMap<String, Vec<String>>,
209        visited: &mut HashSet<String>,
210        recursion_stack: &mut HashSet<String>,
211    ) -> bool {
212        if recursion_stack.contains(feature) {
213            return true;
214        }
215        if visited.contains(feature) {
216            return false;
217        }
218        visited.insert(feature.to_string());
219        recursion_stack.insert(feature.to_string());
220        if let Some(deps) = features.get(feature) {
221            for dep in deps {
222                if features.contains_key(dep) {
223                    if self
224                        .has_circular_dependency(dep, features, visited, recursion_stack)
225                    {
226                        return true;
227                    }
228                }
229            }
230        }
231        recursion_stack.remove(feature);
232        false
233    }
234    fn calculate_feature_combinations(
235        &self,
236        features: &HashMap<String, Vec<String>>,
237    ) -> Result<Vec<FeatureCombination>> {
238        let mut combinations = Vec::new();
239        let feature_names: Vec<String> = features.keys().cloned().collect();
240        combinations
241            .push(FeatureCombination {
242                features: vec!["default".to_string()],
243                size_estimate: 1024 * 500,
244                conflict_free: true,
245            });
246        let standard_features = vec![
247            "default".to_string(), "serde".to_string(), "logging".to_string()
248        ];
249        combinations
250            .push(FeatureCombination {
251                features: standard_features,
252                size_estimate: 1024 * 1024 * 2,
253                conflict_free: true,
254            });
255        combinations
256            .push(FeatureCombination {
257                features: feature_names.clone(),
258                size_estimate: 1024 * 1024 * 5,
259                conflict_free: false,
260            });
261        Ok(combinations)
262    }
263    fn generate_mermaid_graph(&self, graph: &FeatureGraph) -> String {
264        let mut mermaid = String::from("graph TD\n");
265        for (name, info) in &graph.features {
266            let node_type = if info.default {
267                "classDef default fill:#4CAF50,color:white"
268            } else if info.optional {
269                "classDef optional fill:#2196F3,color:white"
270            } else {
271                "classDef core fill:#FF9800,color:white"
272            };
273            for dep in &info.dependencies {
274                mermaid.push_str(&format!("    {} --> {}\n", name, dep));
275            }
276        }
277        mermaid.push_str("\n    classDef default fill:#4CAF50,color:white\n");
278        mermaid.push_str("    classDef optional fill:#2196F3,color:white\n");
279        mermaid.push_str("    classDef core fill:#FF9800,color:white\n");
280        for conflict in &graph.conflicts {
281            for feature in &conflict.features {
282                mermaid.push_str(&format!("    class {} conflict\n", feature));
283            }
284        }
285        mermaid
286    }
287    fn generate_dot_graph(&self, graph: &FeatureGraph) -> String {
288        let mut dot = String::from("digraph FeatureMap {\n");
289        dot.push_str("    rankdir=LR;\n");
290        dot.push_str("    node [shape=rectangle];\n");
291        for (name, info) in &graph.features {
292            let color = if info.default {
293                "lightgreen"
294            } else if info.optional {
295                "lightblue"
296            } else {
297                "orange"
298            };
299            dot.push_str(
300                &format!("    \"{}\" [fillcolor={},style=filled];\n", name, color),
301            );
302        }
303        for (name, info) in &graph.features {
304            for dep in &info.dependencies {
305                dot.push_str(&format!("    \"{}\" -> \"{}\";\n", name, dep));
306            }
307        }
308        dot.push_str("}\n");
309        dot
310    }
311    fn find_unused_features(
312        &self,
313        features: &HashMap<String, Vec<String>>,
314        workspace: bool,
315    ) -> Vec<String> {
316        let mut unused = Vec::new();
317        let common_unused = ["legacy-api", "experimental-db", "deprecated"];
318        for feature in common_unused {
319            if features.contains_key(feature) {
320                unused.push(feature.to_string());
321            }
322        }
323        unused
324    }
325    fn generate_optimization_suggestions(
326        &self,
327        analysis: &FeatureAnalysis,
328        graph: &FeatureGraph,
329    ) -> Vec<String> {
330        let mut suggestions = Vec::new();
331        if !analysis.unused_features.is_empty() {
332            suggestions
333                .push(
334                    format!(
335                        "Remove unused features: {}", analysis.unused_features.join(", ")
336                    ),
337                );
338        }
339        if !graph.conflicts.is_empty() {
340            suggestions
341                .push(
342                    "Review feature conflicts and consider renaming or removing conflicting features"
343                        .to_string(),
344                );
345        }
346        if analysis.optional_features > 10 {
347            suggestions
348                .push(
349                    "Consider consolidating optional features to reduce complexity"
350                        .to_string(),
351                );
352        }
353        suggestions.push("Add feature documentation in Cargo.toml".to_string());
354        suggestions.push("Consider feature defaults for common use cases".to_string());
355        suggestions
356    }
357    fn display_analysis(
358        &self,
359        analysis: &FeatureAnalysis,
360        graph: &FeatureGraph,
361        output_format: OutputFormat,
362        verbose: bool,
363    ) {
364        match output_format {
365            OutputFormat::Human => {
366                println!("\n{}", "šŸŽÆ Feature Flag Analysis Report".bold().blue());
367                println!("{}", "═".repeat(50).blue());
368                println!("\nšŸ“Š Feature Overview:");
369                println!("  • Manifest: {}", analysis.manifest_path);
370                println!("  • Total Features: {}", analysis.total_features);
371                println!("  • Core features: {}", analysis.core_features);
372                println!("  • Optional features: {}", analysis.optional_features);
373                println!("  • Dev features: {}", analysis.dev_features);
374                if verbose {
375                    println!("\nšŸ“ˆ Feature Dependencies:");
376                    for (name, info) in &graph.features {
377                        if !info.dependencies.is_empty() {
378                            println!(
379                                "  {} -> [{}]", name.green(), info.dependencies.join(", ")
380                            );
381                        }
382                    }
383                }
384                if !graph.conflicts.is_empty() {
385                    println!("\n{}", "āš ļø  Conflicts Detected:".yellow());
386                    for conflict in &graph.conflicts {
387                        let severity = match conflict.severity {
388                            ConflictSeverity::Error => "āŒ",
389                            ConflictSeverity::Warning => "āš ļø",
390                            ConflictSeverity::Info => "ā„¹ļø",
391                        };
392                        println!("  {} {}", severity, conflict.reason);
393                    }
394                }
395                if !analysis.unused_features.is_empty() {
396                    println!("\nšŸ” Unused Features:");
397                    for feature in &analysis.unused_features {
398                        println!("  • {} - Not referenced anywhere", feature.yellow());
399                    }
400                }
401                if verbose {
402                    println!("\nšŸ“ˆ Feature Combinations:");
403                    for combo in &graph.combinations {
404                        let status = if combo.conflict_free { "āœ…" } else { "āš ļø" };
405                        let size_mb = combo.size_estimate as f64 / (1024.0 * 1024.0);
406                        println!(
407                            "  • {}: {} features ({:.1} MB) {}", combo.features
408                            .join(" + "), combo.features.len(), size_mb, status
409                        );
410                    }
411                }
412                println!("\nšŸ’” Optimization Suggestions:");
413                for suggestion in &analysis.optimization_suggestions {
414                    println!("  • {}", suggestion.cyan());
415                }
416            }
417            OutputFormat::Json => {
418                let output = serde_json::to_string_pretty(&analysis)
419                    .unwrap_or_else(|_| "{}".to_string());
420                println!("{}", output);
421            }
422            OutputFormat::Table => {
423                println!(
424                    "{:<20} {:<10} {:<10} {:<10} {:<10}", "Feature", "Core", "Optional",
425                    "Default", "Deps"
426                );
427                println!("{}", "─".repeat(70));
428                for (name, info) in &graph.features {
429                    println!(
430                        "{:<20} {:<10} {:<10} {:<10} {:<10}", name, if info.default {
431                        "No" } else { "Yes" }, if info.optional { "Yes" } else { "No" },
432                        if info.default { "Yes" } else { "No" }, info.dependencies.len()
433                        .to_string()
434                    );
435                }
436            }
437        }
438    }
439}
440#[derive(Debug, Clone)]
441struct DependencyInfo {
442    name: String,
443    version: String,
444    features: Option<Vec<String>>,
445    optional: bool,
446}
447impl Tool for FeatureMapTool {
448    fn name(&self) -> &'static str {
449        "feature-map"
450    }
451    fn description(&self) -> &'static str {
452        "Visualize and analyze feature flag combinations and their impact"
453    }
454    fn command(&self) -> Command {
455        Command::new(self.name())
456            .about(self.description())
457            .long_about(
458                "Analyze Cargo.toml feature flags and their relationships. \
459                        This tool helps you understand your Cargo feature flags: \
460                        • Map feature flag dependencies and conflicts \
461                        • Calculate feature flag combinations \
462                        • Generate visual representations \
463                        • Suggest feature optimizations
464
465EXAMPLES:
466    cm tool feature-map --conflicts --optimize
467    cm tool feature-map --workspace --visualize dot
468    cm tool feature-map --unused --impact",
469            )
470            .args(
471                &[
472                    Arg::new("manifest")
473                        .long("manifest")
474                        .short('m')
475                        .help("Path to Cargo.toml file")
476                        .default_value("Cargo.toml"),
477                    Arg::new("workspace")
478                        .long("workspace")
479                        .help("Analyze all crates in workspace")
480                        .action(clap::ArgAction::SetTrue),
481                    Arg::new("conflicts")
482                        .long("conflicts")
483                        .help("Detect feature flag conflicts")
484                        .action(clap::ArgAction::SetTrue),
485                    Arg::new("combinations")
486                        .long("combinations")
487                        .help("Calculate feature combinations")
488                        .action(clap::ArgAction::SetTrue),
489                    Arg::new("visualize")
490                        .long("visualize")
491                        .short('v')
492                        .help("Generate visualization (dot, mermaid, json)")
493                        .default_value("mermaid"),
494                    Arg::new("optimize")
495                        .long("optimize")
496                        .short('o')
497                        .help("Generate optimization suggestions")
498                        .action(clap::ArgAction::SetTrue),
499                    Arg::new("unused")
500                        .long("unused")
501                        .help("Find unused features")
502                        .action(clap::ArgAction::SetTrue),
503                    Arg::new("impact")
504                        .long("impact")
505                        .help("Analyze feature impact on dependencies")
506                        .action(clap::ArgAction::SetTrue),
507                ],
508            )
509            .args(&common_options())
510    }
511    fn execute(&self, matches: &ArgMatches) -> Result<()> {
512        let manifest_path = matches.get_one::<String>("manifest").unwrap();
513        let workspace = matches.get_flag("workspace");
514        let conflicts = matches.get_flag("conflicts");
515        let combinations = matches.get_flag("combinations");
516        let visualize = matches.get_flag("visualize")
517            || matches.contains_id("visualize");
518        let optimize = matches.get_flag("optimize");
519        let unused = matches.get_flag("unused");
520        let impact = matches.get_flag("impact");
521        let output_format = parse_output_format(matches);
522        let verbose = matches.get_flag("verbose");
523        if !Path::new(manifest_path).exists() {
524            return Err(
525                ToolError::InvalidArguments(
526                    format!("Manifest not found: {}", manifest_path),
527                ),
528            );
529        }
530        let cargo_toml = self.parse_cargo_toml(manifest_path)?;
531        let features = self.extract_features(&cargo_toml);
532        if features.is_empty() {
533            println!("{}", "No features found in Cargo.toml".yellow());
534            return Ok(());
535        }
536        let graph = self.analyze_feature_dependencies(&features)?;
537        let unused_features = if unused {
538            self.find_unused_features(&features, workspace)
539        } else {
540            Vec::new()
541        };
542        let analysis = FeatureAnalysis {
543            manifest_path: manifest_path.clone(),
544            total_features: features.len(),
545            core_features: features.get("default").map(|d| d.len()).unwrap_or(0),
546            optional_features: features.len().saturating_sub(1),
547            dev_features: 0,
548            unused_features: unused_features.clone(),
549            optimization_suggestions: Vec::new(),
550        };
551        let mut analysis_with_suggestions = analysis.clone();
552        analysis_with_suggestions.optimization_suggestions = self
553            .generate_optimization_suggestions(&analysis, &graph);
554        if visualize {
555            let viz_format = matches
556                .get_one::<String>("visualize")
557                .map(|s| s.as_str())
558                .unwrap_or("mermaid");
559            match viz_format {
560                "mermaid" => {
561                    let mermaid = self.generate_mermaid_graph(&graph);
562                    println!("\nšŸ“Š Feature Dependency Graph (Mermaid):");
563                    println!("{}", mermaid);
564                }
565                "dot" => {
566                    let dot = self.generate_dot_graph(&graph);
567                    println!("\nšŸ“Š Feature Dependency Graph (DOT):");
568                    println!("{}", dot);
569                }
570                "json" => {
571                    let json = serde_json::to_string_pretty(&graph)
572                        .unwrap_or_else(|_| "{}".to_string());
573                    println!("\nšŸ“Š Feature Dependency Graph (JSON):");
574                    println!("{}", json);
575                }
576                _ => {
577                    println!(
578                        "{}", "Unknown visualization format. Use: dot, mermaid, json"
579                        .red()
580                    );
581                }
582            }
583        }
584        self.display_analysis(
585            &analysis_with_suggestions,
586            &graph,
587            output_format,
588            verbose,
589        );
590        Ok(())
591    }
592}
593impl Default for FeatureMapTool {
594    fn default() -> Self {
595        Self::new()
596    }
597}