From 4e55461a9107a8d93c0177cd787666ae905ea8e8 Mon Sep 17 00:00:00 2001 From: Nathan Long Date: Fri, 17 May 2024 12:46:49 -0400 Subject: [PATCH 1/2] Document why 'mix xref' can give false positives --- lib/mix/lib/mix/tasks/xref.ex | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/lib/mix/lib/mix/tasks/xref.ex b/lib/mix/lib/mix/tasks/xref.ex index c4cd455ae47..7cb956b2e17 100644 --- a/lib/mix/lib/mix/tasks/xref.ex +++ b/lib/mix/lib/mix/tasks/xref.ex @@ -279,6 +279,43 @@ defmodule Mix.Tasks.Xref do * `--no-elixir-version-check` - does not check the Elixir version from mix.exs + ## False positives + + The Elixir compiler tracks dependencies at the *module* level, not at the + *function* level. + Consider this case: + + # lib/a.ex + defmodule A do + @hello B.hello() + def hello, do: @hello + end + + # lib/b.ex + defmodule B do + @hello "hello" + def hello, do: @hello + + def world, do: C.world() + end + + # lib/c.ex + defmodule C do + def world, do: "world" + end + + If `C.world/0` changes, `B` needs to be recompiled so that `B.world/0` will + return the new value. + That change will not actually make `A` compile differently; `A.hello/0`` is + not dependent on `B.world/0`. + But because `A` depends on `B` for *something*, `A` will be recompiled, and + `mix xref graph --label compile-connected` will show that `A` has a + transitive dependency through `B`. + + Tracking at a more granular level might be possible, but might be more + expensive than the unnecessary recompilation of modules like `A` in this + case. + In any case, it is not currently implemented. """ @switches [ From 4912c48cca37cc0bb70c24cbcdc3e2ea4e140892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 17 May 2024 19:46:51 +0200 Subject: [PATCH 2/2] Update xref.ex --- lib/mix/lib/mix/tasks/xref.ex | 72 +++++++++++++++-------------------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/lib/mix/lib/mix/tasks/xref.ex b/lib/mix/lib/mix/tasks/xref.ex index 7cb956b2e17..fc12d4dd23d 100644 --- a/lib/mix/lib/mix/tasks/xref.ex +++ b/lib/mix/lib/mix/tasks/xref.ex @@ -240,10 +240,37 @@ defmodule Mix.Tasks.Xref do Elixir tracks three types of dependencies between modules: compile, exports, and runtime. If a module has a compile time dependency on another module, the caller module has to be recompiled whenever the - callee changes. Compile-time dependencies are typically added when - using macros or when invoking functions in the module body (outside - of functions). You can list all dependencies in a file by running - `mix xref trace path/to/file.ex`. + callee changes (or any runtime dependency of the callee changes). + Let's see an example: + + # lib/a.ex + defmodule A do + @hello B.hello() + def hello, do: @hello + end + + # lib/b.ex + defmodule B do + def hello, do: "hello" + def world, do: C.world() + end + + # lib/c.ex + defmodule C do + def world, do: "world" + end + + If `C.world/0` changes, `B` is marked as stale. `B` does not need to + be recompiled, because it depends on `C` at runtime, but anything that + depends on `B` at compile-time has to recompile, and that includes `A`. + + Compile-time dependencies are typically added when using macros or + when invoking functions in the module body (outside of functions). + You can list all dependencies in a file by running + `mix xref trace path/to/file.ex`. This type of transitive compile-time + dependencies, such as `A` depending on `C` at compile-time through `B`, + can be found with the "compile-connected" label, as in + `mix xref graph --label compile-connected`. Export dependencies are compile time dependencies on the module API, namely structs and its public definitions. For example, if you import @@ -279,43 +306,6 @@ defmodule Mix.Tasks.Xref do * `--no-elixir-version-check` - does not check the Elixir version from mix.exs - ## False positives - - The Elixir compiler tracks dependencies at the *module* level, not at the - *function* level. - Consider this case: - - # lib/a.ex - defmodule A do - @hello B.hello() - def hello, do: @hello - end - - # lib/b.ex - defmodule B do - @hello "hello" - def hello, do: @hello - - def world, do: C.world() - end - - # lib/c.ex - defmodule C do - def world, do: "world" - end - - If `C.world/0` changes, `B` needs to be recompiled so that `B.world/0` will - return the new value. - That change will not actually make `A` compile differently; `A.hello/0`` is - not dependent on `B.world/0`. - But because `A` depends on `B` for *something*, `A` will be recompiled, and - `mix xref graph --label compile-connected` will show that `A` has a - transitive dependency through `B`. - - Tracking at a more granular level might be possible, but might be more - expensive than the unnecessary recompilation of modules like `A` in this - case. - In any case, it is not currently implemented. """ @switches [