@@ -34,13 +34,44 @@ defmodule Mix.Tasks.Xref do
3434 (for example, by invoking its macro or using it in the body of a module)
3535 which also have their own dependencies.
3636
37- Therefore, if your goal is to reduce recompilations, the first step is to run:
37+ The most harmful form of compile-connected dependencies are the ones
38+ that are also in a cycle. Imagine you have files `lib/a.ex`, `lib/b.ex`,
39+ and `lib/c.ex` with the following dependencies:
40+
41+ lib/a.ex
42+ └── lib/b.ex (compile)
43+ └── lib/c.ex
44+ └── lib/a.ex
45+
46+ Because you have a compile-time dependency, any of the files `lib/a.ex`,
47+ `lib/b.ex`, and `lib/c.ex` depend on will cause the whole cycle to
48+ recompile. Therefore, your first priority to reduce compile times is
49+ to remove such cycles. You can spot them by running:
3850
3951 $ mix xref graph --format stats --label compile-connected
4052
41- This command will show general information about the project, but
42- focus on compile-connected dependencies. In the stats, you will see
43- the following report:
53+ Whenever you find a compile-time dependency, such as `lib/a.ex` pointing
54+ to `lib/b.ex`, there are two ways to remove them:
55+
56+ 1. Run `mix xref trace lib/a.ex` to understand where and how `lib/a.ex`
57+ depends on `lib/b.ex` at compile time and address it
58+
59+ 2. Or run `mix xref trace lib/b.ex` and make sure it does not depend on
60+ any other module in your project because a compile dependency makes
61+ those runtime dependencies also compile time by transitivity
62+
63+ We outline all options for `mix xref trace` and the types of dependencies
64+ over the following sections.
65+
66+ If you don't have compile cycles in your project, that's a good beginning,
67+ but you want to avoid any compile-connected dependencies in general, as they
68+ may become cycles in the future. To verify the general health of your project,
69+ you may run:
70+
71+ $ mix xref graph --format stats --label compile-connected
72+
73+ This command will show general information about the project, but focus on
74+ compile-connected dependencies. In the stats, you will see the following report:
4475
4576 Top 10 files with most incoming dependencies:
4677 * lib/livebook_web.ex (97)
@@ -62,17 +93,9 @@ defmodule Mix.Tasks.Xref do
6293
6394 The trouble here is precisely that, if any of the files in the latter
6495 command changes, all of the files in the first command will be recompiled,
65- because compile time dependencies are transitive.
66-
67- Having compile time dependencies is a common feature in Elixir projects.
68- However, the modules you depend on at compile-time must avoid dependencies
69- to modules within the same project. You can understand all of the
70- dependencies of a given file by running:
71-
72- $ mix xref trace lib/livebook_web.ex
73-
74- The command above will output three types of dependencies, which we
75- detail next.
96+ because compile time dependencies are transitive. As we did with cycles,
97+ you can use `mix xref trace` to understand why and how these dependencies
98+ exist.
7699
77100 ### Dependency types
78101
@@ -911,20 +934,20 @@ defmodule Mix.Tasks.Xref do
911934 if files == [ ] , do: nil , else: files
912935 end
913936
914- defp write_graph ( file_references , filter , opts ) do
915- { file_references , aliases } = merge_groups ( file_references , Keyword . get_values ( opts , :group ) )
937+ defp write_graph ( all_references , filter , opts ) do
938+ { all_references , aliases } = merge_groups ( all_references , Keyword . get_values ( opts , :group ) )
916939
917- file_references =
918- exclude ( file_references , get_files ( :exclude , opts , file_references , aliases ) )
940+ all_references =
941+ exclude ( all_references , get_files ( :exclude , opts , all_references , aliases ) )
919942
920- sources = get_files ( :source , opts , file_references , aliases )
921- sinks = get_files ( :sink , opts , file_references , aliases )
943+ sources = get_files ( :source , opts , all_references , aliases )
944+ sinks = get_files ( :sink , opts , all_references , aliases )
922945
923946 file_references =
924947 cond do
925- sinks -> sink_tree ( file_references , sinks )
926- sources -> source_tree ( file_references , sources )
927- true -> file_references
948+ sinks -> sink_tree ( all_references , sinks )
949+ sources -> source_tree ( all_references , sources )
950+ true -> all_references
928951 end
929952
930953 { found , count } =
@@ -966,6 +989,12 @@ defmodule Mix.Tasks.Xref do
966989
967990 Mix.Utils . print_tree ( Enum . sort ( roots ) , callback , opts )
968991
992+ if sources do
993+ # We compute the tree again in case sinks are also given
994+ file_references = source_tree ( all_references , sources )
995+ print_sources_cycles ( file_references , sources , opts )
996+ end
997+
969998 { :references , count }
970999
9711000 other ->
@@ -1219,6 +1248,29 @@ defmodule Mix.Tasks.Xref do
12191248 end )
12201249 end
12211250
1251+ defp print_sources_cycles ( references , sources , opts ) do
1252+ with_digraph ( references , fn graph ->
1253+ shell = Mix . shell ( )
1254+
1255+ graph
1256+ |> cycles ( :compile , opts )
1257+ |> Enum . sort ( :desc )
1258+ |> Enum . each ( fn { length , cycle } ->
1259+ if source = Enum . find ( sources , & List . keymember? ( cycle , & 1 , 0 ) ) do
1260+ shell . info ( """
1261+
1262+ WARNING: Source #{ source } is part of a cycle of #{ length } nodes \
1263+ and this cycle has a compile dependency. Therefore source and the \
1264+ whole cycle will recompile whenever any of the files they depend \
1265+ on change. Run "mix xref graph --format stats --label compile-connected" \
1266+ to print compilation cycles and "mix help xref" for information on \
1267+ removing them\
1268+ """ )
1269+ end
1270+ end )
1271+ end )
1272+ end
1273+
12221274 ## Helpers
12231275
12241276 defp apps ( opts ) do
0 commit comments