lib/bylaw/credo/check/elixir/with_else_clause.ex

defmodule Bylaw.Credo.Check.Elixir.WithElseClause do
  @moduledoc """
  Prefer adding an explicit `else` clause to every `with` expression.

  ## Examples

  Avoid:

        with {:ok, user} <- fetch_user(id) do
          {:ok, user.name}
        end

  Prefer:

        with {:ok, user} <- fetch_user(id) do
          {:ok, user.name}
        else
          {:error, error} -> {:error, error}
        end

  ## Notes

  This check uses static AST analysis, so it favors clear source-level patterns over runtime behavior.

  ## Options

  This check has no check-specific options. Configure it with an empty option list.

  ## Usage

  Add this check to Credo's `checks:` list in `.credo.exs`:

  ```elixir
  %{
    configs: [
      %{
        name: "default",
        checks: [
          {Bylaw.Credo.Check.Elixir.WithElseClause, []}
        ]
      }
    ]
  }
  ```
  """

  use Credo.Check,
    base_priority: :high,
    category: :readability,
    explanations: [
      check: @moduledoc
    ]

  @doc false
  @impl Credo.Check
  def run(source_file, params \\ []) do
    ctx = Context.build(source_file, params, __MODULE__)
    Credo.Code.prewalk(source_file, &walk/2, ctx).issues
  end

  defp walk({:with, meta, arguments} = ast, ctx) do
    case missing_else_clause?(arguments) do
      true -> {ast, put_issue(ctx, issue_for(ctx, meta))}
      false -> {ast, ctx}
    end
  end

  defp walk(ast, ctx), do: {ast, ctx}

  defp missing_else_clause?(arguments) do
    case List.last(arguments) do
      block when is_list(block) ->
        Keyword.keyword?(block) and Keyword.has_key?(block, :do) and
          not Keyword.has_key?(block, :else)

      _other ->
        false
    end
  end

  defp issue_for(ctx, meta) do
    format_issue(
      ctx,
      message: "Add an `else` clause to `with` expressions to make error handling explicit.",
      trigger: "with",
      line_no: meta[:line]
    )
  end
end