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 |
@@ -70,6 +70,38 @@ 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
|
||||
@@ -161,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.1
|
||||
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.1
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+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
|
||||
|
||||
@@ -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("")
|
||||
|
||||
@@ -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