defmodule NPM.Dependency.Range do @moduledoc """ Analyzes dependency version ranges for pinning strategy insights. """ @doc """ Classifies a version range string. """ @spec classify(String.t()) :: atom() def classify("*"), do: :star def classify("latest"), do: :latest def classify("git" <> _ = range), do: classify_url_or(range) def classify("file:" <> _), do: :file def classify("workspace:" <> _), do: :workspace def classify("npm:" <> _), do: :alias def classify("~" <> _), do: :tilde def classify("^" <> _range = full), do: classify_compound(full) def classify(range) when is_binary(range) do cond do String.contains?(range, "://") -> :url String.contains?(range, "||") -> :or_range String.contains?(range, " - ") -> :hyphen NPM.VersionRange.exact?(range) -> :exact true -> :other end end defp classify_url_or(range) do if String.contains?(range, "://"), do: :url, else: :other end defp classify_compound(range) do if String.contains?(range, "||"), do: :or_range, else: :caret end @doc """ Analyzes all dependencies and returns a breakdown by range type. """ @spec analyze(map()) :: map() def analyze(deps) when is_map(deps) do deps |> Enum.map(fn {name, range} -> {name, classify(range)} end) |> Enum.group_by(&elem(&1, 1), &elem(&1, 0)) |> Map.new(fn {type, names} -> {type, Enum.sort(names)} end) end @doc """ Returns a summary of range types. """ @spec summary(map()) :: map() def summary(deps) when is_map(deps) do breakdown = Enum.map(deps, fn {_, range} -> classify(range) end) |> Enum.frequencies() total = map_size(deps) pinned = Map.get(breakdown, :exact, 0) %{ total: total, breakdown: breakdown, pinned_count: pinned, pinned_pct: if(total > 0, do: Float.round(pinned / total * 100, 1), else: 0.0), has_urls: Map.has_key?(breakdown, :url), has_files: Map.has_key?(breakdown, :file) } end @doc """ Returns packages using non-registry sources (git, file, url). """ @spec non_registry(map()) :: [{String.t(), atom()}] def non_registry(deps) when is_map(deps) do deps |> Enum.filter(fn {_, range} -> classify(range) in [:url, :file] end) |> Enum.map(fn {name, range} -> {name, classify(range)} end) |> Enum.sort_by(&elem(&1, 0)) end end