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}