Skip to content

Instantly share code, notes, and snippets.

@RPDevJesco
Created November 15, 2025 18:48
Show Gist options
  • Select an option

  • Save RPDevJesco/3b28bc25fab9fbaa87c61138d5a5dea4 to your computer and use it in GitHub Desktop.

Select an option

Save RPDevJesco/3b28bc25fab9fbaa87c61138d5a5dea4 to your computer and use it in GitHub Desktop.
Detailed summaries of your crates usage in your Rust project
#!/usr/bin/env pwsh
#Requires -Version 5.1
<#
.SYNOPSIS
Analyzes Rust crate dependencies and usage patterns
.DESCRIPTION
Provides comprehensive analysis of crate dependencies including:
- Tree-based counts
- Metadata-based dependency analysis
- Direct dependency usage in source code
- Transitive dependency usage detection
.PARAMETER SkipBuild
Skip the cargo build step
.EXAMPLE
.\crate-summary.ps1
.\crate-summary.ps1 -SkipBuild
#>
param(
[switch]$SkipBuild
)
$ErrorActionPreference = "Stop"
# ---------------------------------------------------------
# Helper Functions
# ---------------------------------------------------------
function Test-Command {
param([string]$Command)
$null -ne (Get-Command $Command -ErrorAction SilentlyContinue)
}
# ---------------------------------------------------------
# Prerequisite Checks
# ---------------------------------------------------------
if (-not (Test-Command cargo)) {
Write-Error "Error: cargo not found in PATH"
exit 1
}
if (-not (Test-Command jq)) {
Write-Error "Error: jq is required for this script (cargo metadata parsing).`nInstall jq (choco install jq), (scoop install jq), 0r (winget install jqlang.jq) and run again."
exit 1
}
Write-Host "== Crate summary for current project ==" -ForegroundColor Cyan
# ---------------------------------------------------------
# Optional Build
# ---------------------------------------------------------
if (-not $SkipBuild) {
Write-Host ""
Write-Host "Building project (debug)..." -ForegroundColor Yellow
cargo build
if ($LASTEXITCODE -ne 0) {
Write-Error "Build failed"
exit 1
}
}
# ---------------------------------------------------------
# Tree-based counts (cargo tree)
# ---------------------------------------------------------
Write-Host ""
Write-Host "=== Tree-based counts (cargo tree) ===" -ForegroundColor Green
$treeOutput = cargo tree --edges normal --prefix none 2>&1 | Where-Object { $_.Trim() -ne "" }
$totalTree = ($treeOutput | Measure-Object).Count
$uniqueTree = ($treeOutput | ForEach-Object { $_.Trim() } | Sort-Object -Unique | Measure-Object).Count
Write-Host "Total entries in tree : $totalTree"
Write-Host "Unique lines in tree : $uniqueTree"
# ---------------------------------------------------------
# Metadata-based counts (cargo metadata + jq)
# ---------------------------------------------------------
Write-Host ""
Write-Host "=== Metadata-based counts (cargo metadata) ===" -ForegroundColor Green
$metadataJson = cargo metadata --format-version 1 2>&1 | ConvertFrom-Json
$rootId = $metadataJson.resolve.root
$totalCrates = $metadataJson.resolve.nodes.Count
$rootPackage = $metadataJson.packages | Where-Object { $_.id -eq $rootId }
$rootName = $rootPackage.name
$rootNode = $metadataJson.resolve.nodes | Where-Object { $_.id -eq $rootId }
$directDeps = $rootNode.dependencies.Count
$transitiveDeps = $totalCrates - 1 - $directDeps
Write-Host "Root crate : $rootName"
Write-Host "Total crates in graph : $totalCrates (including root)"
Write-Host "Direct dependencies : $directDeps"
Write-Host "Transitive deps : $transitiveDeps"
Write-Host ""
Write-Host "Summary:"
Write-Host " Tree entries (dup) : $totalTree"
Write-Host " Tree unique lines : $uniqueTree"
Write-Host " Unique crates total : $totalCrates"
Write-Host " Direct / transitive : $directDeps / $transitiveDeps"
# ---------------------------------------------------------
# Crate usage distribution in src/
# ---------------------------------------------------------
Write-Host ""
Write-Host "=== Crate usage distribution in src/ ===" -ForegroundColor Green
if (-not (Test-Path "src" -PathType Container)) {
Write-Host "No src/ directory found, skipping usage analysis."
exit 0
}
$rsFiles = Get-ChildItem -Path "src" -Filter "*.rs" -Recurse -File
if ($rsFiles.Count -eq 0) {
Write-Host "No .rs files under src/, skipping usage analysis."
exit 0
}
# Get direct dependency names
$directDepNames = $rootPackage.dependencies | ForEach-Object { $_.name } | Sort-Object -Unique
if ($directDepNames.Count -eq 0) {
Write-Host "No direct dependencies found in metadata; skipping usage analysis."
exit 0
}
# Count occurrences of "crate_name::" in source files
$counts = @{}
foreach ($dep in $directDepNames) {
# PowerShell regex pattern - look for non-identifier char followed by dep::
$pattern = "[^A-Za-z0-9_]$([regex]::Escape($dep))::"
$count = 0
foreach ($file in $rsFiles) {
$content = Get-Content $file.FullName -Raw
$matches = [regex]::Matches($content, $pattern)
$count += $matches.Count
}
$counts[$dep] = $count
}
# Compute total usage
$totalUsage = ($counts.Values | Measure-Object -Sum).Sum
if ($totalUsage -eq 0) {
Write-Host "No direct crate usages of the form 'crate_name::' found in src/."
Write-Host "This can happen if crates are mostly used via derives/macros or re-exports."
exit 0
}
# Sort by count descending and display
Write-Host ""
Write-Host ("{0,-20} {1,10} {2,10} {3,12}" -f "Crate", "Calls", "Percent", "Category")
Write-Host ("{0,-20} {1,10} {2,10} {3,12}" -f "-----", "-----", "-------", "--------")
$sortedCounts = $counts.GetEnumerator() | Sort-Object -Property Value -Descending
foreach ($entry in $sortedCounts) {
$crate = $entry.Key
$calls = $entry.Value
$pct = ($calls * 100.0) / $totalUsage
# Determine category
$category = if ($pct -ge 20) { "Heavy" } elseif ($pct -ge 5) { "Medium" } else { "Light" }
Write-Host ("{0,-20} {1,10} {2,9:F2}% {3,12}" -f $crate, $calls, $pct, $category)
}
# ---------------------------------------------------------
# Transitive crate usage in src/
# ---------------------------------------------------------
Write-Host ""
Write-Host "=== Transitive crate usage in src/ ===" -ForegroundColor Green
# Get all package IDs except root
$allDepIds = $metadataJson.resolve.nodes | Where-Object { $_.id -ne $rootId } | ForEach-Object { $_.id }
# Get direct dependency IDs
$directDepIds = $rootNode.dependencies
# Get transitive IDs (all - direct)
$transitiveDepIds = $allDepIds | Where-Object { $_ -notin $directDepIds }
# Map IDs to names
$transitiveDepNames = $metadataJson.packages |
Where-Object { $_.id -in $transitiveDepIds } |
ForEach-Object { $_.name } |
Sort-Object -Unique
if ($transitiveDepNames.Count -eq 0) {
Write-Host "No transitive dependencies found."
exit 0
}
Write-Host "Scanning for usage of $($transitiveDepNames.Count) transitive dependencies..."
# Count usages of transitive deps
$transCounts = @{}
foreach ($dep in $transitiveDepNames) {
$pattern = "[^A-Za-z0-9_]$([regex]::Escape($dep))::"
$count = 0
foreach ($file in $rsFiles) {
$content = Get-Content $file.FullName -Raw
$matches = [regex]::Matches($content, $pattern)
$count += $matches.Count
}
if ($count -gt 0) {
$transCounts[$dep] = $count
}
}
# Check if any transitive deps are used
if ($transCounts.Count -eq 0) {
Write-Host "No transitive crate usages of the form 'crate_name::' found in src/."
Write-Host "All crate usage is through direct dependencies or re-exports."
exit 0
}
$totalTransUsage = ($transCounts.Values | Measure-Object -Sum).Sum
Write-Host ""
Write-Host "Found $totalTransUsage usages of transitive dependencies!"
Write-Host "Consider adding these as direct dependencies if heavily used:"
Write-Host ""
Write-Host ("{0,-20} {1,10} {2,10} {3,12}" -f "Crate", "Calls", "Percent", "Category")
Write-Host ("{0,-20} {1,10} {2,10} {3,12}" -f "-----", "-----", "-------", "--------")
$sortedTransCounts = $transCounts.GetEnumerator() | Sort-Object -Property Value -Descending
foreach ($entry in $sortedTransCounts) {
$crate = $entry.Key
$calls = $entry.Value
$pct = ($calls * 100.0) / $totalTransUsage
# Determine category
$category = if ($pct -ge 20) { "Heavy" } elseif ($pct -ge 5) { "Medium" } else { "Light" }
Write-Host ("{0,-20} {1,10} {2,9:F2}% {3,12}" -f $crate, $calls, $pct, $category)
}
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------------------------------------
# Config
# ---------------------------------------------------------
SKIP_BUILD=0
if [[ "${1-}" == "--skip-build" ]]; then
SKIP_BUILD=1
fi
# ---------------------------------------------------------
# Helpers
# ---------------------------------------------------------
have_cmd() {
command -v "$1" >/dev/null 2>&1
}
if ! have_cmd cargo; then
echo "Error: cargo not found in PATH" >&2
exit 1
fi
if ! have_cmd jq; then
echo "Error: jq is required for this script (cargo metadata parsing)." >&2
echo "Install jq and run again." >&2
exit 1
fi
echo "== Crate summary for current project =="
# ---------------------------------------------------------
# Optional build
# ---------------------------------------------------------
if [[ "$SKIP_BUILD" -eq 0 ]]; then
echo
echo "Building project (debug)..."
cargo build
fi
# ---------------------------------------------------------
# Tree-based counts (cargo tree)
# ---------------------------------------------------------
echo
echo "=== Tree-based counts (cargo tree) ==="
# Total entries in the tree (with duplicates)
total_tree=$(
cargo tree --edges normal --prefix none \
| sed '/^[[:space:]]*$/d' \
| wc -l \
| awk '{print $1}'
)
# Unique lines in the tree (deduped)
unique_tree=$(
cargo tree --edges normal --prefix none \
| sed 's/^[[:space:]]*//' \
| sed '/^$/d' \
| sort -u \
| wc -l \
| awk '{print $1}'
)
echo "Total entries in tree : $total_tree"
echo "Unique lines in tree : $unique_tree"
# ---------------------------------------------------------
# Metadata-based counts (cargo metadata + jq)
# ---------------------------------------------------------
echo
echo "=== Metadata-based counts (cargo metadata) ==="
metadata_json="$(cargo metadata --format-version 1 2>/dev/null)"
root_id="$(printf '%s\n' "$metadata_json" | jq -r '.resolve.root')"
total_crates="$(printf '%s\n' "$metadata_json" | jq '.resolve.nodes | length')"
root_name="$(
printf '%s\n' "$metadata_json" \
| jq -r --arg root "$root_id" '.packages[] | select(.id == $root) | .name'
)"
direct_deps="$(
printf '%s\n' "$metadata_json" \
| jq --arg root "$root_id" '.resolve.nodes[] | select(.id == $root) | .dependencies | length'
)"
transitive_deps=$(( total_crates - 1 - direct_deps ))
echo "Root crate : $root_name"
echo "Total crates in graph : $total_crates (including root)"
echo "Direct dependencies : $direct_deps"
echo "Transitive deps : $transitive_deps"
echo
echo "Summary:"
echo " Tree entries (dup) : $total_tree"
echo " Tree unique lines : $unique_tree"
echo " Unique crates total : $total_crates"
echo " Direct / transitive : $direct_deps / $transitive_deps"
# ---------------------------------------------------------
# Crate usage distribution in src/
# ---------------------------------------------------------
echo
echo "=== Crate usage distribution in src/ ==="
if [[ ! -d src ]]; then
echo "No src/ directory found, skipping usage analysis."
exit 0
fi
# Any .rs files?
if ! find src -type f -name '*.rs' | read -r _; then
echo "No .rs files under src/, skipping usage analysis."
exit 0
fi
# Direct dependency names for the root package
# Using portable array building instead of mapfile
direct_dep_names=()
while IFS= read -r dep_name; do
[[ -n "$dep_name" ]] && direct_dep_names+=("$dep_name")
done < <(
printf '%s\n' "$metadata_json" \
| jq -r --arg root "$root_id" '
.packages[]
| select(.id == $root)
| .dependencies[].name
' \
| sort -u
)
if [[ ${#direct_dep_names[@]} -eq 0 ]]; then
echo "No direct dependencies found in metadata; skipping usage analysis."
exit 0
fi
# For each direct dependency, count occurrences of "crate_name::" in src/**/*.rs
# Store counts in a temp file (key-value pairs) for portability
counts_file="$(mktemp)"
for dep in "${direct_dep_names[@]}"; do
# NOTE: This is heuristic; it looks for something like `<non-ident>dep::`
# to approximate "dep::<symbol>" usages.
# We escape '-' literally and let grep treat it as normal char.
pattern="[^A-Za-z0-9_]${dep}::"
count=$(
grep -REo "$pattern" src 2>/dev/null \
| wc -l \
| awk '{print $1}'
)
echo "$dep $count" >> "$counts_file"
done
# Compute total usage across all direct deps
total_usage=$(awk '{sum += $2} END {print sum}' "$counts_file")
if [[ "$total_usage" -eq 0 ]]; then
echo "No direct crate usages of the form 'crate_name::' found in src/."
echo "This can happen if crates are mostly used via derives/macros or re-exports."
rm -f "$counts_file"
exit 0
fi
# Build a sorted list by count (desc)
# We'll print a table: crate, calls, percent, category
printf "\n%-20s %10s %10s %12s\n" "Crate" "Calls" "Percent" "Category"
printf "%-20s %10s %10s %12s\n" "-----" "-----" "-------" "--------"
# Sort by count (second field) descending
sort -k2 -nr "$counts_file" > "${counts_file}.sorted"
while read -r crate calls; do
# Avoid division by zero (already handled earlier, but just in case)
if [[ "$total_usage" -gt 0 ]]; then
pct=$(awk -v c="$calls" -v t="$total_usage" 'BEGIN { printf "%.2f", (c * 100.0) / t }')
else
pct="0.00"
fi
# Category thresholds (tweak if you like)
# Heavy: >= 20%
# Medium: >= 5%
# Light: otherwise
pct_num=$(printf '%s\n' "$pct" | sed 's/,/./')
category="Light"
awk -v p="$pct_num" 'BEGIN { exit !(p >= 20.0) }' && category="Heavy" || true
if [[ "$category" == "Light" ]]; then
awk -v p="$pct_num" 'BEGIN { exit !(p >= 5.0) }' && category="Medium" || true
fi
printf "%-20s %10d %9s%% %12s\n" "$crate" "$calls" "$pct" "$category"
done < "${counts_file}.sorted"
rm -f "$counts_file" "${counts_file}.sorted"
# ---------------------------------------------------------
# Transitive crate usage in src/
# ---------------------------------------------------------
echo
echo "=== Transitive crate usage in src/ ==="
# Get all transitive dependency names (exclude root and direct deps)
transitive_dep_names=()
while IFS= read -r dep_name; do
[[ -n "$dep_name" ]] && transitive_dep_names+=("$dep_name")
done < <(
printf '%s\n' "$metadata_json" \
| jq -r --arg root "$root_id" '
# Get all package IDs in the dependency graph
[.resolve.nodes[] | select(.id != $root) | .id] as $all_deps |
# Get direct dependency IDs
[.resolve.nodes[] | select(.id == $root) | .dependencies[]] as $direct_deps |
# Get transitive = all - direct
($all_deps - $direct_deps) as $transitive_ids |
# Map IDs to names
.packages[] | select([.id] | inside($transitive_ids)) | .name
' \
| sort -u
)
if [[ ${#transitive_dep_names[@]} -eq 0 ]]; then
echo "No transitive dependencies found."
exit 0
fi
echo "Scanning for usage of ${#transitive_dep_names[@]} transitive dependencies..."
# Count usages of transitive deps
trans_counts_file="$(mktemp)"
for dep in "${transitive_dep_names[@]}"; do
pattern="[^A-Za-z0-9_]${dep}::"
count=$(
grep -REo "$pattern" src 2>/dev/null \
| wc -l \
| awk '{print $1}'
)
if [[ "$count" -gt 0 ]]; then
echo "$dep $count" >> "$trans_counts_file"
fi
done
# Check if any transitive deps are used
if [[ ! -s "$trans_counts_file" ]]; then
echo "No transitive crate usages of the form 'crate_name::' found in src/."
echo "All crate usage is through direct dependencies or re-exports."
rm -f "$trans_counts_file"
exit 0
fi
total_trans_usage=$(awk '{sum += $2} END {print sum}' "$trans_counts_file")
echo
echo "Found $total_trans_usage usages of transitive dependencies!"
echo "Consider adding these as direct dependencies if heavily used:"
echo
printf "%-20s %10s %10s %12s\n" "Crate" "Calls" "Percent" "Category"
printf "%-20s %10s %10s %12s\n" "-----" "-----" "-------" "--------"
# Sort by count descending
sort -k2 -nr "$trans_counts_file" > "${trans_counts_file}.sorted"
while read -r crate calls; do
if [[ "$total_trans_usage" -gt 0 ]]; then
pct=$(awk -v c="$calls" -v t="$total_trans_usage" 'BEGIN { printf "%.2f", (c * 100.0) / t }')
else
pct="0.00"
fi
pct_num=$(printf '%s\n' "$pct" | sed 's/,/./')
category="Light"
awk -v p="$pct_num" 'BEGIN { exit !(p >= 20.0) }' && category="Heavy" || true
if [[ "$category" == "Light" ]]; then
awk -v p="$pct_num" 'BEGIN { exit !(p >= 5.0) }' && category="Medium" || true
fi
printf "%-20s %10d %9s%% %12s\n" "$crate" "$calls" "$pct" "$category"
done < "${trans_counts_file}.sorted"
rm -f "$trans_counts_file" "${trans_counts_file}.sorted"
@cryptopatrick
Copy link

This is awesome! Thanks for sharing it!

@cryptopatrick
Copy link

On my MacOS Sequoia the Crate Usage Distribution calc hanged when running the script.
I ran it through Claude and it found the change that was needed. Here's Claude's explanation:

"The "Crate usage distribution in src/" section was hanging due to bash subshell nesting issues. The problem was
caused by using command substitution $(grep ... | wc ...) inside nested for loops that came after process substitution
for populating arrays. This created file descriptor conflicts that caused grep to hang."

Thanks again for this awesome script - super helpful!
=== Crate usage distribution in src/ ===

Crate Calls Percent Category
serde_json 13 52.00% Heavy
serde 11 44.00% Heavy
regex 1 4.00% Light
pgf2json 0 0.00% Light
clap 0 0.00% Light
bytes 0 0.00% Light

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment