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

defmodule Bylaw.Credo.Check.Elixir.PreferEnumCount do
  @moduledoc """
  Prefer `Enum.count/1` over `length/1`.

  ## Examples

  Avoid:

        length(items)
        items |> length()
  Prefer:

        Enum.count(items)
        items |> Enum.count()

  ## 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.PreferEnumCount, []}
        ]
      }
    ]
  }
  ```
  """

  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

  # Strip guard expressions so length/1 inside guards is not flagged.
  # length/1 is a BIF allowed in guards; Enum.count/1 is not.
  defp walk({:when, meta, [fun_head, _guard]}, ctx) do
    {{:when, meta, [fun_head, nil]}, ctx}
  end

  defp walk({:length, meta, [_value]} = ast, ctx) do
    {ast, put_issue(ctx, issue_for(ctx, meta, "length"))}
  end

  defp walk(
         {{:., meta, [{:__aliases__, _aliases_meta, [:Kernel]}, :length]}, _call_meta, [_value]} =
           ast,
         ctx
       ) do
    {ast, put_issue(ctx, issue_for(ctx, meta, "Kernel.length"))}
  end

  defp walk({:|>, _meta, [_value, {:length, meta, []}]} = ast, ctx) do
    {ast, put_issue(ctx, issue_for(ctx, meta, "length"))}
  end

  defp walk(
         {:|>, _pipe_meta,
          [
            _value,
            {{:., meta, [{:__aliases__, _aliases_meta, [:Kernel]}, :length]}, _call_meta, []}
          ]} = ast,
         ctx
       ) do
    {ast, put_issue(ctx, issue_for(ctx, meta, "Kernel.length"))}
  end

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

  defp issue_for(ctx, meta, trigger) do
    format_issue(
      ctx,
      message: "Prefer `Enum.count/1` over `#{trigger}/1`.",
      trigger: trigger,
      line_no: meta[:line]
    )
  end
end