cargo-perspective 0.1.0

A utility that graphically shows the crate dependency distribution within a project.
Documentation
//! Core data types for dependency analysis and visualization.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Represents a method or function call within a crate.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MethodCall {
    /// Full path to the method (e.g., "tokio::io::AsyncReadExt::read")
    pub path: String,
    /// Number of times this method is called
    pub call_count: usize,
    /// Average execution time in nanoseconds (optional, for v0.2+)
    pub avg_time_ns: Option<u64>,
    /// Source locations where this method is called
    pub call_sites: Vec<CallSite>,
}

/// Represents a location where a method is called.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CallSite {
    pub file_path: String,
    pub line: usize,
    pub column: usize,
}

/// Represents a crate with all its method calls.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CrateUsage {
    /// Name of the crate
    pub name: String,
    /// Whether this is a direct or transitive dependency
    pub is_direct: bool,
    /// Total number of calls to this crate
    pub total_calls: usize,
    /// Map of method path -> method call data
    pub methods: HashMap<String, MethodCall>,
}

impl CrateUsage {
    pub fn new(name: String, is_direct: bool) -> Self {
        Self {
            name,
            is_direct,
            total_calls: 0,
            methods: HashMap::new(),
        }
    }

    /// Add a method call to this crate.
    pub fn add_call(&mut self, method_path: String, call_site: CallSite) {
        self.total_calls += 1;

        let method = self.methods.entry(method_path.clone()).or_insert_with(|| {
            MethodCall {
                path: method_path,
                call_count: 0,
                avg_time_ns: None,
                call_sites: Vec::new(),
            }
        });

        method.call_count += 1;
        method.call_sites.push(call_site);
    }
}

/// Complete analysis results for a Rust project.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProjectAnalysis {
    /// Name of the root crate
    pub root_crate: String,
    /// Map of crate name -> crate usage
    pub crates: HashMap<String, CrateUsage>,
    /// Total number of dependency calls across the project
    pub total_calls: usize,
}

impl ProjectAnalysis {
    pub fn new(root_crate: String) -> Self {
        Self {
            root_crate,
            crates: HashMap::new(),
            total_calls: 0,
        }
    }

    /// Get or create a crate usage entry.
    pub fn get_or_create_crate(&mut self, name: String, is_direct: bool) -> &mut CrateUsage {
        self.crates
            .entry(name.clone())
            .or_insert_with(|| CrateUsage::new(name, is_direct))
    }

    /// Calculate total calls across all crates.
    pub fn recalculate_totals(&mut self) {
        self.total_calls = self.crates.values().map(|c| c.total_calls).sum();
    }
}

/// Rectangle for TreeMap layout.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TreeMapRect {
    pub x: f64,
    pub y: f64,
    pub width: f64,
    pub height: f64,
}

impl TreeMapRect {
    pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
        Self { x, y, width, height }
    }

    pub fn area(&self) -> f64 {
        self.width * self.height
    }

    pub fn aspect_ratio(&self) -> f64 {
        let min = self.width.min(self.height);
        let max = self.width.max(self.height);
        if min == 0.0 {
            f64::INFINITY
        } else {
            max / min
        }
    }
}

/// A node in the TreeMap representing either a crate or a method.
#[derive(Debug, Clone, PartialEq)]
pub struct TreeMapNode {
    /// Label for this node
    pub label: String,
    /// Size value (call count)
    pub value: usize,
    /// Optional performance metric (execution time)
    pub time_ns: Option<u64>,
    /// Child nodes (for crates containing methods)
    pub children: Vec<TreeMapNode>,
    /// Layout rectangle (computed during layout)
    pub rect: Option<TreeMapRect>,
}

impl TreeMapNode {
    pub fn new_crate(name: String, value: usize, children: Vec<TreeMapNode>) -> Self {
        Self {
            label: name,
            value,
            time_ns: None,
            children,
            rect: None,
        }
    }

    pub fn new_method(path: String, call_count: usize, time_ns: Option<u64>) -> Self {
        Self {
            label: path,
            value: call_count,
            time_ns,
            children: Vec::new(),
            rect: None,
        }
    }

    /// Convert ProjectAnalysis to TreeMap structure.
    pub fn from_analysis(analysis: &ProjectAnalysis) -> Vec<TreeMapNode> {
        let mut nodes = Vec::new();

        for (crate_name, crate_usage) in &analysis.crates {
            let method_nodes: Vec<TreeMapNode> = crate_usage
                .methods
                .values()
                .map(|method| {
                    TreeMapNode::new_method(
                        method.path.clone(),
                        method.call_count,
                        method.avg_time_ns,
                    )
                })
                .collect();

            nodes.push(TreeMapNode::new_crate(
                crate_name.clone(),
                crate_usage.total_calls,
                method_nodes,
            ));
        }

        // Sort by value descending for better visualization
        nodes.sort_by(|a, b| b.value.cmp(&a.value));
        nodes
    }
}