Compare commits

...
40 Commits
Author SHA1 Message Date
jamauro 372836d623 Change variable from bk to bucket (#13921)
Simple change from `bk` to `bucket` for consistency with the rest of the doc
2024-10-21 18:00:21 +02:00
Jean Klingler 444cd7e6bd Fix crash in Macro.to_string/1 on invalid sigils (#13905) 2024-10-17 15:39:57 +09:00
Jean Klingler cacb175ab3 Add @doc false to Inspect.Algebra.no_limit/1 (#13893) 2024-10-10 18:21:49 +09:00
Jean Klingler bfce3ed7a9 Fix another dialyzer Kernel.with edge case (#13869)
Close https://github.com/elixir-lang/elixir/issues/13868
2024-09-30 20:11:05 +02:00
Vasily Suvorov 06811cbdc8 Fix typedoc for init_option (#13870)
Change `c:init/1` to`init/2` in the `typedoc`'s text
2024-09-30 15:41:35 +02:00
Igor Goryachev 7a6b35422c Fix logger translator tests under Erlang 27.1 (#13839) 2024-09-21 13:36:09 +02:00
José Valim 78f63d0831 Release v1.17.3 2024-09-18 14:33:39 +02:00
Urmas Talimaa 86ec46a5c4 Fix parsing of fractional durations (#13832)
The parsing of fractional durations checked for non-negativity by
testing second > 0, which reports false for not only negative integers
but also for 0.

Note that changing `if second > 0` to `if second >= 0` would fix
behaviour for "PT0,6S", but would break "PT-0,6S".
2024-09-18 09:03:51 +02:00
José Valim c99631876c Exclude latest tags 2024-09-17 18:01:17 +02:00
José Valim 46815ef554 Fix docs node generation 2024-09-17 16:40:01 +02:00
José Valim 256a55a53e Do not verify modules twice 2024-09-17 16:22:38 +02:00
Jean Klingler a78c92043f Fix: Macro.expand/2 should not expand __ENV__ on :match context (#13807) 2024-09-06 17:58:28 +09:00
José Valim b3bfa519a7 Pass proper path to consolidation in IEx recompile 2024-08-20 11:15:30 +02:00
Steve Cohen b734da3f62 Ensure :native is a valid parameter type (#13781)
Several of the functions in System that deal with time had a typespec
that only allowed the time_unit type even though they accepted the
:native time unit. This confuses dialyzer if :native is passed to
these functions
2024-08-17 10:53:36 +09:00
José Valim 10f86595b5 Raise clearer error message if struct is missing on type checker 2024-08-09 13:05:28 +02:00
José Valim 1da385776f More docs 2024-08-07 01:06:57 +02:00
José Valim a45e08eeda Keep labels with at least one occurrence on xref graph cycles and stats.
Closes #13762.
2024-08-07 00:57:21 +02:00
Artur Plysiuk bb61b953d7 Fix spec for to_timeout/1 (#13755) 2024-08-02 12:15:17 +02:00
José Valim 1e126e88b7 Do not attempt to group module warnings with context
Otherwise, the larger the context, the more expensive
grouping the warnings would get.

Closes #13742
2024-07-28 20:12:15 +02:00
José Valim 47abe2d107 Release v1.17.2 2024-07-06 23:10:25 +02:00
José Valim e254e7e68e Fix regression when fetching repo with a ref
Also note that depth and ref cannot be used in combination.
2024-07-02 20:09:54 +02:00
Wojtek Mach d3c0b38c5d Improvements to generated rel/bin/app.bat (#13679)
* Validate value of `!RELEASE_MODE!`

  * Exit with `ERRORLEVEL` set to `1` on errors.

    Previously we did not and so apps that use releases and rely on exit
    codes to report errors were not doing so (e.g. Livebook).

  * Rename rpc node name from `rem-(...)` to `rpc-(...)`

  * Update generated `rel/env.bat.exs`
2024-07-02 15:21:38 +02:00
José Valim 662dd6b8d9 Add CHANGELOG entry on jaro_distance, closes #13696 2024-06-27 09:14:53 +02:00
José Valim cadd501cbd Clarify cookie generation, closes #13694 2024-06-26 13:28:57 +02:00
Alexey Nikitin ddf84aba7e Fix vm.args.eex generated file when remote? is false (#13688)
before
```
-start_epmd false -erl_epmd_port 6789false
```

after
```
-start_epmd false -erl_epmd_port 6789
```
2024-06-24 09:47:21 +02:00
Jean Klingler 2d808ec652 Fix logger crash when :gen_statem format_status/2 returns non-tuple (#13684) 2024-06-22 20:14:56 +09:00
José Valim 6bfb95ab88 Release v1.17.1 2024-06-18 13:11:00 +02:00
José Valim d53977002d Print type violation slogan 2024-06-18 12:46:59 +02:00
José Valim 776e5a34ea Do not flip async/sync modules in --repeat-until-failure 2024-06-15 18:00:29 +02:00
Jean Klingler ae5707f90a Fix bug when using pinned variable in with else block (#13667) 2024-06-15 18:49:37 +09:00
José Valim 67617e5d8c Do not warn for assignment with blocks in EEx 2024-06-15 11:05:45 +02:00
José Valim 6f5fc94276 Clarify docs, closes #13666 2024-06-15 10:20:40 +02:00
José Valim 6c935ca6ec Clarify charlists intro, closes #13663 2024-06-14 17:34:27 +02:00
José Valim d1076e487c Do not run fixpoint computations on runtime deps
If A depends on B and B depends on C, all as runtime
dependencies, and C changes, there is no change that
can happen in C that will change the result of running
the checker on A.

This could change in the future in two ways:

1. If we introduce inference of return types, then we
   need to infer and check the types for A again. This
   may be an argument for not introducing inference of
   return types (only of patterns and guards)

2. We will need to track "type exports" once we introduce
   type aliases
2024-06-14 12:07:02 +02:00
Jonatan Kłosko b9e447757d Update RELEASE_DISTRIBUTION docs (#13658) 2024-06-13 16:15:45 +02:00
Jean Klingler b7943e113d Fix dialyzer error when with else clause is calling a no_return function (#13659)
Close #13656
2024-06-13 16:15:32 +02:00
Ben Murden 1a320bb60d Minor grammatical fixes on warning message (#13654) 2024-06-13 16:15:25 +02:00
Ian Harris 64e63b1b9d Update compatibility-and-deprecations.md (#13657)
Fixed link for changelog to v1.17.
2024-06-12 19:12:55 +02:00
José Valim 5a8b28427c Update CHANGELOG 2024-06-12 15:20:57 +02:00
José Valim a1eb59d035 Remove /main link from CHANGELOG 2024-06-12 13:54:10 +02:00
46 changed files with 809 additions and 346 deletions
+57 -4
View File
@@ -22,8 +22,6 @@ We focused on atoms and maps on this initial release as they are respectively th
* Accessing a key on a map or a struct that does not have the given key, such as `user.adress`
* Updating a struct or a map that does not define the given key, such as `%{user | adress: ...}`
* Invoking a function on non-modules, such as `user.address()`
* Capturing a function on non-modules, such as `&user.address/0`
@@ -40,7 +38,7 @@ We focused on atoms and maps on this initial release as they are respectively th
* Accessing a field that is not defined in a rescued exception
These new warnings help Elixir developers find bugs earlier and give more confidence when refactoring code, especially around maps and structs. While some of these warnings were emitted in the past, they were discovered using syntax analysis. The new warnings are more reliable, precise, and with better error messages. Keep in mind, however, that the Elixir typechecker only infers types from patterns within the same function at the moment. Analysis from guards and across function boundaries will be added in future relases. For more details, see our new [reference document on gradual set-theoretic types](https://hexdocs.pm/elixir/main/gradual-set-theoretic-types.html).
These new warnings help Elixir developers find bugs earlier and give more confidence when refactoring code, especially around maps and structs. While some of these warnings were emitted in the past, they were discovered using syntax analysis. The new warnings are more reliable, precise, and with better error messages. Keep in mind, however, that the Elixir typechecker only infers types from patterns within the same function at the moment. Analysis from guards and across function boundaries will be added in future relases. For more details, see our new [reference document on gradual set-theoretic types](https://hexdocs.pm/elixir/gradual-set-theoretic-types.html).
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
@@ -72,6 +70,61 @@ Finally, a new `Kernel.to_timeout/1` function has been added, which helps develo
Process.send_after(pid, :wake_up, to_timeout(hour: 1))
```
## v1.17.3 (2024-09-18)
### 1. Bug fixes
#### Elixir
* [Duration] Fix parsing of fractional durations with non-positive seconds
* [Kernel] Do not attempt to group module warnings when they have a large context
#### IEx
* [IEx.Helpers] Properly reconsolidate protocols on `recompile`
#### Mix
* [mix compile.elixir] Do not verify modules twice
* [mix xref] Respect the `--label` option on stats and cycles
## v1.17.2 (2024-07-06)
### 1. Bug fixes
#### Logger
* [Logger.Translator] Fix logger crash when `:gen_statem`'s `format_status/2` returns non-tuple
#### Mix
* [mix deps.get] Fix regression when fetching a git repository with a `:ref`
* [mix release] Validate `RELEASE_MODE` and set ERRORLEVEL on `.bat` scripts
* [mix release] Fix invalid example in code comment inside the generated vm.args.eex
## v1.17.1 (2024-06-18)
### 1. Enhancements
#### Mix
* [mix compile.elixir] Do not run fixpoint computation on runtime dependencies. This should considerably improve compilation times for large projects when changing only one or two files
### 2. Bug fixes
#### EEx
* [EEx] Do not warn for assignment with blocks in EEx
#### Elixir
* [Kernel] Fix bug when using pinned variables inside `with`'s `else` patterns
* [Kernel] Fix Dialyzer error when with else clause is calling a `no_return` function
#### ExUnit
* [ExUnit] Do not alternative sync/async suites on `--repeat-until-failure`
## v1.17.0 (2024-06-12)
### 1. Enhancements
@@ -126,7 +179,6 @@ Process.send_after(pid, :wake_up, to_timeout(hour: 1))
* [mix test] Add `mix test --slowest-modules` to print slowest modules based on all of the tests they hold
* [mix test] Generate cover HTML files in parallel
### 2. Bug fixes
#### Elixir
@@ -141,6 +193,7 @@ Process.send_after(pid, :wake_up, to_timeout(hour: 1))
* [Kernel] Avoid double tracing events when capturing a function
* [Kernel] Fix a bug where captured arguments would conflict when a capture included a macro that also used captures
* [Module] Return default value in `Module.get_attribute/3` for persisted attributes which have not yet been written to
* [String] Properly handle transpositions in `jaro_distance`. This will correct the distance result in certain cases
#### IEx
+4 -4
View File
@@ -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
View File
@@ -1 +1 @@
1.17.0
1.17.3
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/sh
set -e
ELIXIR_VERSION=1.17.0
ELIXIR_VERSION=1.17.3
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
+1 -1
View File
@@ -1,6 +1,6 @@
@echo off
set ELIXIR_VERSION=1.17.0
set ELIXIR_VERSION=1.17.3
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+7 -7
View File
@@ -340,13 +340,6 @@ defmodule EEx.Compiler do
scope,
state
) do
if mark == ~c"" do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end
{rest, line, contents} = look_ahead_middle(rest, meta.line, chars) || {rest, meta.line, chars}
start_line = meta.line
start_column = column(meta.column, mark)
@@ -359,6 +352,13 @@ defmodule EEx.Compiler do
%{state | quoted: [], line: line}
)
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
generate_buffer(rest, buffer, scope, state)
end
-9
View File
@@ -43,15 +43,6 @@ defmodule EEx.SmartEngineTest do
assert_received :found
end
test "error with unused \"do\" block without \"<%=\" modifier" do
stderr =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
assert_eval("", "<% if true do %>I'm invisible!<% end %>", assigns: %{})
end)
assert stderr =~ "the contents of this expression won't be output"
end
defp assert_eval(expected, actual, binding \\ []) do
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
assert result == expected
+10
View File
@@ -543,6 +543,16 @@ defmodule EExTest do
~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"]
end
test "unused \"do\" block without \"<%=\" modifier" do
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
EEx.compile_string("<% if true do %>I'm invisible!<% end %>")
end) =~ "the contents of this expression won't be output"
# These are fine though
EEx.compile_string("<% foo = fn -> %>Hello<% end %>")
EEx.compile_string("<% foo = if true do %>Hello<% end %>")
end
test "from tokenizer" do
warning =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
+9 -1
View File
@@ -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}
_ ->
+10 -9
View File
@@ -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
+15
View File
@@ -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
+2 -3
View File
@@ -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
+2 -2
View File
@@ -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
+25 -9
View File
@@ -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
+18 -8
View File
@@ -176,10 +176,15 @@ defmodule Module.Types.Of do
# TODO: Use the struct default values to define the default types.
def struct(struct, args_types, default_handling, meta, stack, context) do
context = remote(struct, :__struct__, 0, meta, stack, context)
info =
struct.__info__(:struct) ||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
term = term()
defaults =
for %{field: field} <- struct.__info__(:struct), field != :__struct__ do
for %{field: field} <- info, field != :__struct__ do
{field, term}
end
@@ -678,8 +683,8 @@ defmodule Module.Types.Of do
Comparison operators (>, <, >=, <=, min, and max) perform structural \
and not semantic comparison. Comparing with a struct won't give meaningful \
results. Struct that can be compared typically define a compare/2 function \
within their modules that can be used for semantic comparison
results. Structs that can be compared typically define a compare/2 function \
within their modules that can be used for semantic comparison.
"""
])
}
@@ -700,14 +705,16 @@ defmodule Module.Types.Of do
" is undefined (module ",
inspect(module),
" is not available or is yet to be defined)"
])
]),
group: true
}
end
def format_diagnostic({:undefined_function, module, :__struct__, 0, _exports}) do
%{
message:
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)"
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)",
group: true
}
end
@@ -718,7 +725,8 @@ defmodule Module.Types.Of do
Exception.format_mfa(module, fun, arity),
" is undefined or private",
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports)
])
]),
group: true
}
end
@@ -729,7 +737,8 @@ defmodule Module.Types.Of do
Exception.format_mfa(module, fun, arity),
" is deprecated. ",
reason
])
]),
group: true
}
end
@@ -741,7 +750,8 @@ defmodule Module.Types.Of do
inspect(module),
" before invoking the macro ",
Exception.format_mfa(module, fun, arity)
])
]),
group: true
}
end
+1 -1
View File
@@ -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}
+3 -3
View File
@@ -1239,7 +1239,7 @@ defmodule System do
This time is monotonically increasing and starts in an unspecified
point in time.
"""
@spec monotonic_time(time_unit) :: integer
@spec monotonic_time(time_unit | :native) :: integer
def monotonic_time(unit) do
:erlang.monotonic_time(normalize_time_unit(unit))
end
@@ -1265,7 +1265,7 @@ defmodule System do
case of time warps although the VM works towards aligning
them. This time is not monotonic.
"""
@spec system_time(time_unit) :: integer
@spec system_time(time_unit | :native) :: integer
def system_time(unit) do
:erlang.system_time(normalize_time_unit(unit))
end
@@ -1316,7 +1316,7 @@ defmodule System do
`monotonic_time/1`), gives the Erlang system time that corresponds
to that monotonic time.
"""
@spec time_offset(time_unit) :: integer
@spec time_offset(time_unit | :native) :: integer
def time_offset(unit) do
:erlang.time_offset(normalize_time_unit(unit))
end
@@ -9,7 +9,9 @@ iex> is_binary(string)
true
```
In this chapter, we will gain clarity on what exactly binaries are, how they relate to strings, and what single-quoted values, `'like this'`, mean in Elixir. Although strings are one of the most common data types in computer languages, they are subtly complex and are often misunderstood. To understand strings in Elixir, we have to educate ourselves about [Unicode](https://en.wikipedia.org/wiki/Unicode) and character encodings, specifically the [UTF-8](https://en.wikipedia.org/wiki/UTF-8) encoding.
In this chapter, we will gain clarity on what exactly binaries are and how they relate to strings. We will also learn about charlists, `~c"like this"`, which are often used for interoperability with Erlang.
Although strings are one of the most common data types in computer languages, they are subtly complex and are often misunderstood. To understand strings in Elixir, let's first discuss [Unicode](https://en.wikipedia.org/wiki/Unicode) and character encodings, specifically the [UTF-8](https://en.wikipedia.org/wiki/UTF-8) encoding.
## Unicode and Code Points
+1 -1
View File
@@ -124,7 +124,7 @@ iex> {:ok, registry} = GenServer.start_link(KV.Registry, :ok)
{:ok, #PID<0.136.0>}
iex> GenServer.cast(registry, {:create, "shopping"})
:ok
iex> {:ok, bk} = GenServer.call(registry, {:lookup, "shopping"})
iex> {:ok, bucket} = GenServer.call(registry, {:lookup, "shopping"})
{:ok, #PID<0.174.0>}
```
@@ -213,4 +213,4 @@ Version | Deprecated feature | Replaced by (ava
[v1.14]: https://github.com/elixir-lang/elixir/blob/v1.14/CHANGELOG.md#4-hard-deprecations
[v1.15]: https://github.com/elixir-lang/elixir/blob/v1.15/CHANGELOG.md#4-hard-deprecations
[v1.16]: https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md#4-hard-deprecations
[v1.17]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations
[v1.17]: https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md#4-hard-deprecations
+7 -4
View File
@@ -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)
+4 -4
View File
@@ -425,16 +425,16 @@ translate_with_else(Meta, [{'else', Else}], S) ->
RaiseClause = {'->', Generated, [[RaiseVar], RaiseExpr]},
Clauses = elixir_erl_clauses:get_clauses('else', [{'else', Else ++ [RaiseClause]}], match),
{TranslatedClauses, SC} = elixir_erl_clauses:clauses(Clauses, SV),
with_else_closure(Meta, TranslatedClauses, SC).
{TranslatedClauses, SC} = elixir_erl_clauses:clauses(Clauses, SV#elixir_erl{extra=pin_guard}),
with_else_closure(Generated, TranslatedClauses, SC#elixir_erl{extra=SV#elixir_erl.extra}).
with_else_closure(Meta, TranslatedClauses, S) ->
Ann = ?ann(Meta),
{_, FunErlVar, SC} = elixir_erl_var:assign(Meta, S),
{_, ArgErlVar, SA} = elixir_erl_var:assign(Meta, SC),
FunAssign = {match, Ann, FunErlVar, {'fun', Ann, {clauses, TranslatedClauses}}},
FunCall = {call, Ann, FunErlVar, [ArgErlVar]},
Generated = erl_anno:set_generated(true, Ann),
FunAssign = {match, Ann, FunErlVar, {'fun', Generated, {clauses, TranslatedClauses}}},
FunCall = {call, Ann, FunErlVar, [ArgErlVar]},
{{clause, Generated, [ArgErlVar], [], [FunCall]}, FunAssign, SA}.
translate_with_do([{'<-', Meta, [{Var, _, Ctx} = Left, Expr]} | Rest], Ann, Do, Else, S) when is_atom(Var), is_atom(Ctx) ->
+1 -1
View File
@@ -177,7 +177,7 @@ format_snippet(Severity, Position, File, Message, Snippet, Diagnostic) ->
unicode:characters_to_binary(Formatted).
format_detail(#{detail := #{typing_traces := _}}, Message) -> [Message | "\ntyping violation found at:"];
format_detail(#{details := #{typing_traces := _}}, Message) -> [Message | "\ntyping violation found at:"];
format_detail(_, Message) -> Message.
calculate_span_length({StartLine, StartCol}, #{span := {StartLine, EndCol}}) -> EndCol - StartCol;
@@ -237,8 +237,10 @@ defmodule DurationTest do
assert Duration.from_iso8601("P4Y2W3Y") == {:error, :invalid_date_component}
assert Duration.from_iso8601("P5HT4MT3S") == {:error, :invalid_date_component}
assert Duration.from_iso8601("P5H3HT4M") == {:error, :invalid_date_component}
assert Duration.from_iso8601("P0.5Y") == {:error, :invalid_date_component}
assert Duration.from_iso8601("PT1D") == {:error, :invalid_time_component}
assert Duration.from_iso8601("PT.6S") == {:error, :invalid_time_component}
assert Duration.from_iso8601("PT0.5H") == {:error, :invalid_time_component}
assert Duration.from_iso8601("invalid") == {:error, :invalid_duration}
end
@@ -262,6 +264,9 @@ defmodule DurationTest do
assert Duration.from_iso8601!("PT6S") == %Duration{second: 6}
assert Duration.from_iso8601!("PT1,6S") == %Duration{second: 1, microsecond: {600_000, 1}}
assert Duration.from_iso8601!("PT-1.6S") == %Duration{second: -1, microsecond: {-600_000, 1}}
assert Duration.from_iso8601!("PT0,6S") == %Duration{second: 0, microsecond: {600_000, 1}}
assert Duration.from_iso8601!("PT-0,6S") == %Duration{second: 0, microsecond: {-600_000, 1}}
assert Duration.from_iso8601!("-PT-0,6S") == %Duration{second: 0, microsecond: {600_000, 1}}
assert Duration.from_iso8601!("-P10DT4H") == %Duration{day: -10, hour: -4}
assert Duration.from_iso8601!("-P10DT-4H") == %Duration{day: -10, hour: 4}
assert Duration.from_iso8601!("P-10D") == %Duration{day: -10}
@@ -172,6 +172,20 @@ defmodule Code.Normalizer.QuotedASTTest do
) == ~s[~S"""\n"123"\n"""]
end
test "regression: invalid sigil calls" do
assert quoted_to_string(quote do: sigil_r(<<"foo", 123>>, [])) ==
"sigil_r(<<\"foo\", 123>>, [])"
assert quoted_to_string(quote do: sigil_r(<<"foo">>, :invalid_modifiers)) ==
"sigil_r(\"foo\", :invalid_modifiers)"
assert quoted_to_string(quote do: sigil_r(<<"foo">>, [:invalid_modifier])) ==
"sigil_r(\"foo\", [:invalid_modifier])"
assert quoted_to_string(quote do: sigil_r(<<"foo">>, [])) == "~r\"foo\""
assert quoted_to_string(quote do: sigil_r(<<"foo">>, [?a, ?b, ?c])) == "~r\"foo\"abc"
end
test "tuple" do
assert quoted_to_string(quote do: {1, 2}) == "{1, 2}"
assert quoted_to_string(quote do: {1}) == "{1}"
@@ -0,0 +1,13 @@
defmodule Dialyzer.WithNoReturn do
def with_no_return(list) do
no_return = fn -> throw(:no_return) end
with [] <- list do
:ok
else
# note: throwing here directly wouldn't be caught in the first place,
# calling a no_return function is what could cause an issue.
_ -> no_return.()
end
end
end
@@ -0,0 +1,12 @@
defmodule Dialyzer.WithThrowingElse do
def with_throwing_else(map) do
with {:ok, foo} <- Map.fetch(map, :foo),
false <- Enum.empty?(foo) do
foo
else
# several clauses but one is a no_return
:error -> throw(:empty_map)
true -> nil
end
end
end
@@ -26,6 +26,7 @@ defmodule Kernel.DialyzerTest do
:elixir,
:elixir_env,
:elixir_erl_pass,
:maps,
ArgumentError,
Atom,
Code,
@@ -159,6 +160,16 @@ defmodule Kernel.DialyzerTest do
assert_dialyze_no_warnings!(context)
end
test "no warnings on with when else has a no_return type", context do
copy_beam!(context, Dialyzer.WithNoReturn)
assert_dialyze_no_warnings!(context)
end
test "no warnings on with when multiple else clauses and one is a no_return", context do
copy_beam!(context, Dialyzer.WithThrowingElse)
assert_dialyze_no_warnings!(context)
end
test "no warnings on defmacrop", context do
copy_beam!(context, Dialyzer.Defmacrop)
assert_dialyze_no_warnings!(context)
@@ -25,6 +25,30 @@ defmodule Kernel.WithTest do
assert with({^key, res} <- ok(42), do: res) == 42
end
test "pin matching with multiple else" do
key = :error
first_else =
with nil <- error() do
:ok
else
^key -> :pinned
_other -> :other
end
assert first_else == :pinned
second_else =
with nil <- ok(42) do
:ok
else
^key -> :pinned
_other -> :other
end
assert second_else == :other
end
test "two levels with" do
result =
with {:ok, n1} <- ok(11),
+10
View File
@@ -190,6 +190,16 @@ defmodule MacroTest do
assert Code.eval_quoted(expanded) == {env.versioned_vars, []}
end
test "env in :match context does not expand" do
env = %{__ENV__ | line: 0, lexical_tracker: self(), context: :match}
expanded = Macro.expand_once(quote(do: __ENV__), env)
assert expanded == quote(do: __ENV__)
expanded = Macro.expand_once(quote(do: __ENV__.file), env)
assert expanded == quote(do: __ENV__.file)
end
defmacro local_macro(), do: raise("ignored")
test "vars" do
@@ -382,7 +382,7 @@ defmodule Module.Types.ExprTest do
# from: types_test.ex:LINE-2
y = %Point{}
Comparison operators (>, <, >=, <=, min, and max) perform structural and not semantic comparison. Comparing with a struct won't give meaningful results. Struct that can be compared typically define a compare/2 function within their modules that can be used for semantic comparison
Comparison operators (>, <, >=, <=, min, and max) perform structural and not semantic comparison. Comparing with a struct won't give meaningful results. Structs that can be compared typically define a compare/2 function within their modules that can be used for semantic comparison.
"""}
end
end
+1 -1
View File
@@ -500,7 +500,7 @@ defmodule ExUnit do
defp maybe_repeated_run(options, seed, load_us, repeat) do
case ExUnit.Runner.run(options, load_us) do
{%{failures: 0}, {sync_modules, async_modules}}
{%{failures: 0}, {async_modules, sync_modules}}
when repeat > 0 and (sync_modules != [] or async_modules != []) ->
ExUnit.Server.restore_modules(async_modules, sync_modules)
+7 -9
View File
@@ -535,15 +535,13 @@ defmodule ExUnit.Callbacks do
test, as simply shutting down the process would cause it to be restarted
according to its `:restart` value.
Another advantage is that the test process will act as both an ancestor
as well as a caller to the supervised processes. When a process is started
under a supervision tree, it typically populates the `$ancestors` key in
its process dictionary with all of its ancestors, which will include the test
process. Additionally, `start_supervised/2` will also store the test process
in the `$callers` key of the started process, allowing tools that perform
either ancestor or caller tracking to reach the test process. You can learn
more about these keys in
[the `Task` module](`Task#module-ancestor-and-caller-tracking`).
Finally, since Elixir v1.17.0, the test supervisor has both `$ancestors`
and `$callers` key in its process dictionary pointing to the test process.
This means developers can invoke `Process.get(:"$callers", [])` in their
`start_link` function and forward it to the spawned process, which may set
`Process.put(:"$callers", callers)` during its initialization. This may be
useful in projects who track process ownership during tests. You can learn
more about these keys in [the `Task` module](`Task#module-ancestor-and-caller-tracking`).
"""
@doc since: "1.5.0"
@spec start_supervised(Supervisor.child_spec() | module | {module, term}, keyword) ::
@@ -256,6 +256,28 @@ defmodule ExUnit.AssertionsTest do
end
end
test "assert match with __ENV__ in the pattern" do
message =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
assert_raise CompileError, fn ->
Code.eval_string("""
defmodule EnvMatch do
import ExUnit.Assertions
def run do
assert __ENV__ = %{}
end
end
""")
end
end)
assert message =~ "invalid pattern in match, __ENV__ is not allowed in matches"
after
:code.purge(EnvMatch)
:code.delete(EnvMatch)
end
test "assert match?" do
true = assert match?({2, 1}, Value.tuple())
+1 -1
View File
@@ -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)
+1 -3
View File
@@ -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)
+64 -18
View File
@@ -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
+43 -28
View File
@@ -609,8 +609,8 @@ defmodule Mix.Compilers.Elixir do
# within the dependency, they will be recompiled. However, export
# and runtime dependencies won't have recompiled so we need to
# propagate them to the parent app.
{dep_modules, _, _} =
fixpoint_runtime_modules(manifest_sources, Map.from_keys(dep_modules, true))
dep_modules =
fixpoint_non_compile_modules(manifest_sources, Map.from_keys(dep_modules, true))
old_exports = Map.get(deps_exports, app, %{})
@@ -654,21 +654,19 @@ defmodule Mix.Compilers.Elixir do
end
end
defp fixpoint_runtime_modules(sources, modules) when modules != %{} do
fixpoint_runtime_modules(Map.to_list(sources), modules, false, [], [], sources)
defp fixpoint_non_compile_modules(sources, modules) when modules != %{} do
fixpoint_non_compile_modules(Map.to_list(sources), modules, false, [])
end
defp fixpoint_runtime_modules(sources, modules) do
{modules, [], sources}
defp fixpoint_non_compile_modules(_sources, modules) do
modules
end
defp fixpoint_runtime_modules(
[{source_path, source_entry} = pair | sources],
defp fixpoint_non_compile_modules(
[{_source_path, source_entry} = pair | sources],
modules,
new?,
pending_sources,
acc_modules,
acc_sources
pending_sources
) do
source(export_references: export_refs, runtime_references: runtime_refs) = source_entry
@@ -676,24 +674,19 @@ defmodule Mix.Compilers.Elixir do
new_modules = Enum.reject(source(source_entry, :modules), &Map.has_key?(modules, &1))
modules = Enum.reduce(new_modules, modules, &Map.put(&2, &1, true))
new? = new? or new_modules != []
acc_modules = new_modules ++ acc_modules
acc_sources =
Map.replace!(acc_sources, source_path, source(source_entry, runtime_warnings: []))
fixpoint_runtime_modules(sources, modules, new?, pending_sources, acc_modules, acc_sources)
fixpoint_non_compile_modules(sources, modules, new?, pending_sources)
else
pending_sources = [pair | pending_sources]
fixpoint_runtime_modules(sources, modules, new?, pending_sources, acc_modules, acc_sources)
fixpoint_non_compile_modules(sources, modules, new?, pending_sources)
end
end
defp fixpoint_runtime_modules([], modules, new?, pending_sources, acc_modules, acc_sources)
defp fixpoint_non_compile_modules([], modules, new?, pending_sources)
when new? == false or pending_sources == [],
do: {modules, acc_modules, acc_sources}
do: modules
defp fixpoint_runtime_modules([], modules, true, pending_sources, acc_modules, acc_sources),
do: fixpoint_runtime_modules(pending_sources, modules, false, [], acc_modules, acc_sources)
defp fixpoint_non_compile_modules([], modules, true, pending_sources),
do: fixpoint_non_compile_modules(pending_sources, modules, false, [])
defp exports_md5(module, use_attributes?) do
cond do
@@ -1068,7 +1061,7 @@ defmodule Mix.Compilers.Elixir do
end
end
defp each_cycle(runtime_modules, compile_path, timestamp, state) do
defp each_cycle(stale_modules, compile_path, timestamp, state) do
{modules, _exports, sources, changed, pending_modules, stale_exports} = state
{pending_modules, exports, changed} =
@@ -1081,11 +1074,33 @@ defmodule Mix.Compilers.Elixir do
end
if changed == [] do
# We merge runtime_modules (which is a map of %{module => true}) into
# a map of modules (which is a map of %{module => record}). This is fine
# since fixpoint_runtime_modules only cares about map keys.
{_, runtime_modules, sources} =
fixpoint_runtime_modules(sources, Map.merge(modules, runtime_modules))
# We merge stale_modules (which is a map of %{module => true} that the user changed)
# into a map of modules we compiled (which is a map of %{module => record}). This is
# fine because we only care about the keys.
changed_modules = Map.merge(modules, stale_modules)
# Now we do a simple pass finding anything that directly depends on the modules that
# changed. We don't need to compute a fixpoint, because now only the directly affected
# matter.
{sources, runtime_modules} =
Enum.reduce(sources, {sources, []}, fn
{source_path, source_entry}, {acc_sources, acc_modules} ->
source(export_references: export_refs, runtime_references: runtime_refs) =
source_entry
if has_any_key?(changed_modules, export_refs) or
has_any_key?(changed_modules, runtime_refs) do
acc_sources =
Map.replace!(acc_sources, source_path, source(source_entry, runtime_warnings: []))
new_modules =
Enum.reject(source(source_entry, :modules), &Map.has_key?(changed_modules, &1))
{acc_sources, new_modules ++ acc_modules}
else
{acc_sources, acc_modules}
end
end)
runtime_paths =
Enum.map(runtime_modules, &{&1, Path.join(compile_path, Atom.to_string(&1) <> ".beam")})
+18 -16
View File
@@ -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
+2 -1
View File
@@ -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
+5 -5
View File
@@ -402,8 +402,8 @@ defmodule Mix.Tasks.Release do
If you are setting this option manually, we recommend the cookie option
to be a long and randomly generated string, such as:
`Base.url_encode64(:crypto.strong_rand_bytes(40))`. We also recommend to restrict
the characters in the cookie to the subset returned by `Base.url_encode64/1`.
`Base.encode32(:crypto.strong_rand_bytes(40))`. We also recommend restricting
the characters in the cookie to only alphanumeric characters and underscore.
* `:validate_compile_env` - by default a release will match all runtime
configuration against any configuration that was marked at compile time
@@ -833,9 +833,9 @@ defmodule Mix.Tasks.Release do
* `RELEASE_DISTRIBUTION` - how do we want to run the distribution.
May be `name` (long names), `sname` (short names) or `none`
(distribution is not started automatically). Defaults to
`sname` which allows access only within the current system.
`name` allows external connections
(distribution is not started automatically). Defaults to `sname`.
When connecting nodes across hosts, you typically want to set
this to `name` (required to use IPs as host names)
* `RELEASE_BOOT_SCRIPT` - the name of the boot script to use when starting
the release. This script is used when running commands such as `start` and
+31 -8
View File
@@ -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
View File
@@ -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("")
+5 -10
View File
@@ -1597,11 +1597,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
def foo(), do: B.foo()
def bar(), do: B.bar()
def __after_verify__(__MODULE__) do
if Code.ensure_loaded?(B) and not function_exported?(B, :foo, 0) do
:ok
else
IO.warn("AFTER_VERIFY", __ENV__)
end
IO.warn("AFTER_VERIFY", __ENV__)
end
end
""")
@@ -1630,10 +1626,9 @@ defmodule Mix.Tasks.Compile.ElixirTest do
end)
# Check B due to direct dependency on A
# Check C due to transient dependency on A
assert output =~ "A.foo/0 is undefined or private"
assert output =~ "B.bar/0 is undefined or private"
assert output =~ "AFTER_VERIFY"
refute output =~ "B.bar/0 is undefined or private"
refute output =~ "AFTER_VERIFY"
# Ensure only A was recompiled
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
@@ -1650,7 +1645,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
assert output =~ "B.bar/0 is undefined or private"
assert output =~ "AFTER_VERIFY"
# Now we change B and it must no longer emit an AFTER_VERIFY warning
# Now we change B and it must emit an AFTER_VERIFY warning
File.write!("lib/b.ex", """
defmodule B do
end
@@ -1663,7 +1658,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
assert output =~ "B.foo/0 is undefined or private"
assert output =~ "B.bar/0 is undefined or private"
refute output =~ "AFTER_VERIFY"
assert output =~ "AFTER_VERIFY"
end)
end
-37
View File
@@ -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
+73 -2
View File
@@ -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
""")