Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
372836d623 | ||
|
|
444cd7e6bd | ||
|
|
cacb175ab3 | ||
|
|
bfce3ed7a9 | ||
|
|
06811cbdc8 | ||
|
|
7a6b35422c | ||
|
|
78f63d0831 | ||
|
|
86ec46a5c4 | ||
|
|
c99631876c | ||
|
|
46815ef554 | ||
|
|
256a55a53e | ||
|
|
a78c92043f | ||
|
|
b3bfa519a7 | ||
|
|
b734da3f62 | ||
|
|
10f86595b5 | ||
|
|
1da385776f | ||
|
|
a45e08eeda | ||
|
|
bb61b953d7 | ||
|
|
1e126e88b7 |
@@ -70,6 +70,24 @@ 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
|
||||
|
||||
+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.2
|
||||
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.2
|
||||
set ELIXIR_VERSION=1.17.3
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>}
|
||||
```
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -426,7 +426,7 @@ translate_with_else(Meta, [{'else', Else}], S) ->
|
||||
|
||||
Clauses = elixir_erl_clauses:get_clauses('else', [{'else', Else ++ [RaiseClause]}], match),
|
||||
{TranslatedClauses, SC} = elixir_erl_clauses:clauses(Clauses, SV#elixir_erl{extra=pin_guard}),
|
||||
with_else_closure(Meta, TranslatedClauses, SC#elixir_erl{extra=SV#elixir_erl.extra}).
|
||||
with_else_closure(Generated, TranslatedClauses, SC#elixir_erl{extra=SV#elixir_erl.extra}).
|
||||
|
||||
with_else_closure(Meta, TranslatedClauses, S) ->
|
||||
Ann = ?ann(Meta),
|
||||
|
||||
@@ -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,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,
|
||||
@@ -164,6 +165,11 @@ defmodule Kernel.DialyzerTest do
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -948,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+>\]\)
|
||||
"""
|
||||
@@ -963,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
|
||||
|
||||
|
||||
@@ -1077,24 +1077,24 @@ defmodule Mix.Compilers.Elixir do
|
||||
# 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.
|
||||
runtime_modules = Map.merge(modules, stale_modules)
|
||||
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, Map.keys(runtime_modules)}, fn
|
||||
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?(runtime_modules, export_refs) or
|
||||
has_any_key?(runtime_modules, runtime_refs) do
|
||||
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?(runtime_modules, &1))
|
||||
Enum.reject(source(source_entry, :modules), &Map.has_key?(changed_modules, &1))
|
||||
|
||||
{acc_sources, new_modules ++ acc_modules}
|
||||
else
|
||||
|
||||
+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("")
|
||||
|
||||
@@ -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