cargo-treemap 0.1.0

Interactive treemap analyzer for Rust binary size and dependency API usage — like webpack-bundle-analyzer, for cargo.
//! Small shared helpers: byte formatting and Rust-path tokenizing.

use std::collections::HashSet;

/// Human-readable byte count (binary units).
pub fn human_bytes(n: u64) -> String {
    const U: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
    let mut v = n as f64;
    let mut i = 0;
    while v >= 1024.0 && i < U.len() - 1 {
        v /= 1024.0;
        i += 1;
    }
    if i == 0 {
        format!("{n} B")
    } else {
        format!("{v:.1} {}", U[i])
    }
}

/// Human-readable signed byte delta, e.g. `+1.5 KiB` / `-512 B`.
pub fn signed_bytes(n: i64) -> String {
    let sign = if n >= 0 { "+" } else { "-" };
    format!("{sign}{}", human_bytes(n.unsigned_abs()))
}

/// Split a Rust path on `::`, ignoring separators nested inside `<>`, `()` or `[]`.
pub fn split_path(s: &str) -> Vec<String> {
    let mut out = Vec::new();
    let mut depth: i32 = 0;
    let mut cur = String::new();
    let mut chars = s.chars().peekable();
    while let Some(c) = chars.next() {
        match c {
            '<' | '(' | '[' => {
                depth += 1;
                cur.push(c);
            }
            '>' | ')' | ']' => {
                depth -= 1;
                cur.push(c);
            }
            ':' if depth == 0 && chars.peek() == Some(&':') => {
                chars.next();
                out.push(std::mem::take(&mut cur));
            }
            _ => cur.push(c),
        }
    }
    if !cur.is_empty() {
        out.push(cur);
    }
    out.into_iter().filter(|s| !s.is_empty()).collect()
}

/// The leading `ident`-shaped token of a segment, skipping leading `<`, `&`, `*`, spaces.
/// `<serde_json::Value as core::fmt::Debug>` -> `serde_json`.
pub fn leading_ident(s: &str) -> &str {
    let start = s
        .char_indices()
        .find(|(_, c)| c.is_alphanumeric() || *c == '_')
        .map(|(i, _)| i)
        .unwrap_or(s.len());
    let rest = &s[start..];
    let end = rest
        .char_indices()
        .find(|(_, c)| !(c.is_alphanumeric() || *c == '_'))
        .map(|(i, _)| i)
        .unwrap_or(rest.len());
    &rest[..end]
}

/// Strip generic/argument tails for grouping monomorphizations: `foo::<u32>` -> `foo`.
pub fn strip_generics(s: &str) -> String {
    let end = s.find(['<', '(']).unwrap_or(s.len());
    let out = s[..end].trim_end_matches(':').trim();
    if out.is_empty() { s.to_string() } else { out.to_string() }
}

/// Rust primitives/keywords that can appear as the leading token of an impl symbol
/// (`<str as ...>`) but are never crate names, filtered when deriving a crate set from a
/// bare binary's symbols.
pub fn is_primitive_or_keyword(s: &str) -> bool {
    matches!(
        s,
        "str" | "char" | "bool" | "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8"
            | "i16" | "i32" | "i64" | "i128" | "isize" | "f16" | "f32" | "f64" | "f128"
            | "as" | "dyn" | "impl" | "fn" | "mut" | "ref" | "self" | "Self" | "crate"
            | "super" | "move" | "unsafe" | "extern"
    )
}

/// Crates that make up the standard distribution; collapsed under a single `std` bucket
/// unless `--split-std` is given.
pub fn std_crates() -> HashSet<&'static str> {
    // Internal std crates, plus the crates std vendors for backtrace support. These collapse
    // into `std` ONLY when they aren't also a real direct dependency, the caller guards the
    // collapse with the resolved crate set, so a user's own `object`/`gimli`/etc. is kept.
    [
        "core",
        "alloc",
        "std",
        "panic_abort",
        "panic_unwind",
        "compiler_builtins",
        "unwind",
        "proc_macro",
        "test",
        "std_detect",
        "rustc_std_workspace_core",
        "rustc_std_workspace_alloc",
        "rustc_std_workspace_std",
        // std-vendored (private copies), collapsed unless the user depends on them directly.
        "gimli",
        "addr2line",
        "object",
        "miniz_oxide",
        "hashbrown",
        "rustc_demangle",
        "backtrace",
    ]
    .into_iter()
    .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn human_bytes_units() {
        assert_eq!(human_bytes(0), "0 B");
        assert_eq!(human_bytes(512), "512 B");
        assert_eq!(human_bytes(1024), "1.0 KiB");
        assert_eq!(human_bytes(1536), "1.5 KiB");
        assert_eq!(human_bytes(1024 * 1024), "1.0 MiB");
        assert_eq!(human_bytes(3 * 1024 * 1024 * 1024), "3.0 GiB");
    }

    #[test]
    fn split_path_respects_generics() {
        assert_eq!(split_path("a::b::c"), ["a", "b", "c"]);
        // `::` inside <> must not split; the turbofish `::` at depth 0 does.
        assert_eq!(
            split_path("serde_json::de::from_str::<alloc::string::String>"),
            ["serde_json", "de", "from_str", "<alloc::string::String>"]
        );
        // impl block stays one segment, then the method
        assert_eq!(
            split_path("<str as core::fmt::Display>::fmt"),
            ["<str as core::fmt::Display>", "fmt"]
        );
        assert!(split_path("").is_empty());
    }

    #[test]
    fn leading_ident_skips_symbols() {
        assert_eq!(leading_ident("regex_automata::dfa"), "regex_automata");
        assert_eq!(leading_ident("<serde_json::Value as X>"), "serde_json");
        // grabs the first token; `&mut` yields the keyword
        assert_eq!(leading_ident("&mut alloc::vec::Vec"), "mut");
        assert_eq!(leading_ident("*const u8"), "const");
    }

    #[test]
    fn strip_generics_cuts_at_bracket() {
        // cuts at the first `<`/`(` and trims a trailing `::`
        assert_eq!(strip_generics("foo::<u32>"), "foo");
        assert_eq!(strip_generics("plain_name"), "plain_name");
        assert_eq!(strip_generics("call(args)"), "call");
        // never returns empty
        assert_eq!(strip_generics("<impl X>"), "<impl X>");
    }

    #[test]
    fn primitives_and_std_recognized() {
        assert!(is_primitive_or_keyword("u32"));
        assert!(is_primitive_or_keyword("str"));
        assert!(is_primitive_or_keyword("dyn"));
        assert!(!is_primitive_or_keyword("serde"));
        let std = std_crates();
        assert!(std.contains("core"));
        assert!(std.contains("std"));
        assert!(!std.contains("serde"));
    }
}