lib/credo/check/readability/with_custom_tagged_tuple.ex

defmodule Credo.Check.Readability.WithCustomTaggedTuple do
  use Credo.Check,
    id: "EX3032",
    category: :warning,
    base_priority: :low,
    explanations: [
      check: """
      Avoid using custom tags for error reporting from `with` macros.

      This code injects placeholder tags such as `:resource` and `:authz` for the purpose of error
      reporting.

          with {:resource, {:ok, resource}} <- {:resource, Resource.fetch(user)},
               {:authz, :ok} <- {:authz, Resource.authorize(resource, user)} do
            do_something_with(resource)
          else
            {:resource, _} -> {:error, :not_found}
            {:authz, _} -> {:error, :unauthorized}
          end

      Instead, extract each validation into a separate helper function which returns error
      information immediately:

          defp find_resource(user) do
            with :error <- Resource.fetch(user), do: {:error, :not_found}
          end

          defp authorize(resource, user) do
            with :error <- Resource.authorize(resource, user), do: {:error, :unauthorized}
          end

      At this point, the validation chain in `with` becomes clearer and easier to understand:

          with {:ok, resource} <- find_resource(user),
               :ok <- authorize(resource, user),
               do: do_something(user)

      Like all `Readability` issues, this one is not a technical concern.
      But you can improve the odds of others reading and liking your code by making
      it easier to follow.
      """
    ]

  @doc false
  @impl true
  def run(%SourceFile{} = source_file, params \\ []) do
    source_file
    |> errors()
    |> Enum.map(&credo_error(&1, IssueMeta.for(source_file, params)))
  end

  defp errors(source_file) do
    {_ast, errors} = Macro.prewalk(Credo.Code.ast(source_file), MapSet.new(), &traverse/2)
    Enum.sort_by(errors, &{&1.line, &1.column})
  end

  defp traverse({:with, _meta, args}, errors) do
    errors =
      args
      |> Stream.map(&placeholder/1)
      |> Enum.reject(&is_nil/1)
      |> Enum.into(errors)

    {args, errors}
  end

  defp traverse(ast, state), do: {ast, state}

  defp placeholder({:<-, meta, [{placeholder, _}, {placeholder, _}]}) when is_atom(placeholder),
    do: %{placeholder: placeholder, line: meta[:line], column: meta[:column]}

  defp placeholder(_), do: nil

  defp credo_error(error, issue_meta) do
    format_issue(
      issue_meta,
      message: "Invalid usage of placeholder `#{inspect(error.placeholder)}` in with",
      line_no: error.line,
      column: error.column
    )
  end
end