Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
372836d623 | ||
|
|
444cd7e6bd | ||
|
|
cacb175ab3 | ||
|
|
bfce3ed7a9 | ||
|
|
06811cbdc8 | ||
|
|
7a6b35422c | ||
|
|
78f63d0831 | ||
|
|
86ec46a5c4 | ||
|
|
c99631876c | ||
|
|
46815ef554 | ||
|
|
256a55a53e | ||
|
|
a78c92043f | ||
|
|
b3bfa519a7 | ||
|
|
b734da3f62 | ||
|
|
10f86595b5 | ||
|
|
1da385776f | ||
|
|
a45e08eeda | ||
|
|
bb61b953d7 | ||
|
|
1e126e88b7 | ||
|
|
47abe2d107 | ||
|
|
e254e7e68e | ||
|
|
d3c0b38c5d | ||
|
|
662dd6b8d9 | ||
|
|
cadd501cbd | ||
|
|
ddf84aba7e | ||
|
|
2d808ec652 | ||
|
|
6bfb95ab88 | ||
|
|
d53977002d | ||
|
|
776e5a34ea | ||
|
|
ae5707f90a | ||
|
|
67617e5d8c | ||
|
|
6f5fc94276 | ||
|
|
6c935ca6ec | ||
|
|
d1076e487c | ||
|
|
b9e447757d | ||
|
|
b7943e113d | ||
|
|
1a320bb60d | ||
|
|
64e63b1b9d | ||
|
|
5a8b28427c | ||
|
|
a1eb59d035 |
+57
-4
@@ -22,8 +22,6 @@ We focused on atoms and maps on this initial release as they are respectively th
|
||||
|
||||
* Accessing a key on a map or a struct that does not have the given key, such as `user.adress`
|
||||
|
||||
* Updating a struct or a map that does not define the given key, such as `%{user | adress: ...}`
|
||||
|
||||
* Invoking a function on non-modules, such as `user.address()`
|
||||
|
||||
* Capturing a function on non-modules, such as `&user.address/0`
|
||||
@@ -40,7 +38,7 @@ We focused on atoms and maps on this initial release as they are respectively th
|
||||
|
||||
* Accessing a field that is not defined in a rescued exception
|
||||
|
||||
These new warnings help Elixir developers find bugs earlier and give more confidence when refactoring code, especially around maps and structs. While some of these warnings were emitted in the past, they were discovered using syntax analysis. The new warnings are more reliable, precise, and with better error messages. Keep in mind, however, that the Elixir typechecker only infers types from patterns within the same function at the moment. Analysis from guards and across function boundaries will be added in future relases. For more details, see our new [reference document on gradual set-theoretic types](https://hexdocs.pm/elixir/main/gradual-set-theoretic-types.html).
|
||||
These new warnings help Elixir developers find bugs earlier and give more confidence when refactoring code, especially around maps and structs. While some of these warnings were emitted in the past, they were discovered using syntax analysis. The new warnings are more reliable, precise, and with better error messages. Keep in mind, however, that the Elixir typechecker only infers types from patterns within the same function at the moment. Analysis from guards and across function boundaries will be added in future relases. For more details, see our new [reference document on gradual set-theoretic types](https://hexdocs.pm/elixir/gradual-set-theoretic-types.html).
|
||||
|
||||
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
|
||||
|
||||
@@ -72,6 +70,61 @@ Finally, a new `Kernel.to_timeout/1` function has been added, which helps develo
|
||||
Process.send_after(pid, :wake_up, to_timeout(hour: 1))
|
||||
```
|
||||
|
||||
## v1.17.3 (2024-09-18)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Duration] Fix parsing of fractional durations with non-positive seconds
|
||||
* [Kernel] Do not attempt to group module warnings when they have a large context
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Helpers] Properly reconsolidate protocols on `recompile`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile.elixir] Do not verify modules twice
|
||||
* [mix xref] Respect the `--label` option on stats and cycles
|
||||
|
||||
## v1.17.2 (2024-07-06)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger.Translator] Fix logger crash when `:gen_statem`'s `format_status/2` returns non-tuple
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix deps.get] Fix regression when fetching a git repository with a `:ref`
|
||||
* [mix release] Validate `RELEASE_MODE` and set ERRORLEVEL on `.bat` scripts
|
||||
* [mix release] Fix invalid example in code comment inside the generated vm.args.eex
|
||||
|
||||
## v1.17.1 (2024-06-18)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile.elixir] Do not run fixpoint computation on runtime dependencies. This should considerably improve compilation times for large projects when changing only one or two files
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Do not warn for assignment with blocks in EEx
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Fix bug when using pinned variables inside `with`'s `else` patterns
|
||||
* [Kernel] Fix Dialyzer error when with else clause is calling a `no_return` function
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Do not alternative sync/async suites on `--repeat-until-failure`
|
||||
|
||||
## v1.17.0 (2024-06-12)
|
||||
|
||||
### 1. Enhancements
|
||||
@@ -126,7 +179,6 @@ Process.send_after(pid, :wake_up, to_timeout(hour: 1))
|
||||
* [mix test] Add `mix test --slowest-modules` to print slowest modules based on all of the tests they hold
|
||||
* [mix test] Generate cover HTML files in parallel
|
||||
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
@@ -141,6 +193,7 @@ Process.send_after(pid, :wake_up, to_timeout(hour: 1))
|
||||
* [Kernel] Avoid double tracing events when capturing a function
|
||||
* [Kernel] Fix a bug where captured arguments would conflict when a capture included a macro that also used captures
|
||||
* [Module] Return default value in `Module.get_attribute/3` for persisted attributes which have not yet been written to
|
||||
* [String] Properly handle transpositions in `jaro_distance`. This will correct the distance result in certain cases
|
||||
|
||||
#### IEx
|
||||
|
||||
|
||||
+4
-4
@@ -10,13 +10,13 @@
|
||||
|
||||
4. Commit changes above with title "Release vVERSION" and push it
|
||||
|
||||
6. Once GitHub actions completes, generate a new tag, and push it
|
||||
5. Once GitHub actions completes, generate a new tag, and push it
|
||||
|
||||
7. Wait until GitHub Actions publish artifacts to the draft release
|
||||
6. Wait until GitHub Actions publish artifacts to the draft release
|
||||
|
||||
8. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one)
|
||||
7. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one)
|
||||
|
||||
9. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
|
||||
8. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
|
||||
|
||||
## Creating a new vMAJOR.MINOR branch (before first rc)
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.17.0
|
||||
ELIXIR_VERSION=1.17.3
|
||||
|
||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||
cat <<USAGE >&2
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
@echo off
|
||||
|
||||
set ELIXIR_VERSION=1.17.0
|
||||
set ELIXIR_VERSION=1.17.3
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
|
||||
@@ -340,13 +340,6 @@ defmodule EEx.Compiler do
|
||||
scope,
|
||||
state
|
||||
) do
|
||||
if mark == ~c"" do
|
||||
message =
|
||||
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
|
||||
|
||||
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
|
||||
end
|
||||
|
||||
{rest, line, contents} = look_ahead_middle(rest, meta.line, chars) || {rest, meta.line, chars}
|
||||
start_line = meta.line
|
||||
start_column = column(meta.column, mark)
|
||||
@@ -359,6 +352,13 @@ defmodule EEx.Compiler do
|
||||
%{state | quoted: [], line: line}
|
||||
)
|
||||
|
||||
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
|
||||
message =
|
||||
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
|
||||
|
||||
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
|
||||
end
|
||||
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
@@ -43,15 +43,6 @@ defmodule EEx.SmartEngineTest do
|
||||
assert_received :found
|
||||
end
|
||||
|
||||
test "error with unused \"do\" block without \"<%=\" modifier" do
|
||||
stderr =
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
assert_eval("", "<% if true do %>I'm invisible!<% end %>", assigns: %{})
|
||||
end)
|
||||
|
||||
assert stderr =~ "the contents of this expression won't be output"
|
||||
end
|
||||
|
||||
defp assert_eval(expected, actual, binding \\ []) do
|
||||
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
|
||||
assert result == expected
|
||||
|
||||
@@ -543,6 +543,16 @@ defmodule EExTest do
|
||||
~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"]
|
||||
end
|
||||
|
||||
test "unused \"do\" block without \"<%=\" modifier" do
|
||||
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
EEx.compile_string("<% if true do %>I'm invisible!<% end %>")
|
||||
end) =~ "the contents of this expression won't be output"
|
||||
|
||||
# These are fine though
|
||||
EEx.compile_string("<% foo = fn -> %>Hello<% end %>")
|
||||
EEx.compile_string("<% foo = if true do %>Hello<% end %>")
|
||||
end
|
||||
|
||||
test "from tokenizer" do
|
||||
warning =
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
|
||||
@@ -715,7 +715,15 @@ defmodule Calendar.ISO do
|
||||
{second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] ->
|
||||
case parse_microsecond(rest) do
|
||||
{{ms, precision}, "S"} ->
|
||||
ms = if second > 0, do: ms, else: -ms
|
||||
ms =
|
||||
case string do
|
||||
"-" <> _ ->
|
||||
-ms
|
||||
|
||||
_ ->
|
||||
ms
|
||||
end
|
||||
|
||||
{:ok, [second: second, microsecond: {ms, precision}] ++ acc}
|
||||
|
||||
_ ->
|
||||
|
||||
@@ -174,16 +174,17 @@ defmodule Code.Normalizer do
|
||||
|
||||
# Sigils
|
||||
defp do_normalize({sigil, meta, [{:<<>>, _, args} = string, modifiers]} = quoted, state)
|
||||
when is_list(args) and is_atom(sigil) do
|
||||
case Atom.to_string(sigil) do
|
||||
"sigil_" <> _ ->
|
||||
meta =
|
||||
meta
|
||||
|> patch_meta_line(state.parent_meta)
|
||||
|> Keyword.put_new(:delimiter, "\"")
|
||||
|
||||
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
|
||||
when is_atom(sigil) and is_list(args) and is_list(modifiers) do
|
||||
with "sigil_" <> _ <- Atom.to_string(sigil),
|
||||
true <- binary_interpolated?(args),
|
||||
true <- List.ascii_printable?(modifiers) do
|
||||
meta =
|
||||
meta
|
||||
|> patch_meta_line(state.parent_meta)
|
||||
|> Keyword.put_new(:delimiter, "\"")
|
||||
|
||||
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
|
||||
else
|
||||
_ ->
|
||||
normalize_call(quoted, state)
|
||||
end
|
||||
|
||||
@@ -588,6 +588,21 @@ defmodule Inspect.Algebra do
|
||||
doc_cons(doc1, doc2)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Disable any rendering limit while rendering the given document.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.glue("hello", "world") |> Inspect.Algebra.group()
|
||||
iex> Inspect.Algebra.format(doc, 10)
|
||||
["hello", "\n", "world"]
|
||||
iex> doc = Inspect.Algebra.no_limit(doc)
|
||||
iex> Inspect.Algebra.format(doc, 10)
|
||||
["hello", " ", "world"]
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec no_limit(t) :: t
|
||||
def no_limit(doc) do
|
||||
doc_limit(doc, :infinity)
|
||||
end
|
||||
|
||||
@@ -6197,9 +6197,8 @@ defmodule Kernel do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec to_timeout([component, ...] | timeout() | Duration.t()) :: timeout()
|
||||
when component: [{unit, non_neg_integer()}, ...],
|
||||
unit: :week | :day | :hour | :minute | :second | :millisecond
|
||||
@spec to_timeout([{unit, non_neg_integer()}] | timeout() | Duration.t()) :: timeout()
|
||||
when unit: :week | :day | :hour | :minute | :second | :millisecond
|
||||
def to_timeout(duration)
|
||||
|
||||
def to_timeout(:infinity), do: :infinity
|
||||
|
||||
@@ -1832,13 +1832,13 @@ defmodule Macro do
|
||||
defp do_expand_once({:__DIR__, _, atom}, env) when is_atom(atom),
|
||||
do: {:filename.dirname(env.file), true}
|
||||
|
||||
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) do
|
||||
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) and env.context != :match do
|
||||
env = update_in(env.versioned_vars, &maybe_escape_map/1)
|
||||
{maybe_escape_map(env), true}
|
||||
end
|
||||
|
||||
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env)
|
||||
when is_atom(atom) and is_atom(field) do
|
||||
when is_atom(atom) and is_atom(field) and env.context != :match do
|
||||
if Map.has_key?(env, field) do
|
||||
{maybe_escape_map(Map.get(env, field)), true}
|
||||
else
|
||||
|
||||
@@ -291,18 +291,34 @@ defmodule Module.ParallelChecker do
|
||||
## Warning helpers
|
||||
|
||||
defp group_warnings(warnings) do
|
||||
warnings
|
||||
|> Enum.reduce(%{}, fn {module, warning, location}, acc ->
|
||||
locations = MapSet.new([location])
|
||||
Map.update(acc, {module, warning}, locations, &MapSet.put(&1, location))
|
||||
end)
|
||||
|> Enum.map(fn {{module, warning}, locations} -> {module, warning, Enum.sort(locations)} end)
|
||||
|> Enum.sort()
|
||||
{ungrouped, grouped} =
|
||||
Enum.reduce(warnings, {[], %{}}, fn {module, warning, location}, {ungrouped, grouped} ->
|
||||
%{message: _} = diagnostic = module.format_diagnostic(warning)
|
||||
|
||||
if Map.get(diagnostic, :group, false) do
|
||||
locations = MapSet.new([location])
|
||||
|
||||
grouped =
|
||||
Map.update(grouped, warning, {locations, diagnostic}, fn
|
||||
{locations, diagnostic} -> {MapSet.put(locations, location), diagnostic}
|
||||
end)
|
||||
|
||||
{ungrouped, grouped}
|
||||
else
|
||||
{[{[location], diagnostic} | ungrouped], grouped}
|
||||
end
|
||||
end)
|
||||
|
||||
grouped =
|
||||
Enum.map(grouped, fn {_warning, {locations, diagnostic}} ->
|
||||
{Enum.sort(locations), diagnostic}
|
||||
end)
|
||||
|
||||
Enum.sort(ungrouped ++ grouped)
|
||||
end
|
||||
|
||||
defp emit_warnings(warnings, log?) do
|
||||
Enum.flat_map(warnings, fn {module, warning, locations} ->
|
||||
%{message: _} = diagnostic = module.format_diagnostic(warning)
|
||||
Enum.flat_map(warnings, fn {locations, diagnostic} ->
|
||||
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, &1))
|
||||
log? and print_diagnostics(diagnostics)
|
||||
diagnostics
|
||||
|
||||
@@ -176,10 +176,15 @@ defmodule Module.Types.Of do
|
||||
# TODO: Use the struct default values to define the default types.
|
||||
def struct(struct, args_types, default_handling, meta, stack, context) do
|
||||
context = remote(struct, :__struct__, 0, meta, stack, context)
|
||||
|
||||
info =
|
||||
struct.__info__(:struct) ||
|
||||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
|
||||
|
||||
term = term()
|
||||
|
||||
defaults =
|
||||
for %{field: field} <- struct.__info__(:struct), field != :__struct__ do
|
||||
for %{field: field} <- info, field != :__struct__ do
|
||||
{field, term}
|
||||
end
|
||||
|
||||
@@ -678,8 +683,8 @@ defmodule Module.Types.Of do
|
||||
|
||||
Comparison operators (>, <, >=, <=, min, and max) perform structural \
|
||||
and not semantic comparison. Comparing with a struct won't give meaningful \
|
||||
results. Struct that can be compared typically define a compare/2 function \
|
||||
within their modules that can be used for semantic comparison
|
||||
results. Structs that can be compared typically define a compare/2 function \
|
||||
within their modules that can be used for semantic comparison.
|
||||
"""
|
||||
])
|
||||
}
|
||||
@@ -700,14 +705,16 @@ defmodule Module.Types.Of do
|
||||
" is undefined (module ",
|
||||
inspect(module),
|
||||
" is not available or is yet to be defined)"
|
||||
])
|
||||
]),
|
||||
group: true
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:undefined_function, module, :__struct__, 0, _exports}) do
|
||||
%{
|
||||
message:
|
||||
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)"
|
||||
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)",
|
||||
group: true
|
||||
}
|
||||
end
|
||||
|
||||
@@ -718,7 +725,8 @@ defmodule Module.Types.Of do
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is undefined or private",
|
||||
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports)
|
||||
])
|
||||
]),
|
||||
group: true
|
||||
}
|
||||
end
|
||||
|
||||
@@ -729,7 +737,8 @@ defmodule Module.Types.Of do
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is deprecated. ",
|
||||
reason
|
||||
])
|
||||
]),
|
||||
group: true
|
||||
}
|
||||
end
|
||||
|
||||
@@ -741,7 +750,8 @@ defmodule Module.Types.Of do
|
||||
inspect(module),
|
||||
" before invoking the macro ",
|
||||
Exception.format_mfa(module, fun, arity)
|
||||
])
|
||||
]),
|
||||
group: true
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -595,7 +595,7 @@ defmodule Supervisor do
|
||||
@typedoc "The supervisor reference."
|
||||
@type supervisor :: pid | name | {atom, node}
|
||||
|
||||
@typedoc "Options given to `start_link/2` and `c:init/1`."
|
||||
@typedoc "Options given to `start_link/2` and `init/2`."
|
||||
@type init_option ::
|
||||
{:strategy, strategy}
|
||||
| {:max_restarts, non_neg_integer}
|
||||
|
||||
@@ -1239,7 +1239,7 @@ defmodule System do
|
||||
This time is monotonically increasing and starts in an unspecified
|
||||
point in time.
|
||||
"""
|
||||
@spec monotonic_time(time_unit) :: integer
|
||||
@spec monotonic_time(time_unit | :native) :: integer
|
||||
def monotonic_time(unit) do
|
||||
:erlang.monotonic_time(normalize_time_unit(unit))
|
||||
end
|
||||
@@ -1265,7 +1265,7 @@ defmodule System do
|
||||
case of time warps although the VM works towards aligning
|
||||
them. This time is not monotonic.
|
||||
"""
|
||||
@spec system_time(time_unit) :: integer
|
||||
@spec system_time(time_unit | :native) :: integer
|
||||
def system_time(unit) do
|
||||
:erlang.system_time(normalize_time_unit(unit))
|
||||
end
|
||||
@@ -1316,7 +1316,7 @@ defmodule System do
|
||||
`monotonic_time/1`), gives the Erlang system time that corresponds
|
||||
to that monotonic time.
|
||||
"""
|
||||
@spec time_offset(time_unit) :: integer
|
||||
@spec time_offset(time_unit | :native) :: integer
|
||||
def time_offset(unit) do
|
||||
:erlang.time_offset(normalize_time_unit(unit))
|
||||
end
|
||||
|
||||
@@ -9,7 +9,9 @@ iex> is_binary(string)
|
||||
true
|
||||
```
|
||||
|
||||
In this chapter, we will gain clarity on what exactly binaries are, how they relate to strings, and what single-quoted values, `'like this'`, mean in Elixir. Although strings are one of the most common data types in computer languages, they are subtly complex and are often misunderstood. To understand strings in Elixir, we have to educate ourselves about [Unicode](https://en.wikipedia.org/wiki/Unicode) and character encodings, specifically the [UTF-8](https://en.wikipedia.org/wiki/UTF-8) encoding.
|
||||
In this chapter, we will gain clarity on what exactly binaries are and how they relate to strings. We will also learn about charlists, `~c"like this"`, which are often used for interoperability with Erlang.
|
||||
|
||||
Although strings are one of the most common data types in computer languages, they are subtly complex and are often misunderstood. To understand strings in Elixir, let's first discuss [Unicode](https://en.wikipedia.org/wiki/Unicode) and character encodings, specifically the [UTF-8](https://en.wikipedia.org/wiki/UTF-8) encoding.
|
||||
|
||||
## Unicode and Code Points
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ iex> {:ok, registry} = GenServer.start_link(KV.Registry, :ok)
|
||||
{:ok, #PID<0.136.0>}
|
||||
iex> GenServer.cast(registry, {:create, "shopping"})
|
||||
:ok
|
||||
iex> {:ok, bk} = GenServer.call(registry, {:lookup, "shopping"})
|
||||
iex> {:ok, bucket} = GenServer.call(registry, {:lookup, "shopping"})
|
||||
{:ok, #PID<0.174.0>}
|
||||
```
|
||||
|
||||
|
||||
@@ -213,4 +213,4 @@ Version | Deprecated feature | Replaced by (ava
|
||||
[v1.14]: https://github.com/elixir-lang/elixir/blob/v1.14/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.15]: https://github.com/elixir-lang/elixir/blob/v1.15/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.16]: https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.17]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.17]: https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md#4-hard-deprecations
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
skipped = Version.parse!("1.0.3")
|
||||
|
||||
list_contents =
|
||||
text_tags
|
||||
|> String.split()
|
||||
|> Enum.map(fn "v" <> rest -> Version.parse!(rest) end)
|
||||
|> Enum.filter(&(Version.compare(&1, skipped) == :gt))
|
||||
for(
|
||||
"v" <> rest <- String.split(text_tags),
|
||||
not String.ends_with?(rest, "-latest"),
|
||||
version = Version.parse!(rest),
|
||||
Version.compare(version, skipped) == :gt,
|
||||
do: version
|
||||
)
|
||||
|> Enum.sort({:desc, Version})
|
||||
|> Enum.map_intersperse(", ", fn version ->
|
||||
version_string = Version.to_string(version)
|
||||
|
||||
@@ -425,16 +425,16 @@ translate_with_else(Meta, [{'else', Else}], S) ->
|
||||
RaiseClause = {'->', Generated, [[RaiseVar], RaiseExpr]},
|
||||
|
||||
Clauses = elixir_erl_clauses:get_clauses('else', [{'else', Else ++ [RaiseClause]}], match),
|
||||
{TranslatedClauses, SC} = elixir_erl_clauses:clauses(Clauses, SV),
|
||||
with_else_closure(Meta, TranslatedClauses, SC).
|
||||
{TranslatedClauses, SC} = elixir_erl_clauses:clauses(Clauses, SV#elixir_erl{extra=pin_guard}),
|
||||
with_else_closure(Generated, TranslatedClauses, SC#elixir_erl{extra=SV#elixir_erl.extra}).
|
||||
|
||||
with_else_closure(Meta, TranslatedClauses, S) ->
|
||||
Ann = ?ann(Meta),
|
||||
{_, FunErlVar, SC} = elixir_erl_var:assign(Meta, S),
|
||||
{_, ArgErlVar, SA} = elixir_erl_var:assign(Meta, SC),
|
||||
FunAssign = {match, Ann, FunErlVar, {'fun', Ann, {clauses, TranslatedClauses}}},
|
||||
FunCall = {call, Ann, FunErlVar, [ArgErlVar]},
|
||||
Generated = erl_anno:set_generated(true, Ann),
|
||||
FunAssign = {match, Ann, FunErlVar, {'fun', Generated, {clauses, TranslatedClauses}}},
|
||||
FunCall = {call, Ann, FunErlVar, [ArgErlVar]},
|
||||
{{clause, Generated, [ArgErlVar], [], [FunCall]}, FunAssign, SA}.
|
||||
|
||||
translate_with_do([{'<-', Meta, [{Var, _, Ctx} = Left, Expr]} | Rest], Ann, Do, Else, S) when is_atom(Var), is_atom(Ctx) ->
|
||||
|
||||
@@ -177,7 +177,7 @@ format_snippet(Severity, Position, File, Message, Snippet, Diagnostic) ->
|
||||
|
||||
unicode:characters_to_binary(Formatted).
|
||||
|
||||
format_detail(#{detail := #{typing_traces := _}}, Message) -> [Message | "\ntyping violation found at:"];
|
||||
format_detail(#{details := #{typing_traces := _}}, Message) -> [Message | "\ntyping violation found at:"];
|
||||
format_detail(_, Message) -> Message.
|
||||
|
||||
calculate_span_length({StartLine, StartCol}, #{span := {StartLine, EndCol}}) -> EndCol - StartCol;
|
||||
|
||||
@@ -237,8 +237,10 @@ defmodule DurationTest do
|
||||
assert Duration.from_iso8601("P4Y2W3Y") == {:error, :invalid_date_component}
|
||||
assert Duration.from_iso8601("P5HT4MT3S") == {:error, :invalid_date_component}
|
||||
assert Duration.from_iso8601("P5H3HT4M") == {:error, :invalid_date_component}
|
||||
assert Duration.from_iso8601("P0.5Y") == {:error, :invalid_date_component}
|
||||
assert Duration.from_iso8601("PT1D") == {:error, :invalid_time_component}
|
||||
assert Duration.from_iso8601("PT.6S") == {:error, :invalid_time_component}
|
||||
assert Duration.from_iso8601("PT0.5H") == {:error, :invalid_time_component}
|
||||
assert Duration.from_iso8601("invalid") == {:error, :invalid_duration}
|
||||
end
|
||||
|
||||
@@ -262,6 +264,9 @@ defmodule DurationTest do
|
||||
assert Duration.from_iso8601!("PT6S") == %Duration{second: 6}
|
||||
assert Duration.from_iso8601!("PT1,6S") == %Duration{second: 1, microsecond: {600_000, 1}}
|
||||
assert Duration.from_iso8601!("PT-1.6S") == %Duration{second: -1, microsecond: {-600_000, 1}}
|
||||
assert Duration.from_iso8601!("PT0,6S") == %Duration{second: 0, microsecond: {600_000, 1}}
|
||||
assert Duration.from_iso8601!("PT-0,6S") == %Duration{second: 0, microsecond: {-600_000, 1}}
|
||||
assert Duration.from_iso8601!("-PT-0,6S") == %Duration{second: 0, microsecond: {600_000, 1}}
|
||||
assert Duration.from_iso8601!("-P10DT4H") == %Duration{day: -10, hour: -4}
|
||||
assert Duration.from_iso8601!("-P10DT-4H") == %Duration{day: -10, hour: 4}
|
||||
assert Duration.from_iso8601!("P-10D") == %Duration{day: -10}
|
||||
|
||||
@@ -172,6 +172,20 @@ defmodule Code.Normalizer.QuotedASTTest do
|
||||
) == ~s[~S"""\n"123"\n"""]
|
||||
end
|
||||
|
||||
test "regression: invalid sigil calls" do
|
||||
assert quoted_to_string(quote do: sigil_r(<<"foo", 123>>, [])) ==
|
||||
"sigil_r(<<\"foo\", 123>>, [])"
|
||||
|
||||
assert quoted_to_string(quote do: sigil_r(<<"foo">>, :invalid_modifiers)) ==
|
||||
"sigil_r(\"foo\", :invalid_modifiers)"
|
||||
|
||||
assert quoted_to_string(quote do: sigil_r(<<"foo">>, [:invalid_modifier])) ==
|
||||
"sigil_r(\"foo\", [:invalid_modifier])"
|
||||
|
||||
assert quoted_to_string(quote do: sigil_r(<<"foo">>, [])) == "~r\"foo\""
|
||||
assert quoted_to_string(quote do: sigil_r(<<"foo">>, [?a, ?b, ?c])) == "~r\"foo\"abc"
|
||||
end
|
||||
|
||||
test "tuple" do
|
||||
assert quoted_to_string(quote do: {1, 2}) == "{1, 2}"
|
||||
assert quoted_to_string(quote do: {1}) == "{1}"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
defmodule Dialyzer.WithNoReturn do
|
||||
def with_no_return(list) do
|
||||
no_return = fn -> throw(:no_return) end
|
||||
|
||||
with [] <- list do
|
||||
:ok
|
||||
else
|
||||
# note: throwing here directly wouldn't be caught in the first place,
|
||||
# calling a no_return function is what could cause an issue.
|
||||
_ -> no_return.()
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
defmodule Dialyzer.WithThrowingElse do
|
||||
def with_throwing_else(map) do
|
||||
with {:ok, foo} <- Map.fetch(map, :foo),
|
||||
false <- Enum.empty?(foo) do
|
||||
foo
|
||||
else
|
||||
# several clauses but one is a no_return
|
||||
:error -> throw(:empty_map)
|
||||
true -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -26,6 +26,7 @@ defmodule Kernel.DialyzerTest do
|
||||
:elixir,
|
||||
:elixir_env,
|
||||
:elixir_erl_pass,
|
||||
:maps,
|
||||
ArgumentError,
|
||||
Atom,
|
||||
Code,
|
||||
@@ -159,6 +160,16 @@ defmodule Kernel.DialyzerTest do
|
||||
assert_dialyze_no_warnings!(context)
|
||||
end
|
||||
|
||||
test "no warnings on with when else has a no_return type", context do
|
||||
copy_beam!(context, Dialyzer.WithNoReturn)
|
||||
assert_dialyze_no_warnings!(context)
|
||||
end
|
||||
|
||||
test "no warnings on with when multiple else clauses and one is a no_return", context do
|
||||
copy_beam!(context, Dialyzer.WithThrowingElse)
|
||||
assert_dialyze_no_warnings!(context)
|
||||
end
|
||||
|
||||
test "no warnings on defmacrop", context do
|
||||
copy_beam!(context, Dialyzer.Defmacrop)
|
||||
assert_dialyze_no_warnings!(context)
|
||||
|
||||
@@ -25,6 +25,30 @@ defmodule Kernel.WithTest do
|
||||
assert with({^key, res} <- ok(42), do: res) == 42
|
||||
end
|
||||
|
||||
test "pin matching with multiple else" do
|
||||
key = :error
|
||||
|
||||
first_else =
|
||||
with nil <- error() do
|
||||
:ok
|
||||
else
|
||||
^key -> :pinned
|
||||
_other -> :other
|
||||
end
|
||||
|
||||
assert first_else == :pinned
|
||||
|
||||
second_else =
|
||||
with nil <- ok(42) do
|
||||
:ok
|
||||
else
|
||||
^key -> :pinned
|
||||
_other -> :other
|
||||
end
|
||||
|
||||
assert second_else == :other
|
||||
end
|
||||
|
||||
test "two levels with" do
|
||||
result =
|
||||
with {:ok, n1} <- ok(11),
|
||||
|
||||
@@ -190,6 +190,16 @@ defmodule MacroTest do
|
||||
assert Code.eval_quoted(expanded) == {env.versioned_vars, []}
|
||||
end
|
||||
|
||||
test "env in :match context does not expand" do
|
||||
env = %{__ENV__ | line: 0, lexical_tracker: self(), context: :match}
|
||||
|
||||
expanded = Macro.expand_once(quote(do: __ENV__), env)
|
||||
assert expanded == quote(do: __ENV__)
|
||||
|
||||
expanded = Macro.expand_once(quote(do: __ENV__.file), env)
|
||||
assert expanded == quote(do: __ENV__.file)
|
||||
end
|
||||
|
||||
defmacro local_macro(), do: raise("ignored")
|
||||
|
||||
test "vars" do
|
||||
|
||||
@@ -382,7 +382,7 @@ defmodule Module.Types.ExprTest do
|
||||
# from: types_test.ex:LINE-2
|
||||
y = %Point{}
|
||||
|
||||
Comparison operators (>, <, >=, <=, min, and max) perform structural and not semantic comparison. Comparing with a struct won't give meaningful results. Struct that can be compared typically define a compare/2 function within their modules that can be used for semantic comparison
|
||||
Comparison operators (>, <, >=, <=, min, and max) perform structural and not semantic comparison. Comparing with a struct won't give meaningful results. Structs that can be compared typically define a compare/2 function within their modules that can be used for semantic comparison.
|
||||
"""}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -500,7 +500,7 @@ defmodule ExUnit do
|
||||
|
||||
defp maybe_repeated_run(options, seed, load_us, repeat) do
|
||||
case ExUnit.Runner.run(options, load_us) do
|
||||
{%{failures: 0}, {sync_modules, async_modules}}
|
||||
{%{failures: 0}, {async_modules, sync_modules}}
|
||||
when repeat > 0 and (sync_modules != [] or async_modules != []) ->
|
||||
ExUnit.Server.restore_modules(async_modules, sync_modules)
|
||||
|
||||
|
||||
@@ -535,15 +535,13 @@ defmodule ExUnit.Callbacks do
|
||||
test, as simply shutting down the process would cause it to be restarted
|
||||
according to its `:restart` value.
|
||||
|
||||
Another advantage is that the test process will act as both an ancestor
|
||||
as well as a caller to the supervised processes. When a process is started
|
||||
under a supervision tree, it typically populates the `$ancestors` key in
|
||||
its process dictionary with all of its ancestors, which will include the test
|
||||
process. Additionally, `start_supervised/2` will also store the test process
|
||||
in the `$callers` key of the started process, allowing tools that perform
|
||||
either ancestor or caller tracking to reach the test process. You can learn
|
||||
more about these keys in
|
||||
[the `Task` module](`Task#module-ancestor-and-caller-tracking`).
|
||||
Finally, since Elixir v1.17.0, the test supervisor has both `$ancestors`
|
||||
and `$callers` key in its process dictionary pointing to the test process.
|
||||
This means developers can invoke `Process.get(:"$callers", [])` in their
|
||||
`start_link` function and forward it to the spawned process, which may set
|
||||
`Process.put(:"$callers", callers)` during its initialization. This may be
|
||||
useful in projects who track process ownership during tests. You can learn
|
||||
more about these keys in [the `Task` module](`Task#module-ancestor-and-caller-tracking`).
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@spec start_supervised(Supervisor.child_spec() | module | {module, term}, keyword) ::
|
||||
|
||||
@@ -256,6 +256,28 @@ defmodule ExUnit.AssertionsTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "assert match with __ENV__ in the pattern" do
|
||||
message =
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
assert_raise CompileError, fn ->
|
||||
Code.eval_string("""
|
||||
defmodule EnvMatch do
|
||||
import ExUnit.Assertions
|
||||
|
||||
def run do
|
||||
assert __ENV__ = %{}
|
||||
end
|
||||
end
|
||||
""")
|
||||
end
|
||||
end)
|
||||
|
||||
assert message =~ "invalid pattern in match, __ENV__ is not allowed in matches"
|
||||
after
|
||||
:code.purge(EnvMatch)
|
||||
:code.delete(EnvMatch)
|
||||
end
|
||||
|
||||
test "assert match?" do
|
||||
true = assert match?({2, 1}, Value.tuple())
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ defmodule IEx.Helpers do
|
||||
reenable_tasks(config)
|
||||
|
||||
force? = Keyword.get(options, :force, false)
|
||||
args = ["--purge-consolidation-path-if-stale", "--return-errors", consolidation]
|
||||
args = ["--purge-consolidation-path-if-stale", consolidation, "--return-errors"]
|
||||
args = if force?, do: ["--force" | args], else: args
|
||||
|
||||
{result, _} = Mix.Task.run("compile", args)
|
||||
|
||||
@@ -298,7 +298,7 @@ defmodule Logger.Translator do
|
||||
client_info: client,
|
||||
name: name,
|
||||
reason: {kind, reason, stack},
|
||||
state: {state, data},
|
||||
state: state,
|
||||
queue: queue,
|
||||
postponed: postponed,
|
||||
callback_mode: callback_mode,
|
||||
@@ -328,8 +328,6 @@ defmodule Logger.Translator do
|
||||
msg,
|
||||
"\nState: ",
|
||||
inspect(state, inspect_opts),
|
||||
"\nData: ",
|
||||
inspect(data, inspect_opts),
|
||||
"\nCallback mode: ",
|
||||
"#{inspect(callback_mode, inspect_opts)}, state_enter: #{state_enter?}"
|
||||
| format_client_info(client)
|
||||
|
||||
@@ -100,6 +100,28 @@ defmodule Logger.TranslatorTest do
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MyGenStatemHandleEvent do
|
||||
@behaviour :gen_statem
|
||||
|
||||
@impl true
|
||||
def callback_mode, do: :handle_event_function
|
||||
|
||||
@impl true
|
||||
def init(state) do
|
||||
{:ok, :no_state, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event({:call, _}, :error, :no_state, _data) do
|
||||
raise "oops"
|
||||
end
|
||||
|
||||
@impl :gen_statem
|
||||
def format_status(_opts, [_pdict, _, state]) do
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MyBridge do
|
||||
@behaviour :supervisor_bridge
|
||||
|
||||
@@ -393,7 +415,7 @@ defmodule Logger.TranslatorTest do
|
||||
assert {%RuntimeError{message: "oops"}, [_ | _]} = process_metadata[:crash_reason]
|
||||
|
||||
refute Map.has_key?(gen_statem_metadata, :initial_call)
|
||||
assert process_metadata[:initial_call] == {Logger.TranslatorTest.MyGenStatem, :init, 1}
|
||||
assert process_metadata[:initial_call] == {MyGenStatem, :init, 1}
|
||||
|
||||
refute Map.has_key?(gen_statem_metadata, :registered_name)
|
||||
refute Map.has_key?(process_metadata, :registered_name)
|
||||
@@ -445,7 +467,7 @@ defmodule Logger.TranslatorTest do
|
||||
assert capture_log(:debug, fn ->
|
||||
catch_exit(:gen_statem.call(pid, :error))
|
||||
end) =~ """
|
||||
[:ok, :ok, :ok, ...]
|
||||
State: {:started, [:ok, ...]}
|
||||
"""
|
||||
after
|
||||
Application.put_env(:logger, :translator_inspect_opts, [])
|
||||
@@ -462,8 +484,7 @@ defmodule Logger.TranslatorTest do
|
||||
.*
|
||||
Queue: .*
|
||||
Postponed: \[\]
|
||||
State: :started
|
||||
Data: :ok
|
||||
State: {:started, :ok}
|
||||
Callback mode: :state_functions, state_enter: false
|
||||
Client #PID<\d+\.\d+\.\d+> is alive
|
||||
.*
|
||||
@@ -488,8 +509,7 @@ defmodule Logger.TranslatorTest do
|
||||
.*
|
||||
Queue: .*
|
||||
Postponed: \[\]
|
||||
State: :started
|
||||
Data: :ok
|
||||
State: {:started, :ok}
|
||||
Callback mode: :state_functions, state_enter: false
|
||||
Client :named_client is alive
|
||||
.*
|
||||
@@ -513,8 +533,7 @@ defmodule Logger.TranslatorTest do
|
||||
.*
|
||||
Queue: .*
|
||||
Postponed: \[\]
|
||||
State: :started
|
||||
Data: :ok
|
||||
State: {:started, :ok}
|
||||
Callback mode: :state_functions, state_enter: false
|
||||
Client #PID<\d+\.\d+\.\d+> is dead
|
||||
"""s
|
||||
@@ -533,8 +552,7 @@ defmodule Logger.TranslatorTest do
|
||||
.*
|
||||
Queue: .*
|
||||
Postponed: \[\]
|
||||
State: :started
|
||||
Data: :ok
|
||||
State: {:started, :ok}
|
||||
Callback mode: :state_functions, state_enter: false
|
||||
"""s
|
||||
end
|
||||
@@ -552,6 +570,34 @@ defmodule Logger.TranslatorTest do
|
||||
assert_receive {:event, {:string, ["Process " | _]}, _process_metadata}
|
||||
end
|
||||
|
||||
test "translates :gen_statem crashes when callback_mode is :handle_event_function" do
|
||||
{:ok, pid} = :gen_statem.start(MyGenStatemHandleEvent, :ok, [])
|
||||
|
||||
assert capture_log(:debug, fn ->
|
||||
catch_exit(:gen_statem.call(pid, :error))
|
||||
end) =~ ~r"""
|
||||
\[error\] :gen_statem #PID<\d+\.\d+\.\d+> terminating
|
||||
\*\* \(RuntimeError\) oops
|
||||
.*
|
||||
Queue: .*
|
||||
Postponed: \[\]
|
||||
State: :ok
|
||||
Callback mode: .*, state_enter: false
|
||||
"""s
|
||||
|
||||
assert_receive {:event, {:string, [[":gen_statem " <> _ | _] | _]}, gen_statem_metadata}
|
||||
assert_receive {:event, {:string, ["Process " | _]}, process_metadata}
|
||||
|
||||
assert {%RuntimeError{message: "oops"}, [_ | _]} = gen_statem_metadata[:crash_reason]
|
||||
assert {%RuntimeError{message: "oops"}, [_ | _]} = process_metadata[:crash_reason]
|
||||
|
||||
refute Map.has_key?(gen_statem_metadata, :initial_call)
|
||||
assert process_metadata[:initial_call] == {MyGenStatemHandleEvent, :init, 1}
|
||||
|
||||
refute Map.has_key?(gen_statem_metadata, :registered_name)
|
||||
refute Map.has_key?(process_metadata, :registered_name)
|
||||
end
|
||||
|
||||
test "translates Task crashes" do
|
||||
{:ok, pid} = Task.start_link(__MODULE__, :task, [self()])
|
||||
parent = self()
|
||||
@@ -902,13 +948,13 @@ defmodule Logger.TranslatorTest do
|
||||
test "translates Supervisor progress" do
|
||||
{:ok, pid} = Supervisor.start_link([], strategy: :one_for_one)
|
||||
|
||||
assert capture_log(:info, fn ->
|
||||
assert capture_log(:debug, fn ->
|
||||
ref = Process.monitor(pid)
|
||||
Supervisor.start_child(pid, worker(Task, [__MODULE__, :sleep, [self()]]))
|
||||
Process.exit(pid, :normal)
|
||||
receive do: ({:DOWN, ^ref, _, _, _} -> :ok)
|
||||
end) =~ ~r"""
|
||||
\[info\] Child Task of Supervisor #PID<\d+\.\d+\.\d+> \(Supervisor\.Default\) started
|
||||
\[(debug|info)\] Child Task of Supervisor #PID<\d+\.\d+\.\d+> \(Supervisor\.Default\) started
|
||||
Pid: #PID<\d+\.\d+\.\d+>
|
||||
Start Call: Task.start_link\(Logger.TranslatorTest, :sleep, \[#PID<\d+\.\d+\.\d+>\]\)
|
||||
"""
|
||||
@@ -917,36 +963,36 @@ defmodule Logger.TranslatorTest do
|
||||
test "translates Supervisor progress with name" do
|
||||
{:ok, pid} = Supervisor.start_link([], strategy: :one_for_one, name: __MODULE__)
|
||||
|
||||
assert capture_log(:info, fn ->
|
||||
assert capture_log(:debug, fn ->
|
||||
ref = Process.monitor(pid)
|
||||
Supervisor.start_child(pid, worker(Task, [__MODULE__, :sleep, [self()]]))
|
||||
Process.exit(pid, :normal)
|
||||
receive do: ({:DOWN, ^ref, _, _, _} -> :ok)
|
||||
end) =~ ~r"""
|
||||
\[info\] Child Task of Supervisor Logger.TranslatorTest started
|
||||
\[(debug|info)\] Child Task of Supervisor Logger.TranslatorTest started
|
||||
"""
|
||||
|
||||
{:ok, pid} = Supervisor.start_link([], strategy: :one_for_one, name: {:global, __MODULE__})
|
||||
|
||||
assert capture_log(:info, fn ->
|
||||
assert capture_log(:debug, fn ->
|
||||
ref = Process.monitor(pid)
|
||||
Supervisor.start_child(pid, worker(Task, [__MODULE__, :sleep, [self()]]))
|
||||
Process.exit(pid, :normal)
|
||||
receive do: ({:DOWN, ^ref, _, _, _} -> :ok)
|
||||
end) =~ ~r"""
|
||||
\[info\] Child Task of Supervisor Logger.TranslatorTest started
|
||||
\[(debug|info)\] Child Task of Supervisor Logger.TranslatorTest started
|
||||
"""
|
||||
|
||||
{:ok, pid} =
|
||||
Supervisor.start_link([], strategy: :one_for_one, name: {:via, :global, __MODULE__})
|
||||
|
||||
assert capture_log(:info, fn ->
|
||||
assert capture_log(:debug, fn ->
|
||||
ref = Process.monitor(pid)
|
||||
Supervisor.start_child(pid, worker(Task, [__MODULE__, :sleep, [self()]]))
|
||||
Process.exit(pid, :normal)
|
||||
receive do: ({:DOWN, ^ref, _, _, _} -> :ok)
|
||||
end) =~ ~r"""
|
||||
\[info\] Child Task of Supervisor Logger.TranslatorTest started
|
||||
\[(debug|info)\] Child Task of Supervisor Logger.TranslatorTest started
|
||||
"""
|
||||
end
|
||||
|
||||
|
||||
@@ -609,8 +609,8 @@ defmodule Mix.Compilers.Elixir do
|
||||
# within the dependency, they will be recompiled. However, export
|
||||
# and runtime dependencies won't have recompiled so we need to
|
||||
# propagate them to the parent app.
|
||||
{dep_modules, _, _} =
|
||||
fixpoint_runtime_modules(manifest_sources, Map.from_keys(dep_modules, true))
|
||||
dep_modules =
|
||||
fixpoint_non_compile_modules(manifest_sources, Map.from_keys(dep_modules, true))
|
||||
|
||||
old_exports = Map.get(deps_exports, app, %{})
|
||||
|
||||
@@ -654,21 +654,19 @@ defmodule Mix.Compilers.Elixir do
|
||||
end
|
||||
end
|
||||
|
||||
defp fixpoint_runtime_modules(sources, modules) when modules != %{} do
|
||||
fixpoint_runtime_modules(Map.to_list(sources), modules, false, [], [], sources)
|
||||
defp fixpoint_non_compile_modules(sources, modules) when modules != %{} do
|
||||
fixpoint_non_compile_modules(Map.to_list(sources), modules, false, [])
|
||||
end
|
||||
|
||||
defp fixpoint_runtime_modules(sources, modules) do
|
||||
{modules, [], sources}
|
||||
defp fixpoint_non_compile_modules(_sources, modules) do
|
||||
modules
|
||||
end
|
||||
|
||||
defp fixpoint_runtime_modules(
|
||||
[{source_path, source_entry} = pair | sources],
|
||||
defp fixpoint_non_compile_modules(
|
||||
[{_source_path, source_entry} = pair | sources],
|
||||
modules,
|
||||
new?,
|
||||
pending_sources,
|
||||
acc_modules,
|
||||
acc_sources
|
||||
pending_sources
|
||||
) do
|
||||
source(export_references: export_refs, runtime_references: runtime_refs) = source_entry
|
||||
|
||||
@@ -676,24 +674,19 @@ defmodule Mix.Compilers.Elixir do
|
||||
new_modules = Enum.reject(source(source_entry, :modules), &Map.has_key?(modules, &1))
|
||||
modules = Enum.reduce(new_modules, modules, &Map.put(&2, &1, true))
|
||||
new? = new? or new_modules != []
|
||||
acc_modules = new_modules ++ acc_modules
|
||||
|
||||
acc_sources =
|
||||
Map.replace!(acc_sources, source_path, source(source_entry, runtime_warnings: []))
|
||||
|
||||
fixpoint_runtime_modules(sources, modules, new?, pending_sources, acc_modules, acc_sources)
|
||||
fixpoint_non_compile_modules(sources, modules, new?, pending_sources)
|
||||
else
|
||||
pending_sources = [pair | pending_sources]
|
||||
fixpoint_runtime_modules(sources, modules, new?, pending_sources, acc_modules, acc_sources)
|
||||
fixpoint_non_compile_modules(sources, modules, new?, pending_sources)
|
||||
end
|
||||
end
|
||||
|
||||
defp fixpoint_runtime_modules([], modules, new?, pending_sources, acc_modules, acc_sources)
|
||||
defp fixpoint_non_compile_modules([], modules, new?, pending_sources)
|
||||
when new? == false or pending_sources == [],
|
||||
do: {modules, acc_modules, acc_sources}
|
||||
do: modules
|
||||
|
||||
defp fixpoint_runtime_modules([], modules, true, pending_sources, acc_modules, acc_sources),
|
||||
do: fixpoint_runtime_modules(pending_sources, modules, false, [], acc_modules, acc_sources)
|
||||
defp fixpoint_non_compile_modules([], modules, true, pending_sources),
|
||||
do: fixpoint_non_compile_modules(pending_sources, modules, false, [])
|
||||
|
||||
defp exports_md5(module, use_attributes?) do
|
||||
cond do
|
||||
@@ -1068,7 +1061,7 @@ defmodule Mix.Compilers.Elixir do
|
||||
end
|
||||
end
|
||||
|
||||
defp each_cycle(runtime_modules, compile_path, timestamp, state) do
|
||||
defp each_cycle(stale_modules, compile_path, timestamp, state) do
|
||||
{modules, _exports, sources, changed, pending_modules, stale_exports} = state
|
||||
|
||||
{pending_modules, exports, changed} =
|
||||
@@ -1081,11 +1074,33 @@ defmodule Mix.Compilers.Elixir do
|
||||
end
|
||||
|
||||
if changed == [] do
|
||||
# We merge runtime_modules (which is a map of %{module => true}) into
|
||||
# a map of modules (which is a map of %{module => record}). This is fine
|
||||
# since fixpoint_runtime_modules only cares about map keys.
|
||||
{_, runtime_modules, sources} =
|
||||
fixpoint_runtime_modules(sources, Map.merge(modules, runtime_modules))
|
||||
# We merge stale_modules (which is a map of %{module => true} that the user changed)
|
||||
# into a map of modules we compiled (which is a map of %{module => record}). This is
|
||||
# fine because we only care about the keys.
|
||||
changed_modules = Map.merge(modules, stale_modules)
|
||||
|
||||
# Now we do a simple pass finding anything that directly depends on the modules that
|
||||
# changed. We don't need to compute a fixpoint, because now only the directly affected
|
||||
# matter.
|
||||
{sources, runtime_modules} =
|
||||
Enum.reduce(sources, {sources, []}, fn
|
||||
{source_path, source_entry}, {acc_sources, acc_modules} ->
|
||||
source(export_references: export_refs, runtime_references: runtime_refs) =
|
||||
source_entry
|
||||
|
||||
if has_any_key?(changed_modules, export_refs) or
|
||||
has_any_key?(changed_modules, runtime_refs) do
|
||||
acc_sources =
|
||||
Map.replace!(acc_sources, source_path, source(source_entry, runtime_warnings: []))
|
||||
|
||||
new_modules =
|
||||
Enum.reject(source(source_entry, :modules), &Map.has_key?(changed_modules, &1))
|
||||
|
||||
{acc_sources, new_modules ++ acc_modules}
|
||||
else
|
||||
{acc_sources, acc_modules}
|
||||
end
|
||||
end)
|
||||
|
||||
runtime_paths =
|
||||
Enum.map(runtime_modules, &{&1, Path.join(compile_path, Atom.to_string(&1) <> ".beam")})
|
||||
|
||||
+18
-16
@@ -9,7 +9,7 @@ defmodule Mix.SCM.Git do
|
||||
|
||||
@impl true
|
||||
def format(opts) do
|
||||
if rev = get_opts_rev(opts) do
|
||||
if rev = opts[:ref] || opts[:branch] || opts[:tag] do
|
||||
"#{redact_uri(opts[:git])} - #{rev}"
|
||||
else
|
||||
redact_uri(opts[:git])
|
||||
@@ -22,7 +22,7 @@ defmodule Mix.SCM.Git do
|
||||
{:git, _, lock_rev, lock_opts} ->
|
||||
lock = String.slice(lock_rev, 0, 7)
|
||||
|
||||
case Enum.find_value([:branch, :ref, :tag], &List.keyfind(lock_opts, &1, 0)) do
|
||||
case Enum.find_value([:ref, :branch, :tag], &List.keyfind(lock_opts, &1, 0)) do
|
||||
{:ref, _} -> lock <> " (ref)"
|
||||
{key, val} -> lock <> " (#{key}: #{val})"
|
||||
nil -> lock
|
||||
@@ -125,18 +125,18 @@ defmodule Mix.SCM.Git do
|
||||
sparse_toggle(opts)
|
||||
update_origin(opts[:git])
|
||||
|
||||
rev = get_lock_rev(opts[:lock], opts) || get_opts_rev(opts)
|
||||
|
||||
# Fetch external data
|
||||
branch_or_tag = opts[:branch] || opts[:tag]
|
||||
|
||||
["--git-dir=.git", "fetch", "--force", "--quiet"]
|
||||
|> Kernel.++(progress_switch(git_version()))
|
||||
|> Kernel.++(tags_switch(opts[:tag]))
|
||||
|> Kernel.++(depth_switch(opts[:depth]))
|
||||
|> Kernel.++(if rev, do: ["origin", rev], else: [])
|
||||
|> Kernel.++(if branch_or_tag, do: ["origin", branch_or_tag], else: [])
|
||||
|> git!()
|
||||
|
||||
# Migrate the Git repo
|
||||
rev = rev || default_branch()
|
||||
rev = get_lock_rev(opts[:lock], opts) || opts[:ref] || branch_or_tag || default_branch()
|
||||
git!(["--git-dir=.git", "checkout", "--quiet", rev])
|
||||
|
||||
if opts[:submodules] do
|
||||
@@ -250,18 +250,24 @@ defmodule Mix.SCM.Git do
|
||||
end
|
||||
|
||||
defp validate_depth(opts) do
|
||||
case Keyword.take(opts, [:depth]) do
|
||||
[] ->
|
||||
case Keyword.take(opts, [:depth, :ref]) do
|
||||
[_, _] ->
|
||||
Mix.raise(
|
||||
"Cannot specify :depth and :ref at the same time. " <>
|
||||
"Error on Git dependency: #{redact_uri(opts[:git])}"
|
||||
)
|
||||
|
||||
[depth: depth] when is_integer(depth) and depth > 0 ->
|
||||
opts
|
||||
|
||||
[{:depth, depth}] when is_integer(depth) and depth > 0 ->
|
||||
opts
|
||||
|
||||
invalid_depth ->
|
||||
[depth: invalid_depth] ->
|
||||
Mix.raise(
|
||||
"The depth must be a positive integer, and be specified only once, got: #{inspect(invalid_depth)}. " <>
|
||||
"Error on Git dependency: #{redact_uri(opts[:git])}"
|
||||
)
|
||||
|
||||
_ ->
|
||||
opts
|
||||
end
|
||||
end
|
||||
|
||||
@@ -290,10 +296,6 @@ defmodule Mix.SCM.Git do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_opts_rev(opts) do
|
||||
opts[:branch] || opts[:ref] || opts[:tag]
|
||||
end
|
||||
|
||||
defp redact_uri(git) do
|
||||
case URI.parse(git) do
|
||||
%{userinfo: nil} -> git
|
||||
|
||||
@@ -125,7 +125,8 @@ defmodule Mix.Tasks.Deps do
|
||||
* `:depth` *(since v1.17.0)* - creates a shallow clone of the Git repository,
|
||||
limiting the history to the specified number of commits. This can significantly
|
||||
improve clone speed for large repositories when full history is not needed.
|
||||
The value must be a positive integer, typically `1`.
|
||||
The value must be a positive integer, typically `1`. Cannot be used with the
|
||||
`:ref` option.
|
||||
|
||||
If your Git repository requires authentication, such as basic username:password
|
||||
HTTP authentication via URLs, it can be achieved via Git configuration, keeping
|
||||
|
||||
@@ -402,8 +402,8 @@ defmodule Mix.Tasks.Release do
|
||||
|
||||
If you are setting this option manually, we recommend the cookie option
|
||||
to be a long and randomly generated string, such as:
|
||||
`Base.url_encode64(:crypto.strong_rand_bytes(40))`. We also recommend to restrict
|
||||
the characters in the cookie to the subset returned by `Base.url_encode64/1`.
|
||||
`Base.encode32(:crypto.strong_rand_bytes(40))`. We also recommend restricting
|
||||
the characters in the cookie to only alphanumeric characters and underscore.
|
||||
|
||||
* `:validate_compile_env` - by default a release will match all runtime
|
||||
configuration against any configuration that was marked at compile time
|
||||
@@ -833,9 +833,9 @@ defmodule Mix.Tasks.Release do
|
||||
|
||||
* `RELEASE_DISTRIBUTION` - how do we want to run the distribution.
|
||||
May be `name` (long names), `sname` (short names) or `none`
|
||||
(distribution is not started automatically). Defaults to
|
||||
`sname` which allows access only within the current system.
|
||||
`name` allows external connections
|
||||
(distribution is not started automatically). Defaults to `sname`.
|
||||
When connecting nodes across hosts, you typically want to set
|
||||
this to `name` (required to use IPs as host names)
|
||||
|
||||
* `RELEASE_BOOT_SCRIPT` - the name of the boot script to use when starting
|
||||
the release. This script is used when running commands such as `start` and
|
||||
|
||||
@@ -53,7 +53,7 @@ defmodule Mix.Tasks.Release.Init do
|
||||
|
||||
## Enable deployment without epmd
|
||||
## (requires changing both vm.args and remote.vm.args)
|
||||
##-start_epmd false -erl_epmd_port 6789#{remote? && " -dist_listen false"}
|
||||
##-start_epmd false -erl_epmd_port 6789#{if(remote?, do: " -dist_listen false")}
|
||||
"""
|
||||
|
||||
@doc false
|
||||
@@ -136,7 +136,7 @@ defmodule Mix.Tasks.Release.Init do
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "ERROR: Expected sname, name, or none in RELEASE_DISTRIBUTION, got: $RELEASE_DISTRIBUTION" >&2
|
||||
echo "ERROR: Expected RELEASE_DISTRIBUTION to be sname, name, or none, got: $RELEASE_DISTRIBUTION" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -280,7 +280,7 @@ defmodule Mix.Tasks.Release.Init do
|
||||
rem set RELEASE_MODE=interactive
|
||||
|
||||
rem Set the release to work across nodes.
|
||||
rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
|
||||
rem RELEASE_DISTRIBUTION must be sname (local), name (distributed) or none.
|
||||
rem set RELEASE_DISTRIBUTION=name
|
||||
rem set RELEASE_NODE=<%= @release.name %>
|
||||
"""
|
||||
@@ -314,13 +314,33 @@ defmodule Mix.Tasks.Release.Init do
|
||||
if not defined RELEASE_BOOT_SCRIPT_CLEAN (set RELEASE_BOOT_SCRIPT_CLEAN=start_clean)
|
||||
if not defined RELEASE_SYS_CONFIG (set RELEASE_SYS_CONFIG=!REL_VSN_DIR!\sys)
|
||||
|
||||
if "!RELEASE_DISTRIBUTION!" == "none" (
|
||||
rem
|
||||
) else if "!RELEASE_DISTRIBUTION!" == "name" (
|
||||
rem
|
||||
) else if "!RELEASE_DISTRIBUTION!" == "sname" (
|
||||
rem
|
||||
) else (
|
||||
echo ERROR: Expected RELEASE_DISTRIBUTION to be sname, name, or none, got: !RELEASE_DISTRIBUTION!
|
||||
exit /B 1
|
||||
)
|
||||
|
||||
if "!RELEASE_MODE!" == "embedded" (
|
||||
rem
|
||||
) else if "!RELEASE_MODE!" == "interactive" (
|
||||
rem
|
||||
) else (
|
||||
echo ERROR: Expected RELEASE_MODE to be embedded or interactive, got: !RELEASE_MODE!
|
||||
exit /B 1
|
||||
)
|
||||
|
||||
if "%~1" == "start" (set "REL_EXEC=elixir" && set "REL_EXTRA=--no-halt" && set "REL_GOTO=start")
|
||||
if "%~1" == "start_iex" (set "REL_EXEC=iex" && set "REL_EXTRA=--werl" && set "REL_GOTO=start")
|
||||
if "%~1" == "install" (set "REL_GOTO=install")
|
||||
if "%~1" == "eval" (
|
||||
if "%~2" == "" (
|
||||
echo ERROR: EVAL expects an expression as argument
|
||||
goto end
|
||||
exit /B 1
|
||||
)
|
||||
set "REL_GOTO=eval"
|
||||
)
|
||||
@@ -348,7 +368,7 @@ defmodule Mix.Tasks.Release.Init do
|
||||
if "%~1" == "rpc" (
|
||||
if "%~2" == "" (
|
||||
echo ERROR: RPC expects an expression as argument
|
||||
goto end
|
||||
exit /B 1
|
||||
)
|
||||
set "REL_RPC=%~2"
|
||||
goto rpc
|
||||
@@ -369,7 +389,10 @@ defmodule Mix.Tasks.Release.Init do
|
||||
echo pid Prints the operating system PID of the running system via a remote command
|
||||
echo version Prints the release name and version to be booted
|
||||
echo.
|
||||
if not "%~1" == "" (echo ERROR: Unknown command %~1)
|
||||
if not "%~1" == "" (
|
||||
echo ERROR: Unknown command %~1
|
||||
exit /B 1
|
||||
)
|
||||
goto end
|
||||
|
||||
:start
|
||||
@@ -427,7 +450,7 @@ defmodule Mix.Tasks.Release.Init do
|
||||
if "!RELEASE_DISTRIBUTION!" == "none" (
|
||||
set RELEASE_DISTRIBUTION_FLAG=
|
||||
) else (
|
||||
set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "rem-!RANDOM!-!RELEASE_NODE!"
|
||||
set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "rpc-!RANDOM!-!RELEASE_NODE!"
|
||||
)
|
||||
|
||||
"!REL_VSN_DIR!\elixir.bat" ^
|
||||
@@ -452,7 +475,7 @@ defmodule Mix.Tasks.Release.Init do
|
||||
|
||||
if "!RELEASE_DISTRIBUTION!" == "none" (
|
||||
echo ERROR: RELEASE_DISTRIBUTION is required in install command
|
||||
goto end
|
||||
exit /B 1
|
||||
)
|
||||
|
||||
"!ERLSRV!" add "!RELEASE_NAME!_!RELEASE_NAME!" ^
|
||||
|
||||
+263
-131
@@ -14,16 +14,111 @@ defmodule Mix.Tasks.Xref do
|
||||
|
||||
$ mix xref MODE
|
||||
|
||||
All available modes are discussed below.
|
||||
All available modes are discussed below, after a brief
|
||||
introduction to xref.
|
||||
|
||||
This task is automatically re-enabled, so you can print
|
||||
information multiple times in the same Mix invocation.
|
||||
|
||||
## mix xref callers MODULE
|
||||
## A brief introduction to xref
|
||||
|
||||
Prints all callers of the given module. Example:
|
||||
The goal of `xref` is to analyze the dependencies between modules
|
||||
and files. It is most commonly used to find problematic areas where
|
||||
touching one file in a project causes a large subset of the project
|
||||
to recompile. The most common cause of these problems are the so-called
|
||||
"compile-connected" files. Those are files you depend on at compile-time
|
||||
(for example, by invoking its macro or using it in the body of amodule)
|
||||
which also have their own dependencies.
|
||||
|
||||
$ mix xref callers MyMod
|
||||
Therefore, if your goal is to reduce recompilations, the first step is to run:
|
||||
|
||||
$ mix xref graph --format stats --label compile-connected
|
||||
|
||||
This command will show general information about the project, but
|
||||
focus on compile-connected dependencies. In the stats, you will see
|
||||
the following report:
|
||||
|
||||
Top 10 files with most incoming dependencies:
|
||||
* lib/livebook_web.ex (97)
|
||||
* lib/livebook/config.ex (3)
|
||||
* proto/lib/livebook_proto/deployment_group.pb.ex (2)
|
||||
* lib/livebook_web/plugs/memory_provider.ex (2)
|
||||
* proto/lib/livebook_proto/user_connected.pb.ex (1)
|
||||
|
||||
You can see the first file, "lib/livebook_web.ex", is depended on by 97
|
||||
other files and, because we are using compile-connected, it also means
|
||||
that "lib/livebook_web.ex" itself has its own dependencies. We can find
|
||||
which files depend on "lib/livebook_web.ex" at compile time like this:
|
||||
|
||||
$ mix xref graph --sink lib/livebook_web.ex --label compile --only-nodes
|
||||
|
||||
And you can find the files lib/livebook_web.ex depends on like this:
|
||||
|
||||
$ mix xref graph --source lib/livebook_web.ex --only-nodes
|
||||
|
||||
The trouble here is precisely that, if any of the files in the latter
|
||||
command changes, all of the files in the first command will be recompiled,
|
||||
because compile time dependencies are transitive.
|
||||
|
||||
Having compile time dependencies is a common feature in Elixir projects.
|
||||
However, the modules you depend on at compile-time must avoid runtime
|
||||
dependencies within the same project. You can understand all of the
|
||||
dependencies of a given file by running:
|
||||
|
||||
$ mix xref trace lib/livebook_web.ex
|
||||
|
||||
The command above will output three types of dependencies, which we
|
||||
detail next.
|
||||
|
||||
### Dependency types
|
||||
|
||||
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 (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).
|
||||
This type of transitive compile-time dependencies, such as `A`
|
||||
depending on `C` at compile-time through `B`, are called compile-connected.
|
||||
|
||||
Export dependencies are compile time dependencies on the module API,
|
||||
namely structs and its public definitions. For example, if you import
|
||||
a module but only use its functions, it is an export dependency. If
|
||||
you use a struct, it is an export dependency too. Export dependencies
|
||||
are only recompiled if the module API changes. Note, however, that compile
|
||||
time dependencies have higher precedence than exports. Therefore if
|
||||
you import a module and use its macros, it is a compile time dependency.
|
||||
|
||||
Runtime dependencies are added whenever you invoke another module
|
||||
inside a function. Modules with runtime dependencies do not have
|
||||
to be compiled when the callee changes, unless there is a transitive
|
||||
compile or an outdated export time dependency between them.
|
||||
|
||||
Over the next sections, we will explain what which `mix xref` command
|
||||
does in detail.
|
||||
|
||||
## mix xref trace FILE
|
||||
|
||||
@@ -87,14 +182,13 @@ defmodule Mix.Tasks.Xref do
|
||||
* `--exclude` - path to exclude. Can be repeated to exclude multiple paths.
|
||||
|
||||
* `--label` - only shows relationships with the given label.
|
||||
The labels are "compile", "export" and "runtime". By default,
|
||||
the `--label` option simply filters the printed graph to show
|
||||
only relationships with the given label. You can pass `--only-direct`
|
||||
to trim the graph to only the nodes that have the direct
|
||||
relationship given by label. There is also a special label
|
||||
called "compile-connected" that keeps only compile-time files
|
||||
with at least one transitive dependency. See "Dependency types"
|
||||
section below.
|
||||
The labels are "compile", "export" and "runtime". By default, the `--label`
|
||||
option does not change how the graph is computed, it simply filters the
|
||||
printed graph to show only relationships with the given label. However,
|
||||
you can pass `--only-direct` to trim the graph to only the nodes that
|
||||
have the direct relationship given by label. There is also a special
|
||||
label called "compile-connected" that keeps only compile-time files with
|
||||
at least one transitive dependency. See "Dependency types" section below.
|
||||
|
||||
* `--group` - provide comma-separated paths to consider as a group. Dependencies
|
||||
from and into multiple files of the group are considered a single dependency.
|
||||
@@ -148,8 +242,8 @@ defmodule Mix.Tasks.Xref do
|
||||
those options with `--label` and `--only-nodes` to get all files that exhibit a certain
|
||||
property, for example:
|
||||
|
||||
# To show all compile-time relationships
|
||||
$ mix xref graph --label compile
|
||||
# To show all compile-connected relationships
|
||||
$ mix xref graph --label compile-connected
|
||||
|
||||
# To get the tree that depend on lib/foo.ex at compile time
|
||||
$ mix xref graph --label compile --sink lib/foo.ex
|
||||
@@ -163,6 +257,9 @@ defmodule Mix.Tasks.Xref do
|
||||
# To show general statistics about the graph
|
||||
$ mix xref graph --format stats
|
||||
|
||||
# To show all cycles with at least one compile-time dependency
|
||||
$ mix xref graph --format cycles --label compile-connected
|
||||
|
||||
### Understanding the printed graph
|
||||
|
||||
When `mix xref graph` runs, it will print a tree of the following
|
||||
@@ -204,10 +301,9 @@ defmodule Mix.Tasks.Xref do
|
||||
|
||||
The `--label compile` flag removes all non-compile dependencies. However,
|
||||
this can be misleading because having direct compile time dependencies is
|
||||
not necessarily an issue. The biggest concern, as mentioned above, are the
|
||||
transitive compile time dependencies. You can get all compile time
|
||||
dependencies that cause transitive compile time dependencies by using
|
||||
`--label compile-connected`:
|
||||
not necessarily an issue. The biggest concern are the transitive compile
|
||||
time dependencies. You can get all compile time dependencies that cause
|
||||
transitive compile time dependencies by using `--label compile-connected`:
|
||||
|
||||
$ mix xref graph --label compile-connected
|
||||
lib/a.ex
|
||||
@@ -235,56 +331,35 @@ defmodule Mix.Tasks.Xref do
|
||||
command will list all files from all umbrella children, without
|
||||
any namespacing.
|
||||
|
||||
### Dependency types
|
||||
### Understanding the printed cycle
|
||||
|
||||
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 (or any runtime dependency of the callee changes).
|
||||
Let's see an example:
|
||||
If you run `mix xref graph --format cycle`, Elixir will print cycles
|
||||
of shape:
|
||||
|
||||
# lib/a.ex
|
||||
defmodule A do
|
||||
@hello B.hello()
|
||||
def hello, do: @hello
|
||||
end
|
||||
Cycle of length 3:
|
||||
|
||||
# lib/b.ex
|
||||
defmodule B do
|
||||
def hello, do: "hello"
|
||||
def world, do: C.world()
|
||||
end
|
||||
lib/c.ex
|
||||
lib/b.ex
|
||||
lib/a.ex
|
||||
|
||||
# lib/c.ex
|
||||
defmodule C do
|
||||
def world, do: "world"
|
||||
end
|
||||
The cycles are given in order: `c.ex` depends on `b.ex` which depends
|
||||
on `a.ex` which depends on `c.ex`. In particular, you want to avoid
|
||||
cycles with compile dependencies in there. You can find those cycles
|
||||
with:
|
||||
|
||||
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`.
|
||||
$ mix xref graph --format cycles --label compile-connected
|
||||
|
||||
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`.
|
||||
Which may look like this:
|
||||
|
||||
Export dependencies are compile time dependencies on the module API,
|
||||
namely structs and its public definitions. For example, if you import
|
||||
a module but only use its functions, it is an export dependency. If
|
||||
you use a struct, it is an export dependency too. Export dependencies
|
||||
are only recompiled if the module API changes. Note, however, that compile
|
||||
time dependencies have higher precedence than exports. Therefore if
|
||||
you import a module and use its macros, it is a compile time dependency.
|
||||
Cycle of length 3:
|
||||
|
||||
Runtime dependencies are added whenever you invoke another module
|
||||
inside a function. Modules with runtime dependencies do not have
|
||||
to be compiled when the callee changes, unless there is a transitive
|
||||
compile or an outdated export time dependency between them. The option
|
||||
`--label compile-connected` can be used to find the first case.
|
||||
lib/c.ex
|
||||
lib/b.ex (compile)
|
||||
lib/a.ex
|
||||
|
||||
This means `c.ex` depends on `b.ex` at compile time. Any compile dependency
|
||||
in a cycle is by definition a compile-connected dependency, which must be
|
||||
generally avoided, as explained earlier in the module documentation.
|
||||
|
||||
## Shared options
|
||||
|
||||
@@ -846,44 +921,15 @@ defmodule Mix.Tasks.Xref do
|
||||
true -> file_references
|
||||
end
|
||||
|
||||
# Filter according to non direct label
|
||||
file_references = filter(file_references, filter)
|
||||
|
||||
# If a label is given, remove empty root nodes
|
||||
file_references =
|
||||
if opts[:label] do
|
||||
for {_, [_ | _]} = pair <- file_references, into: %{}, do: pair
|
||||
else
|
||||
file_references
|
||||
end
|
||||
|
||||
roots =
|
||||
if sources do
|
||||
Enum.map(sources, &{&1, nil})
|
||||
else
|
||||
file_references
|
||||
|> Map.drop(sinks || [])
|
||||
|> Enum.map(&{elem(&1, 0), nil})
|
||||
end
|
||||
|
||||
callback = fn {file, type} ->
|
||||
children = if opts[:only_nodes], do: [], else: Map.get(file_references, file, [])
|
||||
type = type && "(#{type})"
|
||||
{{file, type}, Enum.sort(children)}
|
||||
end
|
||||
|
||||
{found, count} =
|
||||
case opts[:format] do
|
||||
"dot" ->
|
||||
{roots, callback, count} =
|
||||
roots_and_callback(file_references, filter, sources, sinks, opts)
|
||||
|
||||
path = Keyword.get(opts, :output, "xref_graph.dot")
|
||||
|
||||
Mix.Utils.write_dot_graph!(
|
||||
path,
|
||||
"xref graph",
|
||||
Enum.sort(roots),
|
||||
callback,
|
||||
opts
|
||||
)
|
||||
Mix.Utils.write_dot_graph!(path, "xref graph", Enum.sort(roots), callback, opts)
|
||||
|
||||
if path != "-" do
|
||||
png_path = (path |> Path.rootname() |> Path.basename()) <> ".png"
|
||||
@@ -899,19 +945,22 @@ defmodule Mix.Tasks.Xref do
|
||||
|> Mix.shell().info()
|
||||
end
|
||||
|
||||
{:references, count_references(file_references)}
|
||||
{:references, count}
|
||||
|
||||
"stats" ->
|
||||
print_stats(file_references, opts)
|
||||
print_stats(file_references, filter, opts)
|
||||
{:stats, 0}
|
||||
|
||||
"cycles" ->
|
||||
{:cycles, print_cycles(file_references, opts)}
|
||||
{:cycles, print_cycles(file_references, filter, opts)}
|
||||
|
||||
other when other in [nil, "plain", "pretty"] ->
|
||||
{roots, callback, count} =
|
||||
roots_and_callback(file_references, filter, sources, sinks, opts)
|
||||
|
||||
Mix.Utils.print_tree(Enum.sort(roots), callback, opts)
|
||||
|
||||
{:references, count_references(file_references)}
|
||||
{:references, count}
|
||||
|
||||
other ->
|
||||
Mix.raise("Unknown --format #{other} in mix xref graph")
|
||||
@@ -920,28 +969,6 @@ defmodule Mix.Tasks.Xref do
|
||||
check_failure(found, count, opts[:fail_above])
|
||||
end
|
||||
|
||||
defp count_references(file_references) do
|
||||
Enum.reduce(file_references, 0, fn {_, refs}, total -> total + length(refs) end)
|
||||
end
|
||||
|
||||
defp filter_fn(file_references, :compile_connected),
|
||||
do: fn {key, type} ->
|
||||
type == :compile and match?([_ | _], file_references[key] || [])
|
||||
end
|
||||
|
||||
defp filter_fn(_file_references, filter),
|
||||
do: fn {_key, type} -> type == filter end
|
||||
|
||||
defp filter(file_references, :all), do: file_references
|
||||
|
||||
defp filter(file_references, filter) do
|
||||
filter_fn = filter_fn(file_references, filter)
|
||||
|
||||
for {key, children} <- file_references,
|
||||
into: %{},
|
||||
do: {key, Enum.filter(children, filter_fn)}
|
||||
end
|
||||
|
||||
defp source_tree(file_references, keys) do
|
||||
keys
|
||||
|> Enum.reduce({%{}, %{}}, fn key, {acc, seen} ->
|
||||
@@ -980,7 +1007,59 @@ defmodule Mix.Tasks.Xref do
|
||||
end)
|
||||
end
|
||||
|
||||
defp print_stats(references, opts) do
|
||||
defp roots_and_callback(file_references, filter, sources, sinks, opts) do
|
||||
# Filter according to non direct label
|
||||
file_references = transitive_filter(file_references, filter)
|
||||
|
||||
# If a label is given, remove empty root nodes
|
||||
file_references =
|
||||
if opts[:label] do
|
||||
for {_, [_ | _]} = pair <- file_references, into: %{}, do: pair
|
||||
else
|
||||
file_references
|
||||
end
|
||||
|
||||
roots =
|
||||
if sources do
|
||||
Enum.map(sources, &{&1, nil})
|
||||
else
|
||||
file_references
|
||||
|> Map.drop(sinks || [])
|
||||
|> Enum.map(&{elem(&1, 0), nil})
|
||||
end
|
||||
|
||||
callback = fn {file, type} ->
|
||||
children = if opts[:only_nodes], do: [], else: Map.get(file_references, file, [])
|
||||
type = type && "(#{type})"
|
||||
{{file, type}, Enum.sort(children)}
|
||||
end
|
||||
|
||||
{roots, callback, count_references(file_references)}
|
||||
end
|
||||
|
||||
defp count_references(file_references) do
|
||||
Enum.reduce(file_references, 0, fn {_, refs}, total -> total + length(refs) end)
|
||||
end
|
||||
|
||||
defp transitive_filter_fn(file_references, :compile_connected),
|
||||
do: fn {key, type} ->
|
||||
type == :compile and match?([_ | _], file_references[key] || [])
|
||||
end
|
||||
|
||||
defp transitive_filter_fn(_file_references, filter),
|
||||
do: fn {_key, type} -> type == filter end
|
||||
|
||||
defp transitive_filter(file_references, :all), do: file_references
|
||||
|
||||
defp transitive_filter(file_references, filter) do
|
||||
filter_fn = transitive_filter_fn(file_references, filter)
|
||||
|
||||
for {key, children} <- file_references,
|
||||
into: %{},
|
||||
do: {key, Enum.filter(children, filter_fn)}
|
||||
end
|
||||
|
||||
defp print_stats(references, filter, opts) do
|
||||
with_digraph(references, fn graph ->
|
||||
shell = Mix.shell()
|
||||
|
||||
@@ -995,11 +1074,11 @@ defmodule Mix.Tasks.Xref do
|
||||
shell.info("Compile dependencies: #{counters.compile} (edges)")
|
||||
shell.info("Exports dependencies: #{counters.export} (edges)")
|
||||
shell.info("Runtime dependencies: #{counters.nil} (edges)")
|
||||
shell.info("Cycles: #{length(cycles(graph, opts))}")
|
||||
shell.info("Cycles: #{length(cycles(graph, filter, opts))}")
|
||||
|
||||
outgoing =
|
||||
references
|
||||
|> Enum.map(fn {file, _} -> {:digraph.out_degree(graph, file), file} end)
|
||||
|> Enum.map(fn {file, _} -> {out_stats_filter(references, graph, file, filter), file} end)
|
||||
|> Enum.sort(:desc)
|
||||
|> Enum.take(10)
|
||||
|
||||
@@ -1008,7 +1087,7 @@ defmodule Mix.Tasks.Xref do
|
||||
|
||||
incoming =
|
||||
references
|
||||
|> Enum.map(fn {file, _} -> {:digraph.in_degree(graph, file), file} end)
|
||||
|> Enum.map(fn {file, _} -> {in_stats_filter(references, graph, file, filter), file} end)
|
||||
|> Enum.sort(:desc)
|
||||
|> Enum.take(10)
|
||||
|
||||
@@ -1017,6 +1096,32 @@ defmodule Mix.Tasks.Xref do
|
||||
end)
|
||||
end
|
||||
|
||||
defp out_stats_filter(_references, graph, file, :all), do: :digraph.out_degree(graph, file)
|
||||
|
||||
defp out_stats_filter(references, graph, file, filter) do
|
||||
filter_fn = transitive_filter_fn(references, filter)
|
||||
|
||||
graph
|
||||
|> :digraph.out_neighbours(file)
|
||||
|> Enum.count(fn v ->
|
||||
{_edge, _v1, _v2, label} = :digraph.edge(graph, {file, v})
|
||||
filter_fn.({file, label})
|
||||
end)
|
||||
end
|
||||
|
||||
defp in_stats_filter(_references, graph, file, :all), do: :digraph.in_degree(graph, file)
|
||||
|
||||
defp in_stats_filter(references, graph, file, filter) do
|
||||
filter_fn = transitive_filter_fn(references, filter)
|
||||
|
||||
graph
|
||||
|> :digraph.in_neighbours(file)
|
||||
|> Enum.count(fn v ->
|
||||
{_edge, _v1, _v2, label} = :digraph.edge(graph, {v, file})
|
||||
filter_fn.({file, label})
|
||||
end)
|
||||
end
|
||||
|
||||
defp with_digraph(references, callback) do
|
||||
graph = :digraph.new()
|
||||
|
||||
@@ -1026,7 +1131,7 @@ defmodule Mix.Tasks.Xref do
|
||||
end
|
||||
|
||||
for {file, deps} <- references, {dep, label} <- deps do
|
||||
:digraph.add_edge(graph, file, dep, label)
|
||||
:digraph.add_edge(graph, {file, dep}, file, dep, label)
|
||||
end
|
||||
|
||||
callback.(graph)
|
||||
@@ -1035,7 +1140,7 @@ defmodule Mix.Tasks.Xref do
|
||||
end
|
||||
end
|
||||
|
||||
defp cycles(graph, opts) do
|
||||
defp cycles(graph, filter, opts) do
|
||||
# Vertices order in cyclic_strong_components/1 return is arbitrary and changes between
|
||||
# OTP versions, sorting is necessary to make the output stable across versions.
|
||||
cycles =
|
||||
@@ -1044,21 +1149,47 @@ defmodule Mix.Tasks.Xref do
|
||||
|> Enum.reduce([], &inner_cycles(graph, Enum.sort(&1), &2))
|
||||
|> Enum.map(&{length(&1), &1})
|
||||
|
||||
if min = opts[:min_cycle_size], do: Enum.filter(cycles, &(elem(&1, 0) > min)), else: cycles
|
||||
cycles =
|
||||
if min = opts[:min_cycle_size] do
|
||||
Enum.filter(cycles, &(elem(&1, 0) > min))
|
||||
else
|
||||
cycles
|
||||
end
|
||||
|
||||
# :compile_connected is the same
|
||||
if cycle_fn = cycle_filter_fn(filter) do
|
||||
Enum.filter(cycles, fn {_length, cycle} -> Enum.any?(cycle, cycle_fn) end)
|
||||
else
|
||||
cycles
|
||||
end
|
||||
end
|
||||
|
||||
# In cycles, a compile connected is compile
|
||||
defp cycle_filter_fn(:all), do: nil
|
||||
defp cycle_filter_fn(:compile_connected), do: cycle_filter_fn(:compile)
|
||||
defp cycle_filter_fn(filter), do: fn {_node, type} -> type == filter end
|
||||
|
||||
defp inner_cycles(_graph, [], acc), do: acc
|
||||
|
||||
defp inner_cycles(graph, [v | vertices], acc) do
|
||||
cycle = :digraph.get_cycle(graph, v)
|
||||
inner_cycles(graph, vertices -- cycle, [cycle | acc])
|
||||
inner_cycles(graph, vertices -- cycle, [label_cycle(cycle, graph) | acc])
|
||||
end
|
||||
|
||||
defp print_cycles(references, opts) do
|
||||
defp label_cycle([from, to | cycle], graph) do
|
||||
{_edge, _v1, _v2, label} = :digraph.edge(graph, {from, to})
|
||||
[{to, label} | label_cycle([to | cycle], graph)]
|
||||
end
|
||||
|
||||
defp label_cycle([_from], _graph) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp print_cycles(references, filter, opts) do
|
||||
with_digraph(references, fn graph ->
|
||||
shell = Mix.shell()
|
||||
|
||||
case graph |> cycles(opts) |> Enum.sort(:desc) do
|
||||
case graph |> cycles(filter, opts) |> Enum.sort(:desc) do
|
||||
[] ->
|
||||
shell.info("No cycles found")
|
||||
0
|
||||
@@ -1069,8 +1200,9 @@ defmodule Mix.Tasks.Xref do
|
||||
for {length, cycle} <- cycles do
|
||||
shell.info("Cycle of length #{length}:\n")
|
||||
|
||||
for node <- cycle do
|
||||
shell.info(" " <> node)
|
||||
for {node, type} <- cycle do
|
||||
type = if type, do: " (#{type})", else: ""
|
||||
shell.info(" " <> node <> type)
|
||||
end
|
||||
|
||||
shell.info("")
|
||||
|
||||
@@ -1597,11 +1597,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
def foo(), do: B.foo()
|
||||
def bar(), do: B.bar()
|
||||
def __after_verify__(__MODULE__) do
|
||||
if Code.ensure_loaded?(B) and not function_exported?(B, :foo, 0) do
|
||||
:ok
|
||||
else
|
||||
IO.warn("AFTER_VERIFY", __ENV__)
|
||||
end
|
||||
IO.warn("AFTER_VERIFY", __ENV__)
|
||||
end
|
||||
end
|
||||
""")
|
||||
@@ -1630,10 +1626,9 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
end)
|
||||
|
||||
# Check B due to direct dependency on A
|
||||
# Check C due to transient dependency on A
|
||||
assert output =~ "A.foo/0 is undefined or private"
|
||||
assert output =~ "B.bar/0 is undefined or private"
|
||||
assert output =~ "AFTER_VERIFY"
|
||||
refute output =~ "B.bar/0 is undefined or private"
|
||||
refute output =~ "AFTER_VERIFY"
|
||||
|
||||
# Ensure only A was recompiled
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
@@ -1650,7 +1645,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
assert output =~ "B.bar/0 is undefined or private"
|
||||
assert output =~ "AFTER_VERIFY"
|
||||
|
||||
# Now we change B and it must no longer emit an AFTER_VERIFY warning
|
||||
# Now we change B and it must emit an AFTER_VERIFY warning
|
||||
File.write!("lib/b.ex", """
|
||||
defmodule B do
|
||||
end
|
||||
@@ -1663,7 +1658,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
|
||||
assert output =~ "B.foo/0 is undefined or private"
|
||||
assert output =~ "B.bar/0 is undefined or private"
|
||||
refute output =~ "AFTER_VERIFY"
|
||||
assert output =~ "AFTER_VERIFY"
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
@@ -530,43 +530,6 @@ defmodule Mix.Tasks.DepsGitTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "with ref" do
|
||||
[last, _ | _] = get_git_repo_revs("git_repo")
|
||||
|
||||
Process.put(:git_repo_opts, depth: 1, ref: last)
|
||||
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(GitApp)
|
||||
|
||||
Mix.Tasks.Deps.Get.run([])
|
||||
message = "* Getting git_repo (#{fixture_path("git_repo")} - #{last})"
|
||||
assert_received {:mix_shell, :info, [^message]}
|
||||
assert_shallow("deps/git_repo", 1)
|
||||
end)
|
||||
end
|
||||
|
||||
test "changing refspec updates retaining depth" do
|
||||
[last, first | _] = get_git_repo_revs("git_repo")
|
||||
|
||||
Process.put(:git_repo_opts, ref: first, depth: 1)
|
||||
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(GitApp)
|
||||
|
||||
Mix.Tasks.Deps.Get.run([])
|
||||
message = "* Getting git_repo (#{fixture_path("git_repo")} - #{first})"
|
||||
assert_received {:mix_shell, :info, [^message]}
|
||||
assert_shallow("deps/git_repo", 1)
|
||||
assert File.read!("mix.lock") =~ first
|
||||
|
||||
# Change refspec
|
||||
update_dep(ref: last, depth: 1)
|
||||
Mix.Tasks.Deps.Get.run([])
|
||||
assert_shallow("deps/git_repo", 1)
|
||||
assert File.read!("mix.lock") =~ last
|
||||
end)
|
||||
end
|
||||
|
||||
test "removing depth retains shallow repository" do
|
||||
# For compatibility and simplicity, we follow Git's behavior and do not
|
||||
# attempt to unshallow an existing repository. This should not be a
|
||||
|
||||
@@ -439,14 +439,85 @@ defmodule Mix.Tasks.XrefTest do
|
||||
""")
|
||||
end
|
||||
|
||||
test "stats with compile label" do
|
||||
assert_graph(["--format", "stats", "--label", "compile"], """
|
||||
Tracked files: 5 (nodes)
|
||||
Compile dependencies: 3 (edges)
|
||||
Exports dependencies: 0 (edges)
|
||||
Runtime dependencies: 3 (edges)
|
||||
Cycles: 1
|
||||
|
||||
Top 5 files with most outgoing dependencies:
|
||||
* lib/c.ex (1)
|
||||
* lib/b.ex (1)
|
||||
* lib/a.ex (1)
|
||||
* lib/e.ex (0)
|
||||
* lib/d.ex (0)
|
||||
|
||||
Top 5 files with most incoming dependencies:
|
||||
* lib/e.ex (1)
|
||||
* lib/d.ex (1)
|
||||
* lib/b.ex (1)
|
||||
* lib/c.ex (0)
|
||||
* lib/a.ex (0)
|
||||
""")
|
||||
end
|
||||
|
||||
test "stats with compile-connected label" do
|
||||
assert_graph(["--format", "stats", "--label", "compile-connected"], """
|
||||
Tracked files: 5 (nodes)
|
||||
Compile dependencies: 3 (edges)
|
||||
Exports dependencies: 0 (edges)
|
||||
Runtime dependencies: 3 (edges)
|
||||
Cycles: 1
|
||||
|
||||
Top 5 files with most outgoing dependencies:
|
||||
* lib/c.ex (1)
|
||||
* lib/b.ex (1)
|
||||
* lib/a.ex (1)
|
||||
* lib/e.ex (0)
|
||||
* lib/d.ex (0)
|
||||
|
||||
Top 5 files with most incoming dependencies:
|
||||
* lib/d.ex (1)
|
||||
* lib/b.ex (1)
|
||||
* lib/e.ex (0)
|
||||
* lib/c.ex (0)
|
||||
* lib/a.ex (0)
|
||||
""")
|
||||
end
|
||||
|
||||
test "cycles" do
|
||||
assert_graph(["--format", "cycles"], """
|
||||
1 cycles found. Showing them in decreasing size:
|
||||
|
||||
Cycle of length 3:
|
||||
Cycle of length 2:
|
||||
|
||||
lib/b.ex (compile)
|
||||
lib/a.ex
|
||||
lib/b.ex
|
||||
|
||||
""")
|
||||
end
|
||||
|
||||
test "cycles with compile label require at least one of such type" do
|
||||
assert_graph(["--format", "cycles", "--label", "compile"], """
|
||||
1 cycles found. Showing them in decreasing size:
|
||||
|
||||
Cycle of length 2:
|
||||
|
||||
lib/b.ex (compile)
|
||||
lib/a.ex
|
||||
|
||||
""")
|
||||
end
|
||||
|
||||
test "cycles with compile-connected label is the same as compile" do
|
||||
assert_graph(["--format", "cycles", "--label", "compile-connected"], """
|
||||
1 cycles found. Showing them in decreasing size:
|
||||
|
||||
Cycle of length 2:
|
||||
|
||||
lib/b.ex (compile)
|
||||
lib/a.ex
|
||||
|
||||
""")
|
||||
|
||||
Reference in New Issue
Block a user