defmodule NPM.Dependency.Stats do
@moduledoc """
Computes aggregate statistics across all dependencies.
"""
@doc """
Computes comprehensive stats from a lockfile.
"""
@spec compute(map()) :: map()
def compute(lockfile) do
versions = Enum.map(lockfile, fn {_, e} -> extract_version(e) end) |> Enum.reject(&is_nil/1)
%{
total: map_size(lockfile),
scoped: Enum.count(lockfile, fn {name, _} -> NPM.Scope.scoped?(name) end),
version_stats: version_stats(versions),
top_scopes: top_scopes(lockfile, 5)
}
end
@doc """
Returns the top N scopes by package count.
"""
@spec top_scopes(map(), non_neg_integer()) :: [{String.t(), non_neg_integer()}]
def top_scopes(lockfile, n \\ 5) do
lockfile
|> Enum.flat_map(fn {name, _} ->
case NPM.Scope.extract(name) do
nil -> []
scope -> [scope]
end
end)
|> Enum.frequencies()
|> Enum.sort_by(&elem(&1, 1), :desc)
|> Enum.take(n)
end
@doc """
Returns the average number of transitive dependencies.
"""
@spec avg_deps(map()) :: float()
def avg_deps(lockfile) when map_size(lockfile) == 0, do: 0.0
def avg_deps(lockfile) do
total_deps =
lockfile
|> Enum.map(fn {_, e} -> dep_count(e) end)
|> Enum.sum()
Float.round(total_deps / map_size(lockfile), 2)
end
@doc """
Formats stats for display.
"""
@spec format(map()) :: String.t()
def format(stats) do
"""
Total packages: #{stats.total}
Scoped: #{stats.scoped}
Major versions: #{format_version_stats(stats.version_stats)}
Top scopes: #{format_scopes(stats.top_scopes)}\
"""
end
defp version_stats(versions) do
versions
|> Enum.map(fn v -> v |> String.split(".", parts: 2) |> hd() end)
|> Enum.frequencies()
|> Enum.sort_by(&elem(&1, 1), :desc)
|> Enum.take(5)
end
defp extract_version(%{version: v}), do: v
defp extract_version(%{"version" => v}), do: v
defp extract_version(_), do: nil
defp dep_count(%{dependencies: deps}) when is_map(deps), do: map_size(deps)
defp dep_count(%{"dependencies" => deps}) when is_map(deps), do: map_size(deps)
defp dep_count(_), do: 0
defp format_version_stats(stats) do
Enum.map_join(stats, ", ", fn {v, count} -> "v#{v}(#{count})" end)
end
defp format_scopes([]), do: "none"
defp format_scopes(scopes), do: Enum.map_join(scopes, ", ", fn {s, c} -> "@#{s}(#{c})" end)
end