lib/boundary/mix/tasks/find_external_deps.ex

defmodule Mix.Tasks.Boundary.FindExternalDeps do
  @shortdoc "Prints information about external dependencies of all application boundaries."

  @moduledoc """
  Prints information about external dependencies of all application boundaries.

  Note that `:stdlib`, `:kernel`, `:elixir`, and `:boundary` will not be included in the output.
  """

  # credo:disable-for-this-file Credo.Check.Readability.Specs

  use Boundary, classify_to: Boundary.Mix
  use Mix.Task

  alias Boundary.Mix.CompilerState

  @impl Mix.Task
  def run(_argv) do
    Mix.Task.run("compile")
    Boundary.Mix.load_app()

    view = Boundary.Mix.View.build()

    message =
      view
      |> find_external_deps()
      |> Enum.filter(fn {name, _external_deps} -> Boundary.fetch!(view, name).app == Boundary.Mix.app_name() end)
      |> Enum.sort()
      |> Stream.map(&message/1)
      |> Enum.join("\n")

    Mix.shell().info("\n" <> message)
  end

  defp message({boundary_name, external_deps}) do
    header = "#{[IO.ANSI.bright()]}#{inspect(boundary_name)}#{IO.ANSI.reset()}"

    if Enum.empty?(external_deps) do
      header <> " - no external deps\n"
    else
      """
      #{header}:
        #{external_deps |> Enum.sort() |> Stream.map(&inspect/1) |> Enum.join(", ")}
      """
    end
  end

  defp find_external_deps(boundary_view) do
    CompilerState.start_link()

    for reference <- CompilerState.references(),
        boundary = Boundary.for_module(boundary_view, reference.from),
        boundary.check.out,
        app = Boundary.app(boundary_view, reference.to),
        app not in [:boundary, Boundary.Mix.app_name(), nil],
        reduce: Enum.into(Boundary.all(boundary_view), %{}, &{&1.name, MapSet.new()}) do
      acc ->
        Map.update(acc, boundary.name, MapSet.new([app]), &MapSet.put(&1, app))
    end
  end
end