lib/bylaw/html/check/require_link_href.ex

defmodule Bylaw.HTML.Check.RequireLinkHref do
  @moduledoc """
  Validates that rendered anchor tags define an `href` attribute.

  This check inspects rendered `<a>` elements and flags anchors without `href`.
  Use a `<button>` for actions that do not navigate.

  ## Examples

  Bad:

      <a>Settings</a>

  Why this is bad:

  An anchor without `href` is not durable navigation. It is less predictable for
  keyboard users, assistive technology, browser affordances, and link behavior
  such as opening in a new tab or copying the target URL.

  Better:

      <a href="/settings">Settings</a>

  Why this is better:

  The element exposes a real destination, so it behaves like a browser link.

  Bad:

      <a phx-click="save">Save</a>

  Why this is bad:

  A click-only anchor is acting as an event control, not navigation.

  Better:

      <button type="button" phx-click="save">Save</button>

  Why this is better:

  A button communicates that the element performs an action.

  ## Notes

  The check only verifies that an `href` attribute is present. It allows
  `href=""` and `href="#"`; use `Bylaw.HTML.Check.PreferButtonForAction` to
  flag rendered action links that combine `phx-click` with placeholder hrefs.

  This check runs on rendered HTML, so dynamic attributes are evaluated after
  rendering. It does not report the source component or template that produced
  an anchor.

  ## Options

  This check has no check-specific options. Add the module directly to the
  explicit checks list:

      Bylaw.HTML.Check.RequireLinkHref

  ## Usage

  Add this module to the explicit check list passed through `Bylaw.HTML`.
  See `Bylaw.HTML` for the full rendered HTML validation setup.
  """

  @behaviour Bylaw.HTML.Check

  alias Bylaw.HTML.Issue

  @doc """
  Implements the `Bylaw.HTML.Check` validation callback.
  """
  @impl Bylaw.HTML.Check
  @spec validate(Bylaw.HTML.Check.context()) :: Bylaw.HTML.Check.result()
  def validate(%{document: document}) do
    document
    |> LazyHTML.query("a")
    |> Enum.filter(&missing_href?/1)
    |> Enum.map(&issue_for/1)
    |> result()
  end

  def validate(context) do
    raise ArgumentError,
          "expected context to be a map with a parsed document, got: #{inspect(context)}"
  end

  defp missing_href?(element) do
    element
    |> LazyHTML.attribute("href")
    |> Enum.empty?()
  end

  defp issue_for(element) do
    %Issue{
      check: __MODULE__,
      message: "expected <a> to define href; use <button> for actions",
      tag: "a",
      snippet: LazyHTML.to_html(element)
    }
  end

  defp result([]), do: :ok
  defp result(issues), do: {:error, issues}
end