Skip to main content

lib/npm/deprecation_analysis.ex

defmodule NPM.DeprecationAnalysis do
  @moduledoc """
  Analyzes deprecation messages to extract replacement suggestions
  and categorize deprecation reasons.
  """

  @doc """
  Extracts suggested replacement package from deprecation message.
  """
  @spec replacement(String.t()) :: String.t() | nil
  def replacement(message) do
    patterns = [
      ~r/use\s+`?([a-z@][a-z0-9\-\/_\.]*)`?\s+instead/i,
      ~r/replaced\s+by\s+`?([a-z@][a-z0-9\-\/_\.]*)`?/i,
      ~r/moved\s+to\s+`?([a-z@][a-z0-9\-\/_\.]*)`?/i,
      ~r/see\s+`?([a-z@][a-z0-9\-\/_\.]*)`?/i
    ]

    Enum.find_value(patterns, fn pattern ->
      case Regex.run(pattern, message) do
        [_, name] -> name
        _ -> nil
      end
    end)
  end

  @categories [
    {~w(security vulnerab), :security},
    {~w(renamed moved), :renamed},
    {["replaced", "use "], :replaced},
    {["no longer maintained", "unmaintained"], :unmaintained},
    {["broken", "do not use"], :broken}
  ]

  @doc """
  Categorizes the deprecation reason.
  """
  @spec categorize(String.t()) :: atom()
  def categorize(message) do
    msg = String.downcase(message)

    Enum.find_value(@categories, :other, fn {keywords, category} ->
      if Enum.any?(keywords, &String.contains?(msg, &1)), do: category
    end)
  end

  @doc """
  Analyzes a list of deprecated packages.
  """
  @spec analyze([{String.t(), String.t()}]) :: map()
  def analyze(deprecations) do
    by_category =
      deprecations
      |> Enum.map(fn {name, msg} -> {name, msg, categorize(msg)} end)
      |> Enum.group_by(&elem(&1, 2))

    replacements =
      deprecations
      |> Enum.flat_map(fn {name, msg} ->
        case replacement(msg) do
          nil -> []
          rep -> [{name, rep}]
        end
      end)

    %{
      total: length(deprecations),
      by_category: Map.new(by_category, fn {k, v} -> {k, length(v)} end),
      with_replacement: length(replacements),
      replacements: replacements
    }
  end

  @doc """
  Formats an analysis report.
  """
  @spec format_report(map()) :: String.t()
  def format_report(%{total: 0}), do: "No deprecated packages."

  def format_report(analysis) do
    lines = ["#{analysis.total} deprecated packages:"]

    category_lines =
      analysis.by_category
      |> Enum.sort_by(&elem(&1, 1), :desc)
      |> Enum.map(fn {cat, count} -> "  #{cat}: #{count}" end)

    replacement_lines =
      if analysis.replacements != [] do
        [
          "Suggested replacements:"
          | Enum.map(analysis.replacements, fn {from, to} -> "  #{from}#{to}" end)
        ]
      else
        []
      end

    Enum.join(lines ++ category_lines ++ replacement_lines, "\n")
  end
end