Is `mix xref graph --label compile-connected` overzealous?

I’m confronted with a case where mix xref graph --label compile-connected --fail-above 0 would force me to refactor something which seems unnecessary. I’ll give a simplified example, starting with an expected case and then showing my case.

Expected case

Here is a clear transitive compile-time dependency, where A depends on B which depends on C.

(Note: pretend that each of these modules is in its own file.)

  • If C.hello/0 changes, that means B needs to recompile and B.hello/0 changes
  • If B.hello/0 changes, that means A needs to recompile and A.hello/0 changes
defmodule A do
  @hello B.hello()
  def hello, do: @hello
end

defmodule B do
  @hello C.hello()
  def hello, do: @hello
end

defmodule C do
  def hello, do: "hello"
end

We can find this out like this:

$ mix xref graph --label compile-connected --fail-above 0

lib/a.ex
└── lib/b.ex (compile)
** (Mix) Too many references (found: 1, permitted: 0)

If we want to know “why is it bad that A depends on B? What does B depend on?”, we can check:

$ mix xref graph --source lib/b.ex

lib/b.ex
└── lib/c.ex (compile)

(Tangential point: it would be nice if the first command would show this automatically; its output implies “A shouldn’t depend on B” when maybe the truth is that B shouldn’t depend on C.)

A More Confusing Case

Here is a more confusing case, like the one I’m facing:

  • If C.world/0 changes, that means B needs to recompile and B.world/0 changes
  • If B.world/0 changes, I’m pretty sure that A isn’t going to compile any differently, but because the A depends at compile time on B for B.hello/0, it will get (needlessly?) recompiled.
defmodule A do
  @hello B.hello()
  def hello, do: @hello
end

defmodule B do
  @hello "hello"
  def hello, do: @hello

  def world, do: C.world()
end

defmodule C do
  def world, do: "world"
end

The mix xref commands above see these two situations as the same, even though to me they seem quite different. This seems like a shortcoming of mix xref and/or the compile process.

Am I wrong?

My interpretation of this situation is that you’re tracking more detail in your mental model of the compiler than the compiler actually uses.

C.world/0 changes” is just “C changes” to the compiler.

2 Likes

Michał Muskała said the same in Elixir Slack:

The xref (and compiler) tracks at the granularity of modules, not individual functions

Further conversation in Elixir Slack with Michał Muskała:

Me: So this is a case of “in theory it would be nice if it could be more granular, but that would be hard to implement”? I think in the case that I showed, recompiling A is wasted work

Michał Muskała: Yes, in this case it is. Tracking this however is very complex - the graphs can get pretty big and just maintaining them might be more expensive than just doing some of the extra compilations

3 Likes

thanks for sharing! i am just naively wondering - shouldn’t compiler know/navigate/traverse that graph, at least to some extent anyway?