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