lib/bylaw/credo/check/phoenix/uri_decode_query.ex

defmodule Bylaw.Credo.Check.Phoenix.URIDecodeQuery do
  @moduledoc """
  Use `Plug.Conn.Query.decode/1` instead of `URI.decode_query/1` for query
  strings handled by Phoenix or Plug.

  ## Examples

  Avoid:

        URI.decode_query("ids[]=1&ids[]=2")

  Prefer:

        Plug.Conn.Query.decode("ids[]=1&ids[]=2")

  ## Notes

  `URI.decode_query/1` does not decode Plug-style array and nested
  parameters the same way Phoenix controllers and LiveViews receive them.
  That can make hand-parsed query strings disagree with request params.

  `Plug.Conn.Query.decode/1` follows Plug's query parser semantics, so the
  decoded data matches the rest of the Phoenix request stack.

  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.Phoenix.URIDecodeQuery, []}
        ]
      }
    ]
  }
  ```
  """

  use Credo.Check,
    category: :warning,
    base_priority: :higher,
    explanations: [
      check: @moduledoc
    ]

  @replacement_module Plug.Conn.Query
  @replacement_function :decode
  @doc false
  @impl Credo.Check
  def run(%Credo.SourceFile{} = source_file, params \\ []) do
    issue_meta = IssueMeta.for(source_file, params)
    Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta))
  end

  defp traverse(
         {:., _dot_meta, [{:__aliases__, meta, [:URI]}, :decode_query]} = ast,
         issues,
         issue_meta
       ) do
    {ast, [issue_for(issue_meta, meta[:line] || 0) | issues]}
  end

  defp traverse(ast, issues, _issue_meta), do: {ast, issues}

  defp issue_for(issue_meta, line_no) do
    format_issue(
      issue_meta,
      message:
        "Use #{@replacement_module}.#{@replacement_function}/1 instead of URI.decode_query/1. " <>
          "URI.decode_query/1 handles array parameters incorrectly.",
      trigger: "URI.decode_query",
      line_no: line_no
    )
  end
end