@@ -34,13 +34,44 @@ defmodule Mix.Tasks.Xref do
34
34
(for example, by invoking its macro or using it in the body of a module)
35
35
which also have their own dependencies.
36
36
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:
38
50
39
51
$ mix xref graph --format stats --label compile-connected
40
52
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:
44
75
45
76
Top 10 files with most incoming dependencies:
46
77
* lib/livebook_web.ex (97)
@@ -62,17 +93,9 @@ defmodule Mix.Tasks.Xref do
62
93
63
94
The trouble here is precisely that, if any of the files in the latter
64
95
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.
76
99
77
100
### Dependency types
78
101
@@ -911,20 +934,20 @@ defmodule Mix.Tasks.Xref do
911
934
if files == [ ] , do: nil , else: files
912
935
end
913
936
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 ) )
916
939
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 ) )
919
942
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 )
922
945
923
946
file_references =
924
947
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
928
951
end
929
952
930
953
{ found , count } =
@@ -966,6 +989,12 @@ defmodule Mix.Tasks.Xref do
966
989
967
990
Mix.Utils . print_tree ( Enum . sort ( roots ) , callback , opts )
968
991
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
+
969
998
{ :references , count }
970
999
971
1000
other ->
@@ -1219,6 +1248,29 @@ defmodule Mix.Tasks.Xref do
1219
1248
end )
1220
1249
end
1221
1250
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
+
1222
1274
## Helpers
1223
1275
1224
1276
defp apps ( opts ) do
0 commit comments