lib/contexted/module_analyzer.ex

defmodule Contexted.ModuleAnalyzer do
  @moduledoc """
  The `Contexted.ModuleAnalyzer` defines utils functions that analyze and extract information from other modules.
  """

  @doc """
  Fetches the `@doc` definitions for all functions within the given module.
  """
  @spec get_functions_docs(module()) :: [tuple()]
  def get_functions_docs(module) do
    case Code.fetch_docs(module) do
      {:docs_v1, _, _, _, _, _, functions_docs} ->
        Enum.filter(functions_docs, fn
          {{:function, _, _}, _, _, _, _} -> true
          {_, _, _, _, _} -> false
        end)

      _ ->
        []
    end
  end

  @doc """
  Fetches the `@spec` definitions for all functions within the given module.
  """
  @spec get_functions_specs(module()) :: [tuple()]
  def get_functions_specs(module) do
    case Code.Typespec.fetch_specs(module) do
      {:ok, specs} -> specs
      _ -> []
    end
  end

  @doc """
  Finds and returns the `@spec` definition in string format for the specified function name and arity.
  Returns `nil` if the function is not found in the specs.
  """
  @spec get_function_spec([tuple()], atom(), non_neg_integer()) :: String.t() | nil
  def get_function_spec(specs, function_name, arity) do
    # Find the spec tuple in the specs
    spec = find_spec(specs, function_name, arity)

    # If spec is found, build the spec expression
    if spec do
      build_spec(spec)
    else
      nil
    end
  end

  @doc """
  Finds and returns the `@doc` definition in string format for the specified function name and arity.
  Returns `nil` if the function is not found in the function docs.
  """
  @spec get_function_doc([tuple()], atom(), non_neg_integer()) :: String.t() | nil
  def get_function_doc(functions_docs, name, arity) do
    Enum.find(functions_docs, fn
      {{:function, func_name, func_arity}, _, _, _, _} ->
        func_name == name && func_arity == arity
    end)
    |> case do
      {_, _, _, %{"en" => doc}, _} ->
        "@doc \"\"\"\n#{doc}\n\"\"\""

      _ ->
        nil
    end
  end

  @doc """
  Generates a list of unique argument names based on the given arity.
  """
  @spec generate_random_function_arguments(non_neg_integer()) :: [atom()]
  def generate_random_function_arguments(arity) do
    if arity > 0 do
      Enum.map(0..(arity - 1), &{String.to_atom("arg_#{&1}"), [], nil})
    else
      []
    end
  end

  @spec find_spec([tuple()], atom(), non_neg_integer()) :: tuple() | nil
  defp find_spec(specs, function_name, arity) do
    Enum.find(specs, fn
      {{^function_name, ^arity}, _} -> true
      _ -> false
    end)
  end

  @spec build_spec(tuple()) :: String.t()
  defp build_spec({{function_name, _arity}, specs}) do
    Enum.map_join(specs, "\n", fn spec ->
      Code.Typespec.spec_to_quoted(function_name, spec)
      |> add_spec_ast()
      |> Macro.to_string()
    end)
  end

  @spec add_spec_ast(tuple()) :: tuple()
  defp add_spec_ast(ast) do
    {:@, [context: Elixir, imports: [{1, Kernel}]],
     [
       {:spec, [context: Elixir],
        [
          ast
        ]}
     ]}
  end
end