Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47abe2d107 | ||
|
|
e254e7e68e | ||
|
|
d3c0b38c5d | ||
|
|
662dd6b8d9 | ||
|
|
cadd501cbd | ||
|
|
ddf84aba7e | ||
|
|
2d808ec652 | ||
|
|
6bfb95ab88 | ||
|
|
d53977002d | ||
|
|
776e5a34ea | ||
|
|
ae5707f90a | ||
|
|
67617e5d8c | ||
|
|
6f5fc94276 | ||
|
|
6c935ca6ec | ||
|
|
d1076e487c | ||
|
|
b9e447757d | ||
|
|
b7943e113d | ||
|
|
1a320bb60d | ||
|
|
64e63b1b9d | ||
|
|
5a8b28427c | ||
|
|
a1eb59d035 |
+39
-4
@@ -22,8 +22,6 @@ We focused on atoms and maps on this initial release as they are respectively th
|
||||
|
||||
* Accessing a key on a map or a struct that does not have the given key, such as `user.adress`
|
||||
|
||||
* Updating a struct or a map that does not define the given key, such as `%{user | adress: ...}`
|
||||
|
||||
* Invoking a function on non-modules, such as `user.address()`
|
||||
|
||||
* Capturing a function on non-modules, such as `&user.address/0`
|
||||
@@ -40,7 +38,7 @@ We focused on atoms and maps on this initial release as they are respectively th
|
||||
|
||||
* Accessing a field that is not defined in a rescued exception
|
||||
|
||||
These new warnings help Elixir developers find bugs earlier and give more confidence when refactoring code, especially around maps and structs. While some of these warnings were emitted in the past, they were discovered using syntax analysis. The new warnings are more reliable, precise, and with better error messages. Keep in mind, however, that the Elixir typechecker only infers types from patterns within the same function at the moment. Analysis from guards and across function boundaries will be added in future relases. For more details, see our new [reference document on gradual set-theoretic types](https://hexdocs.pm/elixir/main/gradual-set-theoretic-types.html).
|
||||
These new warnings help Elixir developers find bugs earlier and give more confidence when refactoring code, especially around maps and structs. While some of these warnings were emitted in the past, they were discovered using syntax analysis. The new warnings are more reliable, precise, and with better error messages. Keep in mind, however, that the Elixir typechecker only infers types from patterns within the same function at the moment. Analysis from guards and across function boundaries will be added in future relases. For more details, see our new [reference document on gradual set-theoretic types](https://hexdocs.pm/elixir/gradual-set-theoretic-types.html).
|
||||
|
||||
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
|
||||
|
||||
@@ -72,6 +70,43 @@ 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.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 +161,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 +175,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
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.17.0
|
||||
ELIXIR_VERSION=1.17.2
|
||||
|
||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||
cat <<USAGE >&2
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
@echo off
|
||||
|
||||
set ELIXIR_VERSION=1.17.0
|
||||
set ELIXIR_VERSION=1.17.2
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
|
||||
@@ -340,13 +340,6 @@ defmodule EEx.Compiler do
|
||||
scope,
|
||||
state
|
||||
) do
|
||||
if mark == ~c"" do
|
||||
message =
|
||||
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
|
||||
|
||||
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
|
||||
end
|
||||
|
||||
{rest, line, contents} = look_ahead_middle(rest, meta.line, chars) || {rest, meta.line, chars}
|
||||
start_line = meta.line
|
||||
start_column = column(meta.column, mark)
|
||||
@@ -359,6 +352,13 @@ defmodule EEx.Compiler do
|
||||
%{state | quoted: [], line: line}
|
||||
)
|
||||
|
||||
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
|
||||
message =
|
||||
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
|
||||
|
||||
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
|
||||
end
|
||||
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
@@ -43,15 +43,6 @@ defmodule EEx.SmartEngineTest do
|
||||
assert_received :found
|
||||
end
|
||||
|
||||
test "error with unused \"do\" block without \"<%=\" modifier" do
|
||||
stderr =
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
assert_eval("", "<% if true do %>I'm invisible!<% end %>", assigns: %{})
|
||||
end)
|
||||
|
||||
assert stderr =~ "the contents of this expression won't be output"
|
||||
end
|
||||
|
||||
defp assert_eval(expected, actual, binding \\ []) do
|
||||
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
|
||||
assert result == expected
|
||||
|
||||
@@ -543,6 +543,16 @@ defmodule EExTest do
|
||||
~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"]
|
||||
end
|
||||
|
||||
test "unused \"do\" block without \"<%=\" modifier" do
|
||||
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
EEx.compile_string("<% if true do %>I'm invisible!<% end %>")
|
||||
end) =~ "the contents of this expression won't be output"
|
||||
|
||||
# These are fine though
|
||||
EEx.compile_string("<% foo = fn -> %>Hello<% end %>")
|
||||
EEx.compile_string("<% foo = if true do %>Hello<% end %>")
|
||||
end
|
||||
|
||||
test "from tokenizer" do
|
||||
warning =
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
|
||||
@@ -678,8 +678,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.
|
||||
"""
|
||||
])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(Meta, TranslatedClauses, SC#elixir_erl{extra=SV#elixir_erl.extra}).
|
||||
|
||||
with_else_closure(Meta, TranslatedClauses, S) ->
|
||||
Ann = ?ann(Meta),
|
||||
{_, FunErlVar, SC} = elixir_erl_var:assign(Meta, S),
|
||||
{_, ArgErlVar, SA} = elixir_erl_var:assign(Meta, SC),
|
||||
FunAssign = {match, Ann, FunErlVar, {'fun', Ann, {clauses, TranslatedClauses}}},
|
||||
FunCall = {call, Ann, FunErlVar, [ArgErlVar]},
|
||||
Generated = erl_anno:set_generated(true, Ann),
|
||||
FunAssign = {match, Ann, FunErlVar, {'fun', Generated, {clauses, TranslatedClauses}}},
|
||||
FunCall = {call, Ann, FunErlVar, [ArgErlVar]},
|
||||
{{clause, Generated, [ArgErlVar], [], [FunCall]}, FunAssign, SA}.
|
||||
|
||||
translate_with_do([{'<-', Meta, [{Var, _, Ctx} = Left, Expr]} | Rest], Ann, Do, Else, S) when is_atom(Var), is_atom(Ctx) ->
|
||||
|
||||
@@ -177,7 +177,7 @@ format_snippet(Severity, Position, File, Message, Snippet, Diagnostic) ->
|
||||
|
||||
unicode:characters_to_binary(Formatted).
|
||||
|
||||
format_detail(#{detail := #{typing_traces := _}}, Message) -> [Message | "\ntyping violation found at:"];
|
||||
format_detail(#{details := #{typing_traces := _}}, Message) -> [Message | "\ntyping violation found at:"];
|
||||
format_detail(_, Message) -> Message.
|
||||
|
||||
calculate_span_length({StartLine, StartCol}, #{span := {StartLine, EndCol}}) -> EndCol - StartCol;
|
||||
|
||||
@@ -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
|
||||
@@ -159,6 +159,11 @@ 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 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),
|
||||
|
||||
@@ -382,7 +382,7 @@ defmodule Module.Types.ExprTest do
|
||||
# from: types_test.ex:LINE-2
|
||||
y = %Point{}
|
||||
|
||||
Comparison operators (>, <, >=, <=, min, and max) perform structural and not semantic comparison. Comparing with a struct won't give meaningful results. Struct that can be compared typically define a compare/2 function within their modules that can be used for semantic comparison
|
||||
Comparison operators (>, <, >=, <=, min, and max) perform structural and not semantic comparison. Comparing with a struct won't give meaningful results. Structs that can be compared typically define a compare/2 function within their modules that can be used for semantic comparison.
|
||||
"""}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -500,7 +500,7 @@ defmodule ExUnit do
|
||||
|
||||
defp maybe_repeated_run(options, seed, load_us, repeat) do
|
||||
case ExUnit.Runner.run(options, load_us) do
|
||||
{%{failures: 0}, {sync_modules, async_modules}}
|
||||
{%{failures: 0}, {async_modules, sync_modules}}
|
||||
when repeat > 0 and (sync_modules != [] or async_modules != []) ->
|
||||
ExUnit.Server.restore_modules(async_modules, sync_modules)
|
||||
|
||||
|
||||
@@ -535,15 +535,13 @@ defmodule ExUnit.Callbacks do
|
||||
test, as simply shutting down the process would cause it to be restarted
|
||||
according to its `:restart` value.
|
||||
|
||||
Another advantage is that the test process will act as both an ancestor
|
||||
as well as a caller to the supervised processes. When a process is started
|
||||
under a supervision tree, it typically populates the `$ancestors` key in
|
||||
its process dictionary with all of its ancestors, which will include the test
|
||||
process. Additionally, `start_supervised/2` will also store the test process
|
||||
in the `$callers` key of the started process, allowing tools that perform
|
||||
either ancestor or caller tracking to reach the test process. You can learn
|
||||
more about these keys in
|
||||
[the `Task` module](`Task#module-ancestor-and-caller-tracking`).
|
||||
Finally, since Elixir v1.17.0, the test supervisor has both `$ancestors`
|
||||
and `$callers` key in its process dictionary pointing to the test process.
|
||||
This means developers can invoke `Process.get(:"$callers", [])` in their
|
||||
`start_link` function and forward it to the spawned process, which may set
|
||||
`Process.put(:"$callers", callers)` during its initialization. This may be
|
||||
useful in projects who track process ownership during tests. You can learn
|
||||
more about these keys in [the `Task` module](`Task#module-ancestor-and-caller-tracking`).
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@spec start_supervised(Supervisor.child_spec() | module | {module, term}, keyword) ::
|
||||
|
||||
@@ -298,7 +298,7 @@ defmodule Logger.Translator do
|
||||
client_info: client,
|
||||
name: name,
|
||||
reason: {kind, reason, stack},
|
||||
state: {state, data},
|
||||
state: state,
|
||||
queue: queue,
|
||||
postponed: postponed,
|
||||
callback_mode: callback_mode,
|
||||
@@ -328,8 +328,6 @@ defmodule Logger.Translator do
|
||||
msg,
|
||||
"\nState: ",
|
||||
inspect(state, inspect_opts),
|
||||
"\nData: ",
|
||||
inspect(data, inspect_opts),
|
||||
"\nCallback mode: ",
|
||||
"#{inspect(callback_mode, inspect_opts)}, state_enter: #{state_enter?}"
|
||||
| format_client_info(client)
|
||||
|
||||
@@ -100,6 +100,28 @@ defmodule Logger.TranslatorTest do
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MyGenStatemHandleEvent do
|
||||
@behaviour :gen_statem
|
||||
|
||||
@impl true
|
||||
def callback_mode, do: :handle_event_function
|
||||
|
||||
@impl true
|
||||
def init(state) do
|
||||
{:ok, :no_state, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event({:call, _}, :error, :no_state, _data) do
|
||||
raise "oops"
|
||||
end
|
||||
|
||||
@impl :gen_statem
|
||||
def format_status(_opts, [_pdict, _, state]) do
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MyBridge do
|
||||
@behaviour :supervisor_bridge
|
||||
|
||||
@@ -393,7 +415,7 @@ defmodule Logger.TranslatorTest do
|
||||
assert {%RuntimeError{message: "oops"}, [_ | _]} = process_metadata[:crash_reason]
|
||||
|
||||
refute Map.has_key?(gen_statem_metadata, :initial_call)
|
||||
assert process_metadata[:initial_call] == {Logger.TranslatorTest.MyGenStatem, :init, 1}
|
||||
assert process_metadata[:initial_call] == {MyGenStatem, :init, 1}
|
||||
|
||||
refute Map.has_key?(gen_statem_metadata, :registered_name)
|
||||
refute Map.has_key?(process_metadata, :registered_name)
|
||||
@@ -445,7 +467,7 @@ defmodule Logger.TranslatorTest do
|
||||
assert capture_log(:debug, fn ->
|
||||
catch_exit(:gen_statem.call(pid, :error))
|
||||
end) =~ """
|
||||
[:ok, :ok, :ok, ...]
|
||||
State: {:started, [:ok, ...]}
|
||||
"""
|
||||
after
|
||||
Application.put_env(:logger, :translator_inspect_opts, [])
|
||||
@@ -462,8 +484,7 @@ defmodule Logger.TranslatorTest do
|
||||
.*
|
||||
Queue: .*
|
||||
Postponed: \[\]
|
||||
State: :started
|
||||
Data: :ok
|
||||
State: {:started, :ok}
|
||||
Callback mode: :state_functions, state_enter: false
|
||||
Client #PID<\d+\.\d+\.\d+> is alive
|
||||
.*
|
||||
@@ -488,8 +509,7 @@ defmodule Logger.TranslatorTest do
|
||||
.*
|
||||
Queue: .*
|
||||
Postponed: \[\]
|
||||
State: :started
|
||||
Data: :ok
|
||||
State: {:started, :ok}
|
||||
Callback mode: :state_functions, state_enter: false
|
||||
Client :named_client is alive
|
||||
.*
|
||||
@@ -513,8 +533,7 @@ defmodule Logger.TranslatorTest do
|
||||
.*
|
||||
Queue: .*
|
||||
Postponed: \[\]
|
||||
State: :started
|
||||
Data: :ok
|
||||
State: {:started, :ok}
|
||||
Callback mode: :state_functions, state_enter: false
|
||||
Client #PID<\d+\.\d+\.\d+> is dead
|
||||
"""s
|
||||
@@ -533,8 +552,7 @@ defmodule Logger.TranslatorTest do
|
||||
.*
|
||||
Queue: .*
|
||||
Postponed: \[\]
|
||||
State: :started
|
||||
Data: :ok
|
||||
State: {:started, :ok}
|
||||
Callback mode: :state_functions, state_enter: false
|
||||
"""s
|
||||
end
|
||||
@@ -552,6 +570,34 @@ defmodule Logger.TranslatorTest do
|
||||
assert_receive {:event, {:string, ["Process " | _]}, _process_metadata}
|
||||
end
|
||||
|
||||
test "translates :gen_statem crashes when callback_mode is :handle_event_function" do
|
||||
{:ok, pid} = :gen_statem.start(MyGenStatemHandleEvent, :ok, [])
|
||||
|
||||
assert capture_log(:debug, fn ->
|
||||
catch_exit(:gen_statem.call(pid, :error))
|
||||
end) =~ ~r"""
|
||||
\[error\] :gen_statem #PID<\d+\.\d+\.\d+> terminating
|
||||
\*\* \(RuntimeError\) oops
|
||||
.*
|
||||
Queue: .*
|
||||
Postponed: \[\]
|
||||
State: :ok
|
||||
Callback mode: .*, state_enter: false
|
||||
"""s
|
||||
|
||||
assert_receive {:event, {:string, [[":gen_statem " <> _ | _] | _]}, gen_statem_metadata}
|
||||
assert_receive {:event, {:string, ["Process " | _]}, process_metadata}
|
||||
|
||||
assert {%RuntimeError{message: "oops"}, [_ | _]} = gen_statem_metadata[:crash_reason]
|
||||
assert {%RuntimeError{message: "oops"}, [_ | _]} = process_metadata[:crash_reason]
|
||||
|
||||
refute Map.has_key?(gen_statem_metadata, :initial_call)
|
||||
assert process_metadata[:initial_call] == {MyGenStatemHandleEvent, :init, 1}
|
||||
|
||||
refute Map.has_key?(gen_statem_metadata, :registered_name)
|
||||
refute Map.has_key?(process_metadata, :registered_name)
|
||||
end
|
||||
|
||||
test "translates Task crashes" do
|
||||
{:ok, pid} = Task.start_link(__MODULE__, :task, [self()])
|
||||
parent = self()
|
||||
|
||||
@@ -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.
|
||||
runtime_modules = Map.merge(modules, stale_modules)
|
||||
|
||||
# Now we do a simple pass finding anything that directly depends on the modules that
|
||||
# changed. We don't need to compute a fixpoint, because now only the directly affected
|
||||
# matter.
|
||||
{sources, runtime_modules} =
|
||||
Enum.reduce(sources, {sources, Map.keys(runtime_modules)}, fn
|
||||
{source_path, source_entry}, {acc_sources, acc_modules} ->
|
||||
source(export_references: export_refs, runtime_references: runtime_refs) =
|
||||
source_entry
|
||||
|
||||
if has_any_key?(runtime_modules, export_refs) or
|
||||
has_any_key?(runtime_modules, runtime_refs) do
|
||||
acc_sources =
|
||||
Map.replace!(acc_sources, source_path, source(source_entry, runtime_warnings: []))
|
||||
|
||||
new_modules =
|
||||
Enum.reject(source(source_entry, :modules), &Map.has_key?(runtime_modules, &1))
|
||||
|
||||
{acc_sources, new_modules ++ acc_modules}
|
||||
else
|
||||
{acc_sources, acc_modules}
|
||||
end
|
||||
end)
|
||||
|
||||
runtime_paths =
|
||||
Enum.map(runtime_modules, &{&1, Path.join(compile_path, Atom.to_string(&1) <> ".beam")})
|
||||
|
||||
+18
-16
@@ -9,7 +9,7 @@ defmodule Mix.SCM.Git do
|
||||
|
||||
@impl true
|
||||
def format(opts) do
|
||||
if rev = get_opts_rev(opts) do
|
||||
if rev = opts[:ref] || opts[:branch] || opts[:tag] do
|
||||
"#{redact_uri(opts[:git])} - #{rev}"
|
||||
else
|
||||
redact_uri(opts[:git])
|
||||
@@ -22,7 +22,7 @@ defmodule Mix.SCM.Git do
|
||||
{:git, _, lock_rev, lock_opts} ->
|
||||
lock = String.slice(lock_rev, 0, 7)
|
||||
|
||||
case Enum.find_value([:branch, :ref, :tag], &List.keyfind(lock_opts, &1, 0)) do
|
||||
case Enum.find_value([:ref, :branch, :tag], &List.keyfind(lock_opts, &1, 0)) do
|
||||
{:ref, _} -> lock <> " (ref)"
|
||||
{key, val} -> lock <> " (#{key}: #{val})"
|
||||
nil -> lock
|
||||
@@ -125,18 +125,18 @@ defmodule Mix.SCM.Git do
|
||||
sparse_toggle(opts)
|
||||
update_origin(opts[:git])
|
||||
|
||||
rev = get_lock_rev(opts[:lock], opts) || get_opts_rev(opts)
|
||||
|
||||
# Fetch external data
|
||||
branch_or_tag = opts[:branch] || opts[:tag]
|
||||
|
||||
["--git-dir=.git", "fetch", "--force", "--quiet"]
|
||||
|> Kernel.++(progress_switch(git_version()))
|
||||
|> Kernel.++(tags_switch(opts[:tag]))
|
||||
|> Kernel.++(depth_switch(opts[:depth]))
|
||||
|> Kernel.++(if rev, do: ["origin", rev], else: [])
|
||||
|> Kernel.++(if branch_or_tag, do: ["origin", branch_or_tag], else: [])
|
||||
|> git!()
|
||||
|
||||
# Migrate the Git repo
|
||||
rev = rev || default_branch()
|
||||
rev = get_lock_rev(opts[:lock], opts) || opts[:ref] || branch_or_tag || default_branch()
|
||||
git!(["--git-dir=.git", "checkout", "--quiet", rev])
|
||||
|
||||
if opts[:submodules] do
|
||||
@@ -250,18 +250,24 @@ defmodule Mix.SCM.Git do
|
||||
end
|
||||
|
||||
defp validate_depth(opts) do
|
||||
case Keyword.take(opts, [:depth]) do
|
||||
[] ->
|
||||
case Keyword.take(opts, [:depth, :ref]) do
|
||||
[_, _] ->
|
||||
Mix.raise(
|
||||
"Cannot specify :depth and :ref at the same time. " <>
|
||||
"Error on Git dependency: #{redact_uri(opts[:git])}"
|
||||
)
|
||||
|
||||
[depth: depth] when is_integer(depth) and depth > 0 ->
|
||||
opts
|
||||
|
||||
[{:depth, depth}] when is_integer(depth) and depth > 0 ->
|
||||
opts
|
||||
|
||||
invalid_depth ->
|
||||
[depth: invalid_depth] ->
|
||||
Mix.raise(
|
||||
"The depth must be a positive integer, and be specified only once, got: #{inspect(invalid_depth)}. " <>
|
||||
"Error on Git dependency: #{redact_uri(opts[:git])}"
|
||||
)
|
||||
|
||||
_ ->
|
||||
opts
|
||||
end
|
||||
end
|
||||
|
||||
@@ -290,10 +296,6 @@ defmodule Mix.SCM.Git do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_opts_rev(opts) do
|
||||
opts[:branch] || opts[:ref] || opts[:tag]
|
||||
end
|
||||
|
||||
defp redact_uri(git) do
|
||||
case URI.parse(git) do
|
||||
%{userinfo: nil} -> git
|
||||
|
||||
@@ -125,7 +125,8 @@ defmodule Mix.Tasks.Deps do
|
||||
* `:depth` *(since v1.17.0)* - creates a shallow clone of the Git repository,
|
||||
limiting the history to the specified number of commits. This can significantly
|
||||
improve clone speed for large repositories when full history is not needed.
|
||||
The value must be a positive integer, typically `1`.
|
||||
The value must be a positive integer, typically `1`. Cannot be used with the
|
||||
`:ref` option.
|
||||
|
||||
If your Git repository requires authentication, such as basic username:password
|
||||
HTTP authentication via URLs, it can be achieved via Git configuration, keeping
|
||||
|
||||
@@ -402,8 +402,8 @@ defmodule Mix.Tasks.Release do
|
||||
|
||||
If you are setting this option manually, we recommend the cookie option
|
||||
to be a long and randomly generated string, such as:
|
||||
`Base.url_encode64(:crypto.strong_rand_bytes(40))`. We also recommend to restrict
|
||||
the characters in the cookie to the subset returned by `Base.url_encode64/1`.
|
||||
`Base.encode32(:crypto.strong_rand_bytes(40))`. We also recommend restricting
|
||||
the characters in the cookie to only alphanumeric characters and underscore.
|
||||
|
||||
* `:validate_compile_env` - by default a release will match all runtime
|
||||
configuration against any configuration that was marked at compile time
|
||||
@@ -833,9 +833,9 @@ defmodule Mix.Tasks.Release do
|
||||
|
||||
* `RELEASE_DISTRIBUTION` - how do we want to run the distribution.
|
||||
May be `name` (long names), `sname` (short names) or `none`
|
||||
(distribution is not started automatically). Defaults to
|
||||
`sname` which allows access only within the current system.
|
||||
`name` allows external connections
|
||||
(distribution is not started automatically). Defaults to `sname`.
|
||||
When connecting nodes across hosts, you typically want to set
|
||||
this to `name` (required to use IPs as host names)
|
||||
|
||||
* `RELEASE_BOOT_SCRIPT` - the name of the boot script to use when starting
|
||||
the release. This script is used when running commands such as `start` and
|
||||
|
||||
@@ -53,7 +53,7 @@ defmodule Mix.Tasks.Release.Init do
|
||||
|
||||
## Enable deployment without epmd
|
||||
## (requires changing both vm.args and remote.vm.args)
|
||||
##-start_epmd false -erl_epmd_port 6789#{remote? && " -dist_listen false"}
|
||||
##-start_epmd false -erl_epmd_port 6789#{if(remote?, do: " -dist_listen false")}
|
||||
"""
|
||||
|
||||
@doc false
|
||||
@@ -136,7 +136,7 @@ defmodule Mix.Tasks.Release.Init do
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "ERROR: Expected sname, name, or none in RELEASE_DISTRIBUTION, got: $RELEASE_DISTRIBUTION" >&2
|
||||
echo "ERROR: Expected RELEASE_DISTRIBUTION to be sname, name, or none, got: $RELEASE_DISTRIBUTION" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -280,7 +280,7 @@ defmodule Mix.Tasks.Release.Init do
|
||||
rem set RELEASE_MODE=interactive
|
||||
|
||||
rem Set the release to work across nodes.
|
||||
rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none".
|
||||
rem RELEASE_DISTRIBUTION must be sname (local), name (distributed) or none.
|
||||
rem set RELEASE_DISTRIBUTION=name
|
||||
rem set RELEASE_NODE=<%= @release.name %>
|
||||
"""
|
||||
@@ -314,13 +314,33 @@ defmodule Mix.Tasks.Release.Init do
|
||||
if not defined RELEASE_BOOT_SCRIPT_CLEAN (set RELEASE_BOOT_SCRIPT_CLEAN=start_clean)
|
||||
if not defined RELEASE_SYS_CONFIG (set RELEASE_SYS_CONFIG=!REL_VSN_DIR!\sys)
|
||||
|
||||
if "!RELEASE_DISTRIBUTION!" == "none" (
|
||||
rem
|
||||
) else if "!RELEASE_DISTRIBUTION!" == "name" (
|
||||
rem
|
||||
) else if "!RELEASE_DISTRIBUTION!" == "sname" (
|
||||
rem
|
||||
) else (
|
||||
echo ERROR: Expected RELEASE_DISTRIBUTION to be sname, name, or none, got: !RELEASE_DISTRIBUTION!
|
||||
exit /B 1
|
||||
)
|
||||
|
||||
if "!RELEASE_MODE!" == "embedded" (
|
||||
rem
|
||||
) else if "!RELEASE_MODE!" == "interactive" (
|
||||
rem
|
||||
) else (
|
||||
echo ERROR: Expected RELEASE_MODE to be embedded or interactive, got: !RELEASE_MODE!
|
||||
exit /B 1
|
||||
)
|
||||
|
||||
if "%~1" == "start" (set "REL_EXEC=elixir" && set "REL_EXTRA=--no-halt" && set "REL_GOTO=start")
|
||||
if "%~1" == "start_iex" (set "REL_EXEC=iex" && set "REL_EXTRA=--werl" && set "REL_GOTO=start")
|
||||
if "%~1" == "install" (set "REL_GOTO=install")
|
||||
if "%~1" == "eval" (
|
||||
if "%~2" == "" (
|
||||
echo ERROR: EVAL expects an expression as argument
|
||||
goto end
|
||||
exit /B 1
|
||||
)
|
||||
set "REL_GOTO=eval"
|
||||
)
|
||||
@@ -348,7 +368,7 @@ defmodule Mix.Tasks.Release.Init do
|
||||
if "%~1" == "rpc" (
|
||||
if "%~2" == "" (
|
||||
echo ERROR: RPC expects an expression as argument
|
||||
goto end
|
||||
exit /B 1
|
||||
)
|
||||
set "REL_RPC=%~2"
|
||||
goto rpc
|
||||
@@ -369,7 +389,10 @@ defmodule Mix.Tasks.Release.Init do
|
||||
echo pid Prints the operating system PID of the running system via a remote command
|
||||
echo version Prints the release name and version to be booted
|
||||
echo.
|
||||
if not "%~1" == "" (echo ERROR: Unknown command %~1)
|
||||
if not "%~1" == "" (
|
||||
echo ERROR: Unknown command %~1
|
||||
exit /B 1
|
||||
)
|
||||
goto end
|
||||
|
||||
:start
|
||||
@@ -427,7 +450,7 @@ defmodule Mix.Tasks.Release.Init do
|
||||
if "!RELEASE_DISTRIBUTION!" == "none" (
|
||||
set RELEASE_DISTRIBUTION_FLAG=
|
||||
) else (
|
||||
set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "rem-!RANDOM!-!RELEASE_NODE!"
|
||||
set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "rpc-!RANDOM!-!RELEASE_NODE!"
|
||||
)
|
||||
|
||||
"!REL_VSN_DIR!\elixir.bat" ^
|
||||
@@ -452,7 +475,7 @@ defmodule Mix.Tasks.Release.Init do
|
||||
|
||||
if "!RELEASE_DISTRIBUTION!" == "none" (
|
||||
echo ERROR: RELEASE_DISTRIBUTION is required in install command
|
||||
goto end
|
||||
exit /B 1
|
||||
)
|
||||
|
||||
"!ERLSRV!" add "!RELEASE_NAME!_!RELEASE_NAME!" ^
|
||||
|
||||
@@ -1597,11 +1597,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
def foo(), do: B.foo()
|
||||
def bar(), do: B.bar()
|
||||
def __after_verify__(__MODULE__) do
|
||||
if Code.ensure_loaded?(B) and not function_exported?(B, :foo, 0) do
|
||||
:ok
|
||||
else
|
||||
IO.warn("AFTER_VERIFY", __ENV__)
|
||||
end
|
||||
IO.warn("AFTER_VERIFY", __ENV__)
|
||||
end
|
||||
end
|
||||
""")
|
||||
@@ -1630,10 +1626,9 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
end)
|
||||
|
||||
# Check B due to direct dependency on A
|
||||
# Check C due to transient dependency on A
|
||||
assert output =~ "A.foo/0 is undefined or private"
|
||||
assert output =~ "B.bar/0 is undefined or private"
|
||||
assert output =~ "AFTER_VERIFY"
|
||||
refute output =~ "B.bar/0 is undefined or private"
|
||||
refute output =~ "AFTER_VERIFY"
|
||||
|
||||
# Ensure only A was recompiled
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
@@ -1650,7 +1645,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
assert output =~ "B.bar/0 is undefined or private"
|
||||
assert output =~ "AFTER_VERIFY"
|
||||
|
||||
# Now we change B and it must no longer emit an AFTER_VERIFY warning
|
||||
# Now we change B and it must emit an AFTER_VERIFY warning
|
||||
File.write!("lib/b.ex", """
|
||||
defmodule B do
|
||||
end
|
||||
@@ -1663,7 +1658,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
|
||||
assert output =~ "B.foo/0 is undefined or private"
|
||||
assert output =~ "B.bar/0 is undefined or private"
|
||||
refute output =~ "AFTER_VERIFY"
|
||||
assert output =~ "AFTER_VERIFY"
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
@@ -530,43 +530,6 @@ defmodule Mix.Tasks.DepsGitTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "with ref" do
|
||||
[last, _ | _] = get_git_repo_revs("git_repo")
|
||||
|
||||
Process.put(:git_repo_opts, depth: 1, ref: last)
|
||||
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(GitApp)
|
||||
|
||||
Mix.Tasks.Deps.Get.run([])
|
||||
message = "* Getting git_repo (#{fixture_path("git_repo")} - #{last})"
|
||||
assert_received {:mix_shell, :info, [^message]}
|
||||
assert_shallow("deps/git_repo", 1)
|
||||
end)
|
||||
end
|
||||
|
||||
test "changing refspec updates retaining depth" do
|
||||
[last, first | _] = get_git_repo_revs("git_repo")
|
||||
|
||||
Process.put(:git_repo_opts, ref: first, depth: 1)
|
||||
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(GitApp)
|
||||
|
||||
Mix.Tasks.Deps.Get.run([])
|
||||
message = "* Getting git_repo (#{fixture_path("git_repo")} - #{first})"
|
||||
assert_received {:mix_shell, :info, [^message]}
|
||||
assert_shallow("deps/git_repo", 1)
|
||||
assert File.read!("mix.lock") =~ first
|
||||
|
||||
# Change refspec
|
||||
update_dep(ref: last, depth: 1)
|
||||
Mix.Tasks.Deps.Get.run([])
|
||||
assert_shallow("deps/git_repo", 1)
|
||||
assert File.read!("mix.lock") =~ last
|
||||
end)
|
||||
end
|
||||
|
||||
test "removing depth retains shallow repository" do
|
||||
# For compatibility and simplicity, we follow Git's behavior and do not
|
||||
# attempt to unshallow an existing repository. This should not be a
|
||||
|
||||
Reference in New Issue
Block a user