lib/mix/tasks/compile/parser_path_guard.ex

defmodule Mix.Tasks.Compile.ParserPathGuard do
  @moduledoc false
  use Mix.Task.Compiler

  @recursive true
  @shortdoc "Fails compile when parser usage escapes the XML seam"

  @parser_patterns [~r/\bSaxy\b/, ~r/\bSweetXml\b/, ~r/\bxmerl\b/]
  @allowed_roots [
    "lib/relyra/security/xml.ex",
    "lib/relyra/security/xml/",
    "lib/mix/tasks/compile/parser_path_guard.ex"
  ]

  @impl true
  def run(_args) do
    violations =
      "lib/**/*.ex"
      |> Path.wildcard()
      |> Enum.reject(&allowed_file?/1)
      |> Enum.flat_map(&scan_file/1)

    if violations == [] do
      {:ok, []}
    else
      Mix.shell().error(
        "ParserPathGuard blocked parser references outside Relyra.Security.XML seam:"
      )

      Enum.each(violations, fn {path, line, text} ->
        Mix.shell().error("  #{path}:#{line} -> #{text}")
      end)

      {:error, []}
    end
  end

  defp allowed_file?(path) do
    relative_path = Path.relative_to_cwd(path)

    Enum.any?(@allowed_roots, fn root ->
      String.starts_with?(relative_path, root)
    end)
  end

  defp scan_file(path) do
    path
    |> File.read!()
    |> String.split("\n")
    |> Enum.with_index(1)
    |> Enum.reduce([], fn {line, line_no}, acc ->
      if Enum.any?(@parser_patterns, &Regex.match?(&1, line)) do
        [{path, line_no, String.trim(line)} | acc]
      else
        acc
      end
    end)
    |> Enum.reverse()
  end
end