Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
794441a255 | ||
|
|
727106beb4 | ||
|
|
5e45871b5e | ||
|
|
0db308cbff | ||
|
|
35a7e3aa1f | ||
|
|
7340597020 | ||
|
|
3b6acc33d2 | ||
|
|
2ed048acc6 | ||
|
|
bf0c203d52 | ||
|
|
b074e6b159 | ||
|
|
bfe7315fbb | ||
|
|
28f4fdc21b | ||
|
|
ee1dc0729f | ||
|
|
32e724c87c | ||
|
|
7758084211 | ||
|
|
57d3098c2a |
@@ -234,6 +234,34 @@ These additions offer greater transparency into the components and licenses of e
|
||||
|
||||
This work was performed by [Jonatan Männchen](https://maennchen.dev) and sponsored by the [Erlang Ecosystem Foundation](https://erlef.org).
|
||||
|
||||
## v1.19.4 (2025-11-27)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix xref] Add `--min-cycle-label` to help projects adapt to the more precise `mix xref graph` reports in Elixir v1.19. In previous versions, Elixir would break a large compilation cycle into several smaller ones, and therefore developers would check for `--min-cycle-size` on CI. However, the issue is not the size of the cycle (it has no implication in the amount of compiled files), but how many compile-time dependencies (aka compile labels) in a cycle. The new option allows developers to filter on the label parameter
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [File] Ensure `File.cp_r/3` reports non-existing destination properly (instead of source)
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Fix formatter crash when diffing takes too long
|
||||
* [ExUnit] Ensure parallel matches in `assert` propagate type information
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Fix regression where formatter would crash when given chardata (the crash would happen when logging non-ASCII characters)
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix help] Ensure `app:APP` works when the project or its dependencies were not yet compiled
|
||||
* [mix escript.build] Ensure the `hex` application can be included in escripts
|
||||
|
||||
## v1.19.3 (2025-11-13)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.19.3
|
||||
ELIXIR_VERSION=1.19.4
|
||||
|
||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||
cat <<USAGE >&2
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
:: SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
:: SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
set ELIXIR_VERSION=1.19.3
|
||||
set ELIXIR_VERSION=1.19.4
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
|
||||
@@ -1090,7 +1090,7 @@ defmodule Code do
|
||||
@doc since: "1.6.0"
|
||||
@spec format_string!(binary, [format_opt]) :: iodata
|
||||
def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do
|
||||
line_length = Keyword.get(opts, :line_length, 98)
|
||||
{line_length, opts} = Keyword.pop(opts, :line_length, 98)
|
||||
|
||||
to_quoted_opts =
|
||||
[
|
||||
|
||||
@@ -1224,7 +1224,13 @@ defmodule File do
|
||||
defp do_cp_r(src, dest, on_conflict, dereference?, acc) when is_list(acc) do
|
||||
case :elixir_utils.read_link_type(src) do
|
||||
{:ok, :regular} ->
|
||||
do_cp_file(src, dest, on_conflict, acc)
|
||||
case do_cp_file(src, dest, on_conflict, acc) do
|
||||
# we don't have a way to make a distinction between a non-existing src
|
||||
# or dest being a non-existing dir in the case of :enoent,
|
||||
# but we already know that src exists here.
|
||||
{:error, :enoent, _} -> {:error, :enoent, dest}
|
||||
other -> other
|
||||
end
|
||||
|
||||
{:ok, :symlink} ->
|
||||
case :file.read_link(src) do
|
||||
|
||||
@@ -959,8 +959,9 @@ defmodule Macro do
|
||||
backwards compatibility purposes.
|
||||
|
||||
In future releases, Elixir may introduce truly required struct
|
||||
fields, and therefore only one of required or default will be
|
||||
present. Your code should prepare for such scenario accordingly.
|
||||
fields, the required field will be removed and default will be
|
||||
present only if the field is optional. Your code should prepare
|
||||
for such scenario accordingly.
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec struct_info!(module(), Macro.Env.t()) ::
|
||||
|
||||
@@ -402,30 +402,21 @@ defmodule Module do
|
||||
|
||||
Accepts the function name (as an atom) of a function in the current module.
|
||||
The function must have an arity of 0 (no arguments). If the function does
|
||||
not return `:ok`, the loading of the module will be aborted.
|
||||
For example:
|
||||
not return `:ok`, the loading of the module will be aborted. Its primary
|
||||
use case is to load [NIFs](https://www.erlang.org/doc/man/erl_nif):
|
||||
|
||||
defmodule MyModule do
|
||||
@on_load :load_check
|
||||
@on_load :load_external_code
|
||||
|
||||
def load_check do
|
||||
if some_condition() do
|
||||
:ok
|
||||
else
|
||||
:abort
|
||||
end
|
||||
end
|
||||
|
||||
def some_condition do
|
||||
false
|
||||
def load_external_code do
|
||||
:erlang.load_nif(~c"path/to/extension.so_or_dll")
|
||||
end
|
||||
end
|
||||
|
||||
The function given to `on_load` should avoid calling functions from
|
||||
other modules. If you must call functions in other modules and those
|
||||
modules are defined within the same project, the called modules must
|
||||
have the `@compile {:autoload, true}` annotation, so they are loaded
|
||||
upfront (and not from within the `@on_load` callback).
|
||||
other modules. This is because, when running a `mix release`,
|
||||
`on_load` runs extremely early, before any application starts running,
|
||||
and therefore even systems like the `Logger` and `IO` are not yet available.
|
||||
|
||||
### `@vsn`
|
||||
|
||||
|
||||
@@ -27,8 +27,9 @@ defmodule Record do
|
||||
|
||||
## Types
|
||||
|
||||
Types can be defined for tuples with the `record/2` macro (only available in
|
||||
typespecs). This macro will expand to a tuple as seen in the example below:
|
||||
Types can be defined for tuples with the `record/2` construct (which is only
|
||||
available in typespecs), with the record name as an atom and a keyword list
|
||||
of fields and their types as argument:
|
||||
|
||||
defmodule MyModule do
|
||||
require Record
|
||||
|
||||
@@ -101,7 +101,7 @@ defmodule Regex do
|
||||
* `:ungreedy` (U) - inverts the "greediness" of the regexp
|
||||
(the previous `r` option is deprecated in favor of `U`)
|
||||
|
||||
* `:export` (E) (since Elixir 1.20) - uses an exported pattern
|
||||
* `:export` (E) (since Elixir 1.19.3) - uses an exported pattern
|
||||
which can be shared across nodes or through config, at the cost of a runtime
|
||||
overhead every time to re-import it every time it is executed.
|
||||
This modifier only has an effect starting on Erlang/OTP 28, and it is ignored
|
||||
@@ -597,6 +597,7 @@ defmodule Regex do
|
||||
end
|
||||
|
||||
@compile {:inline, maybe_import_pattern: 1}
|
||||
@compile {:no_warn_undefined, {:re, :import, 1}}
|
||||
defp maybe_import_pattern({:re_exported_pattern, _, _, _, _} = exported),
|
||||
do: :re.import(exported)
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ Now we need to change `loop_acceptor/1` to use `Task.Supervisor` to serve each r
|
||||
```elixir
|
||||
defp loop_acceptor(socket) do
|
||||
{:ok, client} = :gen_tcp.accept(socket)
|
||||
{:ok, pid} = Task.Supervisor.start_child(KV.BucketSupervisor, fn -> serve(client) end)
|
||||
{:ok, pid} = Task.Supervisor.start_child(KV.ServerSupervisor, fn -> serve(client) end)
|
||||
:ok = :gen_tcp.controlling_process(client, pid)
|
||||
loop_acceptor(socket)
|
||||
end
|
||||
|
||||
@@ -787,6 +787,18 @@ defmodule FileTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "cp_r! with file src and dest unknown" do
|
||||
src = fixture_path("cp_r/a/1.txt")
|
||||
dest = tmp_path("tmp/unknown/")
|
||||
|
||||
message =
|
||||
"could not copy recursively from #{inspect(src)} to #{inspect(dest)}. #{dest}: no such file or directory"
|
||||
|
||||
assert_raise File.CopyError, message, fn ->
|
||||
File.cp_r!(src, dest)
|
||||
end
|
||||
end
|
||||
|
||||
test "cp preserves mode" do
|
||||
File.mkdir_p!(tmp_path("tmp"))
|
||||
src = fixture_path("cp_mode")
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
defmodule Dialyzer.Regressions do
|
||||
def io_inspect_opts do
|
||||
IO.inspect(123, label: "foo", limit: :infinity)
|
||||
end
|
||||
|
||||
def format_opts do
|
||||
Code.format_string!("",
|
||||
line_length: 120,
|
||||
force_do_end_blocks: true,
|
||||
locals_without_parens: true,
|
||||
migrate: true
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -185,6 +185,11 @@ defmodule Kernel.DialyzerTest do
|
||||
assert_dialyze_no_warnings!(context)
|
||||
end
|
||||
|
||||
test "no warning in various non-regression cases", context do
|
||||
copy_beam!(context, Dialyzer.Regressions)
|
||||
assert_dialyze_no_warnings!(context)
|
||||
end
|
||||
|
||||
defp copy_beam!(context, module) do
|
||||
name = "#{module}.beam"
|
||||
File.cp!(Path.join(context.base_dir, name), Path.join(context.outdir, name))
|
||||
|
||||
@@ -160,6 +160,7 @@ defmodule ExUnit.Assertions do
|
||||
end
|
||||
end
|
||||
|
||||
{left, right} = move_match(left, right)
|
||||
__match__(left, right, code, check, __CALLER__)
|
||||
end
|
||||
|
||||
@@ -380,6 +381,12 @@ defmodule ExUnit.Assertions do
|
||||
{ExUnit.AssertionError.no_value(), expr}
|
||||
end
|
||||
|
||||
defp move_match(left, {:=, meta, [middle, right]}),
|
||||
do: move_match({:=, meta, [left, middle]}, right)
|
||||
|
||||
defp move_match(left, right),
|
||||
do: {left, right}
|
||||
|
||||
@doc false
|
||||
def __match__({:when, _, _} = left, right, _, _, _) do
|
||||
suggestion =
|
||||
@@ -409,7 +416,7 @@ defmodule ExUnit.Assertions do
|
||||
case right do
|
||||
unquote(left) ->
|
||||
unquote(check)
|
||||
unquote(mark_as_generated(vars))
|
||||
{unquote_splicing(mark_as_generated(vars))}
|
||||
|
||||
_ ->
|
||||
left = unquote(Macro.escape(left))
|
||||
@@ -427,7 +434,7 @@ defmodule ExUnit.Assertions do
|
||||
quote do
|
||||
right = unquote(right)
|
||||
expr = unquote(code)
|
||||
unquote(vars) = unquote(match_expr)
|
||||
{unquote_splicing(vars)} = unquote(match_expr)
|
||||
right
|
||||
end
|
||||
end
|
||||
|
||||
@@ -964,7 +964,14 @@ defmodule ExUnit.DocTest do
|
||||
|
||||
@doc false
|
||||
defmacro __assert__({:=, _, [left, right]} = assertion) do
|
||||
{left, right} = move_match(left, right)
|
||||
code = Macro.escape(assertion, prune_metadata: true)
|
||||
ExUnit.Assertions.__match__(left, right, code, :ok, __CALLER__)
|
||||
end
|
||||
|
||||
defp move_match(left, {:=, meta, [middle, right]}),
|
||||
do: move_match({:=, meta, [left, middle]}, right)
|
||||
|
||||
defp move_match(left, right),
|
||||
do: {left, right}
|
||||
end
|
||||
|
||||
@@ -513,10 +513,15 @@ defmodule ExUnit.Formatter do
|
||||
|
||||
defp format_diff(left, right, context, formatter) do
|
||||
if has_value?(left) and has_value?(right) do
|
||||
{result, env} = find_diff(left, right, context)
|
||||
result = if formatter.(:diff_enabled?, false), do: result
|
||||
hints = Enum.map(env.hints, &{:hint, format_hint(&1)})
|
||||
{result, hints}
|
||||
case find_diff(left, right, context) do
|
||||
{result, env} ->
|
||||
result = if formatter.(:diff_enabled?, false), do: result
|
||||
hints = Enum.map(env.hints, &{:hint, format_hint(&1)})
|
||||
{result, hints}
|
||||
|
||||
nil ->
|
||||
{nil, []}
|
||||
end
|
||||
else
|
||||
{nil, []}
|
||||
end
|
||||
|
||||
@@ -246,6 +246,16 @@ defmodule ExUnit.AssertionsTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "assert parallel match" do
|
||||
assert %URI{} = uri = apply(URI, :parse, ["/foo"])
|
||||
# This should not warn
|
||||
assert %URI{uri | path: "/bar"}.path == "/bar"
|
||||
|
||||
assert uri = %URI{} = apply(URI, :parse, ["/foo"])
|
||||
# This should not warn
|
||||
assert %URI{uri | path: "/bar"}.path == "/bar"
|
||||
end
|
||||
|
||||
test "assert match with pinned variable" do
|
||||
a = 1
|
||||
{2, 1} = assert {2, ^a} = Value.tuple()
|
||||
|
||||
@@ -244,26 +244,38 @@ defmodule Logger.Formatter do
|
||||
case if(is_function(enabled, 0), do: enabled.(), else: enabled) do
|
||||
true ->
|
||||
color = md[:ansi_color] || Map.fetch!(colors, level)
|
||||
fragment = IO.ANSI.format_fragment(color, true)
|
||||
data = IO.iodata_to_binary(data)
|
||||
size = byte_size(data)
|
||||
|
||||
cond do
|
||||
:binary.at(data, size - 2) == ?\r and :binary.at(data, size - 1) == ?\n ->
|
||||
[fragment, binary_part(data, 0, size - 2), IO.ANSI.reset() | "\r\n"]
|
||||
|
||||
:binary.at(data, size - 1) == ?\n ->
|
||||
[fragment, binary_part(data, 0, size - 1), IO.ANSI.reset(), ?\n]
|
||||
|
||||
true ->
|
||||
[fragment, data | IO.ANSI.reset()]
|
||||
end
|
||||
[IO.ANSI.format_fragment(color, true), add_reset(data)]
|
||||
|
||||
false ->
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
defp add_reset(binary) when is_binary(binary) do
|
||||
size = byte_size(binary)
|
||||
|
||||
cond do
|
||||
binary == "" ->
|
||||
IO.ANSI.reset()
|
||||
|
||||
:binary.at(binary, size - 1) == ?\n ->
|
||||
if size > 1 and :binary.at(binary, size - 2) == ?\r do
|
||||
[binary_part(binary, 0, size - 2), IO.ANSI.reset() | "\r\n"]
|
||||
else
|
||||
[binary_part(binary, 0, size - 1), IO.ANSI.reset(), ?\n]
|
||||
end
|
||||
|
||||
true ->
|
||||
[binary | IO.ANSI.reset()]
|
||||
end
|
||||
end
|
||||
|
||||
defp add_reset([?\r, ?\n]), do: [IO.ANSI.reset(), ?\r, ?\n]
|
||||
defp add_reset([?\n]), do: [IO.ANSI.reset(), ?\n]
|
||||
defp add_reset([last]), do: add_reset(last)
|
||||
defp add_reset([h | t]), do: [h | add_reset(t)]
|
||||
defp add_reset(rest), do: [rest | IO.ANSI.reset()]
|
||||
|
||||
@doc """
|
||||
Formats the message of a log event.
|
||||
"""
|
||||
|
||||
@@ -105,6 +105,14 @@ defmodule Logger.FormatterTest do
|
||||
colors: [enabled: true]
|
||||
)
|
||||
|
||||
assert %{level: :warning, msg: {:string, ""}, meta: %{}}
|
||||
|> format(formatter)
|
||||
|> IO.chardata_to_string() == "\e[33m\e[0m"
|
||||
|
||||
assert %{level: :warning, msg: {:string, "s"}, meta: %{}}
|
||||
|> format(formatter)
|
||||
|> IO.chardata_to_string() == "\e[33ms\e[0m"
|
||||
|
||||
assert %{level: :warning, msg: {:string, "message"}, meta: %{}}
|
||||
|> format(formatter)
|
||||
|> IO.chardata_to_string() == "\e[33mmessage\e[0m"
|
||||
@@ -116,6 +124,18 @@ defmodule Logger.FormatterTest do
|
||||
assert %{level: :warning, msg: {:string, "message\r\n"}, meta: %{}}
|
||||
|> format(formatter)
|
||||
|> IO.chardata_to_string() == "\e[33mmessage\e[0m\r\n"
|
||||
|
||||
assert %{level: :warning, msg: {:string, [?é, ["message", [?\n]]]}, meta: %{}}
|
||||
|> format(formatter)
|
||||
|> IO.chardata_to_string() == "\e[33mémessage\e[0m\n"
|
||||
|
||||
assert %{level: :warning, msg: {:string, [?é, "message", ?\r, ?\n]}, meta: %{}}
|
||||
|> format(formatter)
|
||||
|> IO.chardata_to_string() == "\e[33mémessage\e[0m\r\n"
|
||||
|
||||
assert %{level: :warning, msg: {:string, [?é, "message" | "\r\n"]}, meta: %{}}
|
||||
|> format(formatter)
|
||||
|> IO.chardata_to_string() == "\e[33mémessage\e[0m\r\n"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ defmodule Mix.Tasks.Cmd do
|
||||
Your shell will expand "lib/*" and then pass multiple arguments to
|
||||
`mix cmd`, which in turn passes them to `echo`. Note that, `mix cmd`
|
||||
by itself, does not perform any shell expansion. This means that,
|
||||
if you invoke `mix cmd` programatically, as in:
|
||||
if you invoke `mix cmd` programmatically, as in:
|
||||
|
||||
Mix.Task.run("cmd", ["echo", "lib/*"])
|
||||
|
||||
|
||||
@@ -271,7 +271,8 @@ defmodule Mix.Tasks.Escript.Build do
|
||||
[]
|
||||
end
|
||||
|
||||
defp extra_apps_in_app_tree(app) when app in [:eex, :ex_unit, :iex, :logger, :mix] do
|
||||
# We include hex in here as it is always an archive and it cannot be depended on
|
||||
defp extra_apps_in_app_tree(app) when app in [:eex, :ex_unit, :iex, :logger, :mix, :hex] do
|
||||
[app]
|
||||
end
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ defmodule Mix.Tasks.Help do
|
||||
|
||||
def run(["app:" <> app]) do
|
||||
loadpaths!()
|
||||
|
||||
app = String.to_atom(app)
|
||||
|
||||
# If the application is not available, attempt to load it from Erlang/Elixir
|
||||
@@ -119,7 +120,11 @@ defmodule Mix.Tasks.Help do
|
||||
try do
|
||||
Mix.ensure_application!(app)
|
||||
rescue
|
||||
_ -> :ok
|
||||
_ ->
|
||||
# Otherwise, it may be a dep or the current project not yet compiled
|
||||
if Mix.Project.get() do
|
||||
Mix.Task.run("compile")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -44,12 +44,22 @@ defmodule Mix.Tasks.Xref do
|
||||
└── lib/a.ex
|
||||
|
||||
Because you have a compile-time dependency, any of the files `lib/a.ex`,
|
||||
`lib/b.ex`, and `lib/c.ex` depend on will cause the whole cycle to
|
||||
recompile. Therefore, your first priority to reduce compile times is
|
||||
to remove such cycles. You can spot them by running:
|
||||
`lib/b.ex`, and `lib/c.ex` depend on will cause `lib/a.ex` to recompile.
|
||||
In other words, whenever you have a cycle, **a change to any file in the
|
||||
cycle will cause all compile-time deps to recompile**. Therefore, your
|
||||
first priority to reduce constant recompilations is to remove them.
|
||||
You can spot them by running:
|
||||
|
||||
$ mix xref graph --format cycles --label compile-connected
|
||||
|
||||
> #### Use the --label option
|
||||
>
|
||||
> The job of `mix xref` is to explore relationships between files
|
||||
> and it is expected that most of your files are either directly
|
||||
> or indirectly connected. For this reason, it is strongly advised
|
||||
> to pass the `--label` option to filter the amount of data,
|
||||
> specifically with `compile-connected` (or `compile`) as values.
|
||||
|
||||
Whenever you find a compile-time dependency, such as `lib/a.ex` pointing
|
||||
to `lib/b.ex`, there are two ways to remove them:
|
||||
|
||||
@@ -208,13 +218,11 @@ 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 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.
|
||||
The labels are "compile-connected", "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.
|
||||
|
||||
* `--group` - provide comma-separated paths to consider as a group. Dependencies
|
||||
from and into multiple files of the group are considered a single dependency.
|
||||
@@ -239,6 +247,9 @@ defmodule Mix.Tasks.Xref do
|
||||
* `--min-cycle-size` - controls the minimum cycle size on formats
|
||||
like `stats` and `cycles`
|
||||
|
||||
* `--min-cycle-label` - controls the minimum number of dependencies
|
||||
with the given `--label` on a cycle
|
||||
|
||||
* `--format` - can be set to one of:
|
||||
|
||||
* `pretty` - prints the graph to the terminal using Unicode characters.
|
||||
@@ -435,6 +446,7 @@ defmodule Mix.Tasks.Xref do
|
||||
sink: :keep,
|
||||
source: :keep,
|
||||
min_cycle_size: :integer,
|
||||
min_cycle_label: :integer,
|
||||
output: :string
|
||||
]
|
||||
|
||||
@@ -1218,9 +1230,22 @@ defmodule Mix.Tasks.Xref do
|
||||
cycles
|
||||
end
|
||||
|
||||
# :compile_connected is the same
|
||||
min_cycle_label =
|
||||
if integer = opts[:min_cycle_label] do
|
||||
if filter == :all do
|
||||
Mix.raise("--min-cycle-label requires the --label option to be given")
|
||||
end
|
||||
|
||||
integer
|
||||
else
|
||||
1
|
||||
end
|
||||
|
||||
# :compile_connected is the same as :compile
|
||||
if cycle_fn = cycle_filter_fn(filter) do
|
||||
Enum.filter(cycles, fn {_length, cycle} -> Enum.any?(cycle, cycle_fn) end)
|
||||
Enum.filter(cycles, fn {_length, cycle} ->
|
||||
Enum.count_until(cycle, cycle_fn, min_cycle_label) == min_cycle_label
|
||||
end)
|
||||
else
|
||||
cycles
|
||||
end
|
||||
@@ -1269,11 +1294,35 @@ defmodule Mix.Tasks.Xref do
|
||||
shell.info("#{length(cycles)} cycles found. Showing them in decreasing size:\n")
|
||||
|
||||
for {length, cycle} <- cycles do
|
||||
shell.info("Cycle of length #{length}:\n")
|
||||
meta =
|
||||
cycle
|
||||
|> Enum.reduce({0, 0}, fn
|
||||
{_, :compile}, {compile, export} -> {compile + 1, export}
|
||||
{_, :export}, {compile, export} -> {compile, export + 1}
|
||||
{_, _}, {compile, export} -> {compile, export}
|
||||
end)
|
||||
|> case do
|
||||
{0, 0} ->
|
||||
""
|
||||
|
||||
{compile, export} ->
|
||||
info =
|
||||
if(compile > 0, do: ["#{compile} compile"], else: []) ++
|
||||
if(export > 0, do: ["#{export} export"], else: [])
|
||||
|
||||
" (" <> Enum.join(info, ", ") <> ")"
|
||||
end
|
||||
|
||||
shell.info("Cycle of length #{length}#{meta}:\n")
|
||||
|
||||
for {node, type} <- cycle do
|
||||
type = if type, do: " (#{type})", else: ""
|
||||
shell.info(" " <> node <> type)
|
||||
shell.info(
|
||||
case type do
|
||||
:compile -> [:red, " #{node} (compile)"]
|
||||
:export -> " #{node} (export)"
|
||||
_ -> " #{node}"
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
shell.info("")
|
||||
|
||||
@@ -222,6 +222,41 @@ defmodule Mix.Tasks.HelpTest do
|
||||
end)
|
||||
end
|
||||
|
||||
defmodule ExampleProject do
|
||||
def project do
|
||||
[
|
||||
app: :sample,
|
||||
version: "0.1.0"
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
test "help app:APP for current project", context do
|
||||
in_tmp(context.test, fn ->
|
||||
Mix.Project.push(ExampleProject)
|
||||
File.mkdir_p!("lib")
|
||||
|
||||
File.write!("lib/example.ex", ~s'''
|
||||
defmodule Example do
|
||||
@moduledoc """
|
||||
This is an example module.
|
||||
"""
|
||||
end
|
||||
''')
|
||||
|
||||
output =
|
||||
capture_io(fn ->
|
||||
Mix.Tasks.Help.run(["app:sample"])
|
||||
end)
|
||||
|
||||
assert output =~ """
|
||||
# Example
|
||||
|
||||
This is an example module.
|
||||
"""
|
||||
end)
|
||||
end
|
||||
|
||||
test "help Elixir MODULE", context do
|
||||
in_tmp(context.test, fn ->
|
||||
output =
|
||||
|
||||
@@ -500,7 +500,7 @@ defmodule Mix.Tasks.XrefTest do
|
||||
assert_graph(["--format", "cycles"], """
|
||||
1 cycles found. Showing them in decreasing size:
|
||||
|
||||
Cycle of length 2:
|
||||
Cycle of length 2 (1 compile):
|
||||
|
||||
lib/a.ex (compile)
|
||||
lib/b.ex
|
||||
@@ -512,7 +512,7 @@ defmodule Mix.Tasks.XrefTest do
|
||||
assert_graph(["--format", "cycles", "--label", "compile"], """
|
||||
1 cycles found. Showing them in decreasing size:
|
||||
|
||||
Cycle of length 2:
|
||||
Cycle of length 2 (1 compile):
|
||||
|
||||
lib/a.ex (compile)
|
||||
lib/b.ex
|
||||
@@ -524,7 +524,7 @@ defmodule Mix.Tasks.XrefTest do
|
||||
assert_graph(["--format", "cycles", "--label", "compile-connected"], """
|
||||
1 cycles found. Showing them in decreasing size:
|
||||
|
||||
Cycle of length 2:
|
||||
Cycle of length 2 (1 compile):
|
||||
|
||||
lib/a.ex (compile)
|
||||
lib/b.ex
|
||||
@@ -539,7 +539,7 @@ defmodule Mix.Tasks.XrefTest do
|
||||
assert_graph(["--format", "cycles", "--fail-above", "0"], """
|
||||
1 cycles found. Showing them in decreasing size:
|
||||
|
||||
Cycle of length 2:
|
||||
Cycle of length 2 (1 compile):
|
||||
|
||||
lib/a.ex (compile)
|
||||
lib/b.ex
|
||||
@@ -548,11 +548,11 @@ defmodule Mix.Tasks.XrefTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "cycles with min_cycle_size matching actual length" do
|
||||
assert_graph(["--format", "cycles", "--min-cycle-size", "2"], """
|
||||
test "cycles with min_cycle_label matching actual length" do
|
||||
assert_graph(["--format", "cycles", "--label", "compile", "--min-cycle-label", "1"], """
|
||||
1 cycles found. Showing them in decreasing size:
|
||||
|
||||
Cycle of length 2:
|
||||
Cycle of length 2 (1 compile):
|
||||
|
||||
lib/a.ex (compile)
|
||||
lib/b.ex
|
||||
@@ -560,6 +560,24 @@ defmodule Mix.Tasks.XrefTest do
|
||||
""")
|
||||
end
|
||||
|
||||
test "cycles with min_cycle_size matching actual length" do
|
||||
assert_graph(["--format", "cycles", "--min-cycle-size", "2"], """
|
||||
1 cycles found. Showing them in decreasing size:
|
||||
|
||||
Cycle of length 2 (1 compile):
|
||||
|
||||
lib/a.ex (compile)
|
||||
lib/b.ex
|
||||
|
||||
""")
|
||||
end
|
||||
|
||||
test "cycles with min_cycle_label greater than actual length" do
|
||||
assert_graph(["--format", "cycles", "--label", "compile", "--min-cycle-label", "2"], """
|
||||
No cycles found
|
||||
""")
|
||||
end
|
||||
|
||||
test "cycles with min_cycle_size greater than actual length" do
|
||||
assert_graph(["--format", "cycles", "--min-cycle-size", "3"], """
|
||||
No cycles found
|
||||
|
||||
Reference in New Issue
Block a user