Skip to content

Commit 81f885d

Browse files
committed
Add anti-pattern on untracked compile-time dependencies, closes #14465
1 parent 8de98a8 commit 81f885d

File tree

2 files changed

+132
-0
lines changed

2 files changed

+132
-0
lines changed

lib/elixir/lib/module.ex

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,12 @@ defmodule Module do
945945
946946
It handles binaries and atoms.
947947
948+
> #### Untracked compile-time dependencies {. :warning}
949+
>
950+
> Use this function with care, as dynamically defining
951+
> module names at compilation time may lead to
952+
> [untracked compile-time dependencies](macro-anti-patterns.md#untracked-compile-time-dependencies).
953+
948954
## Examples
949955
950956
iex> Module.concat([Foo, Bar])
@@ -965,6 +971,12 @@ defmodule Module do
965971
It handles binaries and atoms. If one of the aliases
966972
is nil, it is discarded.
967973
974+
> #### Untracked compile-time dependencies {. :warning}
975+
>
976+
> Use this function with care, as dynamically defining
977+
> module names at compilation time may lead to
978+
> [untracked compile-time dependencies](macro-anti-patterns.md#untracked-compile-time-dependencies).
979+
968980
## Examples
969981
970982
iex> Module.concat(Foo, Bar)
@@ -990,6 +1002,12 @@ defmodule Module do
9901002
If the alias was not referenced yet, fails with `ArgumentError`.
9911003
It handles binaries and atoms.
9921004
1005+
> #### Untracked compile-time dependencies {. :warning}
1006+
>
1007+
> Use this function with care, as dynamically defining
1008+
> module names at compilation time may lead to
1009+
> [untracked compile-time dependencies](macro-anti-patterns.md#untracked-compile-time-dependencies).
1010+
9931011
## Examples
9941012
9951013
iex> Module.safe_concat([List, Chars])
@@ -1008,6 +1026,12 @@ defmodule Module do
10081026
If the alias was not referenced yet, fails with `ArgumentError`.
10091027
It handles binaries and atoms.
10101028
1029+
> #### Untracked compile-time dependencies {. :warning}
1030+
>
1031+
> Use this function with care, as dynamically defining
1032+
> module names at compilation time may lead to
1033+
> [untracked compile-time dependencies](macro-anti-patterns.md#untracked-compile-time-dependencies).
1034+
10111035
## Examples
10121036
10131037
iex> Module.safe_concat(List, Chars)

lib/elixir/pages/anti-patterns/macro-anti-patterns.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,3 +287,111 @@ For convenience, the markup notation to generate the admonition block above is t
287287
> function, so your module can be used as a child
288288
> in a supervision tree.
289289
```
290+
291+
## Untracked compile-time dependencies
292+
293+
#### Problem
294+
295+
This anti-pattern is the opposite of ["Compile-time dependencies"](#compile-time-dependencies) and it happens when a compile-time dependency is accidentally bypassed, making the Elixir compiler is to track dependencies and recompile files correctly. This happens when building aliases (in other words, module names) dynamically, either within a module or within a macro.
296+
297+
#### Example
298+
299+
For example, imagine you invoke a module at compile-time, you could write it as such:
300+
301+
```elixir
302+
defmodule MyModule do
303+
SomeOtherModule.example()
304+
end
305+
```
306+
307+
In this case, Elixir knows `MyModule` is invoked `SomeOtherModule.example/0` outside of a function, and therefore at compile-time.
308+
309+
Elixir can also track module names even during dynamic calls:
310+
311+
```elixir
312+
defmodule MyModule do
313+
mods = [OtherModule.Foo, OtherModule.Bar]
314+
315+
for mod <- mods do
316+
mod.example()
317+
end
318+
end
319+
```
320+
321+
In the previous example, even though Elixir does not know which modules the function `example/0` was invoked on, it knows the modules `OtherModule.Foo` and `OtherModule.Bar` are referred outside of a function and therefore they become compile-time dependencies. If any of them change, Elixir will recompile `MyModule` itself.
322+
323+
However, you should not programatically generate the module names themselves, as that would make it impossible for Elixir to track them. More precisely, do not do this:
324+
325+
```elixir
326+
defmodule MyModule do
327+
parts = [:Foo, :Bar]
328+
329+
for part <- parts do
330+
Module.concat(OtherModule, part).example()
331+
end
332+
end
333+
```
334+
335+
In this case, because the whole module was generated, Elixir sees a dependency only to `OtherModule`, never to `OtherModule.Foo` and `OtherModule.Bar`, potentially leading to inconsistencies when recompiling projects.
336+
337+
A similar bug can happen when abusing the property that aliases are simply atoms, defining the atoms directly. In the case below, Elixir never sees the aliases, leading to untracked compile-time dependencies:
338+
339+
```elixir
340+
defmodule MyModule do
341+
mods = [:"Elixir.OtherModule.Foo", :"Elixir.OtherModule.Bar"]
342+
343+
for mod <- mods do
344+
mod.example()
345+
end
346+
end
347+
```
348+
349+
#### Refactoring
350+
351+
To address this anti-pattern, you should avoid defining module names programatically. For example, if you need to dispatch to multiple modules, do so by using full module names.
352+
353+
Instead of:
354+
355+
```elixir
356+
defmodule MyModule do
357+
parts = [:Foo, :Bar]
358+
359+
for part <- parts do
360+
Module.concat(OtherModule, part).example()
361+
end
362+
end
363+
```
364+
365+
Do:
366+
367+
```elixir
368+
defmodule MyModule do
369+
mods = [OtherModule.Foo, OtherModule.Bar]
370+
371+
for mod <- mods do
372+
mod.example()
373+
end
374+
end
375+
```
376+
377+
If you really need to define modules dynamically, you can do so via meta-programming, building the whole module name at compile-time:
378+
379+
```elixir
380+
defmodule MyMacro do
381+
defmacro call_examples(parts) do
382+
for part <- parts do
383+
quote do
384+
# This builds OtherModule.Foo at compile-time
385+
OtherModule.unquote(part).example()
386+
end
387+
end
388+
end
389+
end
390+
391+
defmodule MyModule do
392+
import MyMacro
393+
call_examples [:Foo, :Bar]
394+
end
395+
```
396+
397+
In actual projects, developers may use `mix xref trace path/to/file.ex` to execute a file and have it print information about which modules it depends on, and if those modules are compile-time, runtime, or export dependencies. This can help you debug if the dependencies are being properly tracked in relation to external modules. See `mix xref` for more information.

0 commit comments

Comments
 (0)