Compare commits

...
12 Commits
Author SHA1 Message Date
José Valim 915d6dca16 Release v1.19.6 2026-08-28 12:07:13 +02:00
José Valim a983c8c043 Fix recursion on charlist error path 2026-08-28 12:05:38 +02:00
José Valim d33fd8e413 Release v1.19.5 2026-01-09 11:57:56 +01:00
José Valim 02603b147c Do not check the position in Regex errors, closes #15033 2026-01-01 10:54:31 +01:00
José Valim e126dab368 Optimize protocol consolidation
* Allow directory listings to be pre-computed
* Do not require struct modules to be loaded

The current implementation would require us to
recompute protocols whenever a struct changed,
something that is not currently available.
Furthermore, the fact we had to load modules
naturally made consolidation more expensive.
Therefore we need a lazy type resolution,
and until we have such feature, we will only
track struct names.
2025-12-19 18:42:49 +01:00
Łukasz Samson 38547285f1 Fix parser crash on missing parentheses on expression following operator \"not in\" warning (#15011) 2025-12-12 12:14:39 +01:00
Jechol Lee 4f07670d08 Fix unnecessary recompilation when dbg_callback is modified at runtime (#15007)
When :elixir app starts, store the dbg_callback value in dbg_callback_initial.
Mix compiler now compares against dbg_callback_initial instead of dbg_callback.

This prevents unnecessary recompilation when tools like Kino modify
dbg_callback at runtime. Previously, such runtime modifications would trigger
a full recompilation of all files using dbg/2, even though the config hadn't
actually changed.

The key insight is that dbg/2 is a compile-time macro, so runtime modifications
to dbg_callback don't affect already-compiled code. Only actual config changes
(reflected in dbg_callback_initial) should trigger recompilation.

This is a more general solution than detecting specific wrapping patterns,
as it works with any tool that modifies dbg_callback at runtime.
2025-12-10 09:21:49 +01:00
Andrea Leopardi dcff0ffaea Fix spec for t:ExUnit.Callbacks.child_spec_overrides/2 (#15006) 2025-12-09 08:14:48 -05:00
José Valim 97407cde90 Do not crash on invalid metadata in deprecated backends, closes #14995 2025-12-04 11:58:18 +01:00
Jean Klingler 579fc97b97 Revert Stream transform simplification causing regression (#14993)
* Revert "Simplify transform as suspend is guaranteed to return one entry"

This reverts commit a7c008f2cd.

* Add regression test case
2025-12-04 18:06:12 +09:00
José Valim 003ef335e7 Fix #iex:break as part of multi-line prompts, closes #14992 2025-12-04 00:10:07 +01:00
José Valim 028cdbb91e Support fetching abstract code for modules compiled with Elixir v1.14 and earlier, closes #14987 2025-12-03 20:00:37 +01:00
28 changed files with 352 additions and 124 deletions
+32
View File
@@ -234,6 +234,38 @@ These additions offer greater transparency into the components and licenses of e
This work was performed by [Jonatan Männchen](https://maennchen.dev) and sponsored by the [Erlang Ecosystem Foundation](https://erlef.org).
## v1.19.6 (2026-08-28)
### 1. Security
* [List] Avoid recursion when invalid charlists are given to `List.to_string/1` or `List.to_charlist/1` (CVE-2026-75758, GHSA-jf5q-v438-665c)
## v1.19.5 (2026-01-09)
### 1. Enhancements
#### Elixir
* [Protocol] Optimize protocol consolidation to no longer load structs
### 2. Bug fixes
#### Elixir
* [Kernel] Fix unnecessary recompilation when `dbg_callback` is modified at runtime
* [Kernel] Fix parser crash on missing parentheses on expression following operator `not in`
* [Kernel] Support fetching abstract code for modules compiled with Elixir v1.14 and earlier
* [Protocol] Ensure protocol consolidation no longer stores outdated struct types. As a consequence, protocols types only track struct names at the moment
* [Stream] Revert optimization which caused nested streams in `Stream.flat_map/2` to crash
#### IEx
* [IEx] Fix usage of `#iex:break` as part of multi-line prompts
#### Logger
* [Logger.Backends] Do not crash on invalid metadata
## v1.19.4 (2025-11-27)
### 1. Enhancements
+1 -1
View File
@@ -1 +1 @@
1.19.4
1.19.6
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.19.4
ELIXIR_VERSION=1.19.6
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
+1 -1
View File
@@ -4,7 +4,7 @@
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
set ELIXIR_VERSION=1.19.4
set ELIXIR_VERSION=1.19.6
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+25 -5
View File
@@ -389,13 +389,20 @@ defimpl Inspect, for: List do
close = color_doc("]", :list, opts)
cond do
lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
inspected =
case Identifier.escape(IO.chardata_to_string(term), ?", printable_limit) do
{escaped, ""} -> [?~, ?c, ?", escaped, ?"]
{escaped, _} -> [?~, ?c, ?", escaped, ?", " ++ ..."]
(lists == :as_charlists and unicode_list?(term, printable_limit)) or
(lists == :infer and List.ascii_printable?(term, printable_limit)) ->
{split, tail} =
if is_integer(printable_limit) do
case Enum.split(term, printable_limit) do
{split, []} -> {split, []}
{split, _} -> {split, " ++ ..."}
end
else
{term, []}
end
{escaped, _} = Identifier.escape(IO.chardata_to_string(split), ?")
inspected = [?~, ?c, ?", escaped, ?" | tail]
color_doc(IO.iodata_to_binary(inspected), :charlist, opts)
keyword?(term) ->
@@ -409,6 +416,19 @@ defimpl Inspect, for: List do
end
end
defp unicode_list?(_, 0), do: true
defp unicode_list?([char | rest], counter)
when char in 0..0xD7FF or char in 0xE000..0x10FFFF,
do: unicode_list?(rest, decrement(counter))
defp unicode_list?([], _counter), do: true
defp unicode_list?(_, _counter), do: false
@compile {:inline, decrement: 1}
defp decrement(:infinity), do: :infinity
defp decrement(counter), do: counter - 1
@doc false
def keyword({key, value}, opts) do
key = color_doc(Macro.inspect_atom(:key, key), :atom, opts)
+2 -2
View File
@@ -21,8 +21,8 @@ defmodule Inspect.Opts do
is `:decimal` and if it is printable, otherwise in bit syntax. See
`String.printable?/1` to learn when a string is printable.
* `:charlists` - when `:as_charlists` all lists will be printed as charlists,
non-printable elements will be escaped.
* `:charlists` - when `:as_charlists` all charlists will be printed as charlists,
non-printable code points will be escaped. Other lists will be printed as lists.
When `:as_lists` all lists will be printed as lists.
+2 -2
View File
@@ -1137,7 +1137,7 @@ defmodule List do
Please check the given list or call inspect/1 to get the list representation, got:
#{inspect(list)}
#{inspect(list, charlists: :as_lists)}
"""
else
result when is_binary(result) ->
@@ -1189,7 +1189,7 @@ defmodule List do
Please check the given list or call inspect/1 to get the list representation, got:
#{inspect(list)}
#{inspect(list, charlists: :as_lists)}
"""
else
result when is_list(result) ->
+11
View File
@@ -1067,6 +1067,17 @@ defmodule Module.Types.Apply do
"""
end
# The protocol has, at the moment, simplified clauses, so we build the complete one
clauses =
case mod.__protocol__(:impls) do
{:consolidated, mods} ->
domain = mods |> Enum.map(&Module.Types.Of.impl/1) |> Enum.reduce(&union/2)
[{[domain], dynamic()}]
_ ->
clauses
end
{"""
but expected a type that implements the #{inspect(mod)} protocol.
#{fix}\
+15 -4
View File
@@ -141,14 +141,25 @@ defmodule Module.Types.Of do
{Any, term()}
]
@doc """
Currently, for protocol implementations, we only store
the open struct definition. This is because we don't want
to reconsolidate whenever the struct changes, but at the
moment we can't store references either. Ideally struct
types on protocol dispatches would be lazily resolved.
"""
def impl(for, mode \\ :closed)
for {for, type} <- impls do
def impl(unquote(for)), do: unquote(Macro.escape(type))
def impl(unquote(for), _mode), do: unquote(Macro.escape(type))
end
def impl(struct) do
# Elixir did not strictly require the implementation to be available, so we need a fallback.
def impl(struct, mode) do
# Elixir did not strictly require the implementation to be available,
# so we need to deal with such cases accordingly.
# TODO: Assume implementation is available on Elixir v2.0.
if info = Code.ensure_loaded?(struct) && struct.__info__(:struct) do
# A warning is emitted since v1.19+.
if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do
struct_type(struct, info)
else
open_map(__struct__: atom([struct]))
+29 -16
View File
@@ -453,7 +453,7 @@ defmodule Protocol do
true
"""
@spec extract_protocols([charlist | String.t()]) :: [atom]
@spec extract_protocols([charlist | String.t() | {charlist, [charlist]}]) :: [atom]
def extract_protocols(paths) do
extract_matching_by_attribute(paths, [?E, ?l, ?i, ?x, ?i, ?r, ?.], fn module, attributes ->
case attributes[:__protocol__] do
@@ -482,7 +482,7 @@ defmodule Protocol do
true
"""
@spec extract_impls(module, [charlist | String.t()]) :: [atom]
@spec extract_impls(module, [charlist | String.t() | {charlist, [charlist]}]) :: [atom]
def extract_impls(protocol, paths) when is_atom(protocol) do
prefix = Atom.to_charlist(protocol) ++ [?.]
@@ -496,17 +496,25 @@ defmodule Protocol do
defp extract_matching_by_attribute(paths, prefix, callback) do
for path <- paths,
# Do not use protocols as they may be consolidating
path = if(is_list(path), do: path, else: String.to_charlist(path)),
file <- list_dir(path),
{path, files} = list_dir(path),
file <- files,
mod = extract_from_file(path, file, prefix, callback),
do: mod
end
# Do not use protocols as they may be consolidating
defp list_dir({path, files}) when is_list(path) and is_list(files) do
{path, files}
end
defp list_dir(path) when is_binary(path) do
list_dir(String.to_charlist(path))
end
defp list_dir(path) when is_list(path) do
case :file.list_dir(path) do
{:ok, files} -> files
_ -> []
{:ok, files} -> {path, files}
_ -> {path, []}
end
end
@@ -633,7 +641,7 @@ defmodule Protocol do
checker =
if checker do
update_in(checker.exports, fn exports ->
signatures = new_signatures(definitions, protocol_funs, protocol, types)
signatures = new_signatures(definitions, protocol_funs, protocol, types, structs)
for {fun, info} <- exports do
if sig = Map.get(signatures, fun) do
@@ -652,14 +660,13 @@ defmodule Protocol do
end
end
defp new_signatures(definitions, protocol_funs, protocol, types) do
defp new_signatures(definitions, protocol_funs, protocol, types, structs) do
alias Module.Types.Descr
types_minus_any = List.delete(types, Any)
clauses =
types
|> List.delete(Any)
|> Enum.map(fn impl ->
{[Module.Types.Of.impl(impl)], Descr.atom([__concat__(protocol, impl)])}
Enum.map(types_minus_any, fn impl ->
{[Module.Types.Of.impl(impl, :open)], Descr.atom([__concat__(protocol, impl)])}
end)
{domain, impl_for, impl_for!} =
@@ -674,10 +681,16 @@ defmodule Protocol do
end
_ ->
structs_domain =
case structs do
[] -> Descr.none()
_ -> Descr.open_map(__struct__: Descr.atom(structs))
end
domain =
clauses
|> Enum.map(fn {[domain], _} -> domain end)
|> Enum.reduce(&Descr.union/2)
Enum.reduce(types_minus_any -- structs, structs_domain, fn impl, acc ->
Descr.union(Module.Types.Of.impl(impl, :open), acc)
end)
not_domain = Descr.negation(domain)
+22 -22
View File
@@ -964,17 +964,13 @@ defmodule Stream do
after_fun.(user_acc)
:erlang.raise(kind, reason, __STACKTRACE__)
else
{:suspended, [val], next} ->
do_transform_user(val, user_acc, :cont, next, inner_acc, funs)
{:suspended, vals, next} ->
do_transform_user(:lists.reverse(vals), user_acc, :cont, next, inner_acc, funs)
{_, result} ->
{_, vals} ->
# Do not attempt to call the resource again, it has either done or halted
next = fn _ -> {:done, []} end
case result do
[val] -> do_transform_user(val, user_acc, :last, next, inner_acc, funs)
[] -> do_transform(user_acc, :last, next, inner_acc, funs)
end
do_transform_user(:lists.reverse(vals), user_acc, :last, next, inner_acc, funs)
end
end
@@ -989,7 +985,7 @@ defmodule Stream do
after_fun.(user_acc)
:erlang.raise(kind, reason, __STACKTRACE__)
else
result -> do_transform_result(result, :halt, next, inner_acc, funs)
result -> do_transform_result(result, [], :halt, next, inner_acc, funs)
end
else
do_transform(user_acc, :halt, next, inner_acc, funs)
@@ -1002,7 +998,11 @@ defmodule Stream do
{:halted, elem(inner_acc, 1)}
end
defp do_transform_user(val, user_acc, next_op, next, inner_acc, funs) do
defp do_transform_user([], user_acc, next_op, next, inner_acc, funs) do
do_transform(user_acc, next_op, next, inner_acc, funs)
end
defp do_transform_user([val | vals], user_acc, next_op, next, inner_acc, funs) do
{user, _, _, _, after_fun} = funs
try do
@@ -1013,20 +1013,20 @@ defmodule Stream do
after_fun.(user_acc)
:erlang.raise(kind, reason, __STACKTRACE__)
else
result -> do_transform_result(result, next_op, next, inner_acc, funs)
result -> do_transform_result(result, vals, next_op, next, inner_acc, funs)
end
end
defp do_transform_result(result, next_op, next, inner_acc, funs) do
defp do_transform_result(result, vals, next_op, next, inner_acc, funs) do
{_, fun, inner, _, after_fun} = funs
case result do
{[], user_acc} ->
do_transform(user_acc, next_op, next, inner_acc, funs)
do_transform_user(vals, user_acc, next_op, next, inner_acc, funs)
{list, user_acc} when is_list(list) ->
reduce = &Enumerable.List.reduce(list, &1, fun)
do_transform_inner_list(user_acc, next_op, next, inner_acc, reduce, funs)
do_transform_inner_list(vals, user_acc, next_op, next, inner_acc, reduce, funs)
{:halt, user_acc} ->
next.({:halt, []})
@@ -1035,11 +1035,11 @@ defmodule Stream do
{other, user_acc} ->
reduce = &Enumerable.reduce(other, &1, inner)
do_transform_inner_enum(user_acc, next_op, next, inner_acc, reduce, funs)
do_transform_inner_enum(vals, user_acc, next_op, next, inner_acc, reduce, funs)
end
end
defp do_transform_inner_list(user_acc, next_op, next, inner_acc, reduce, funs) do
defp do_transform_inner_list(vals, user_acc, next_op, next, inner_acc, reduce, funs) do
{_, _, _, _, after_fun} = funs
try do
@@ -1051,7 +1051,7 @@ defmodule Stream do
:erlang.raise(kind, reason, __STACKTRACE__)
else
{:done, acc} ->
do_transform(user_acc, next_op, next, {:cont, acc}, funs)
do_transform_user(vals, user_acc, next_op, next, {:cont, acc}, funs)
{:halted, acc} ->
next.({:halt, []})
@@ -1059,12 +1059,12 @@ defmodule Stream do
{:halted, acc}
{:suspended, acc, continuation} ->
resume = &do_transform_inner_list(user_acc, next_op, next, &1, continuation, funs)
resume = &do_transform_inner_list(vals, user_acc, next_op, next, &1, continuation, funs)
{:suspended, acc, resume}
end
end
defp do_transform_inner_enum(user_acc, next_op, next, {op, inner_acc}, reduce, funs) do
defp do_transform_inner_enum(vals, user_acc, next_op, next, {op, inner_acc}, reduce, funs) do
{_, _, _, _, after_fun} = funs
try do
@@ -1078,7 +1078,7 @@ defmodule Stream do
# The user wanted to cont/suspend but the stream halted,
# so we continue with the user intention.
{:halted, [inner_op | acc]} when op != :halt and inner_op != :halt ->
do_transform(user_acc, next_op, next, {inner_op, acc}, funs)
do_transform_user(vals, user_acc, next_op, next, {inner_op, acc}, funs)
{:halted, [_ | acc]} ->
next.({:halt, []})
@@ -1086,10 +1086,10 @@ defmodule Stream do
{:halted, acc}
{:done, [_ | acc]} ->
do_transform(user_acc, next_op, next, {:cont, acc}, funs)
do_transform_user(vals, user_acc, next_op, next, {:cont, acc}, funs)
{:suspended, [_ | acc], continuation} ->
resume = &do_transform_inner_enum(user_acc, next_op, next, &1, continuation, funs)
resume = &do_transform_inner_enum(vals, user_acc, next_op, next, &1, continuation, funs)
{:suspended, acc, resume}
end
end
+6
View File
@@ -64,6 +64,11 @@ start(_Type, _Args) ->
application:set_env(elixir, ansi_enabled, prim_tty:isatty(stdout) == true)
end,
%% Store the initial dbg_callback value before any runtime modifications.
%% This allows Mix compiler to detect config changes vs runtime changes
%% (e.g., Kino wrapping dbg_callback at runtime should not trigger recompilation).
{ok, InitialDbgCallback} = application:get_env(elixir, dbg_callback),
Tokenizer = case code:ensure_loaded('Elixir.String.Tokenizer') of
{module, Mod} -> Mod;
_ -> elixir_tokenizer
@@ -90,6 +95,7 @@ start(_Type, _Args) ->
{docs, true},
{ignore_already_consolidated, false},
{ignore_module_conflict, false},
{initial_dbg_callback, InitialDbgCallback},
{infer_signatures, [elixir]},
{on_undefined_variable, raise},
{parser_options, [{columns, true}]},
+5
View File
@@ -566,6 +566,11 @@ extract_bit_type({'-', _, [L, R]}, Acc) ->
extract_bit_type({unit, _, [Arg]}, Acc) ->
[{unit, Arg} | Acc];
extract_bit_type({Other, _, nil}, Acc) ->
[Other | Acc];
%% TODO: Remove me on Elixir v2.0.
%% Elixir v1.14 and earlier emitted an empty list
%% and may still be processed via debug_info.
extract_bit_type({Other, _, []}, Acc) ->
[Other | Acc].
%% Optimizations that are specific to Erlang and change
+2 -1
View File
@@ -27,8 +27,9 @@ each_clause({'catch', Meta, Raw, Expr}, S) ->
{Args, Guards} = elixir_utils:extract_splat_guards(Raw),
Match =
%% TODO: Remove me on Elixir v2.0.
%% Elixir v1.17 and earlier emitted single argument
%% and may still be processed via debug_info
%% and may still be processed via debug_info.
case Args of
[X] -> [throw, X];
[X, Y] -> [X, Y]
+8
View File
@@ -1298,6 +1298,14 @@ warn_pipe(_Token, _) ->
ok.
%% TODO: Make this an error on v2.0
warn_no_parens_after_do_op({{in_op, Location, Op, _InLocation}, _}) ->
{Line, _, _} = Location,
warn(
Line,
"missing parentheses on expression following operator \"" ++ atom_to_list(Op) ++ "\", "
"you must add parentheses to avoid ambiguities"
);
warn_no_parens_after_do_op({{_Type, Location, Op}, _}) ->
{Line, _, _} = Location,
+16 -1
View File
@@ -282,10 +282,15 @@ defmodule Inspect.ListTest do
test "printable limit" do
assert inspect(~c"hello world", printable_limit: 4) == ~s(~c"hell" ++ ...)
assert inspect(~c"hello", printable_limit: :infinity) == ~s(~c"hello")
# Non printable characters after the limit don't matter
assert inspect(~c"hello world" ++ [0], printable_limit: 4) == ~s(~c"hell" ++ ...)
# Non printable strings aren't affected by printable limit
assert inspect([0, 1, 2, 3, 4], printable_limit: 3) == ~s([0, 1, 2, 3, 4])
# Ensure a non-printable value after limit does not crash
assert inspect(~c"hello world" ++ [nil], printable_limit: 4) == ~s(~c"hell" ++ ...)
# Ensure a non-printable value after default limit does not hang
assert String.ends_with?(inspect(List.duplicate(?a, 5000) ++ [nil]), "\" ++ ...")
end
test "keyword" do
@@ -310,10 +315,20 @@ defmodule Inspect.ListTest do
assert inspect([0], charlists: :infer) == "[0]"
end
test "opt as strings" do
test "opt as charlists" do
assert inspect(~c"john" ++ [0] ++ ~c"doe", charlists: :as_charlists) == ~s(~c"john\\0doe")
assert inspect(~c"john", charlists: :as_charlists) == ~s(~c"john")
assert inspect([0], charlists: :as_charlists) == ~s(~c"\\0")
assert inspect([nil], charlists: :as_charlists) == "[nil]"
assert inspect([0xD800], charlists: :as_charlists) == "[55296]"
assert inspect([1_114_112], charlists: :as_charlists) == "[1114112]"
assert inspect([[?a]], charlists: :as_charlists) == ~s([~c"a"])
assert inspect([?a, ?b, ?c | nil], charlists: :as_charlists, printable_limit: 4) ==
"[97, 98, 99 | nil]"
assert inspect([?a, ?b, ?c | nil], charlists: :as_charlists, printable_limit: 3) ==
~s(~c"abc" ++ ...)
end
test "opt as lists" do
@@ -1953,6 +1953,18 @@ defmodule Kernel.WarningTest do
)
end
test "do+end with not in operator without explicit parentheses" do
assert_warn_eval(
["nofile:3\n", "missing parentheses on expression following operator \"not in\""],
"""
quote do
case do
end not in no_parens 1, 2
end
"""
)
end
test "variable is being expanded to function call (on_undefined_variable: warn)" do
capture_io(:stderr, fn ->
Code.put_compiler_option(:on_undefined_variable, :warn)
+12
View File
@@ -331,6 +331,12 @@ defmodule ListTest do
assert_raise ArgumentError, ~r"cannot convert the given list to a string", fn ->
List.to_string([:a, :b])
end
invalid = List.duplicate(?a, 4096) ++ [nil]
assert_raise ArgumentError, ~r/got:\n\n\[97, 97, .*\.\.\.\]\n\z/s, fn ->
List.to_string(invalid)
end
end
test "to_charlist/1" do
@@ -349,6 +355,12 @@ defmodule ListTest do
assert_raise ArgumentError, ~r"cannot convert the given list to a charlist", fn ->
List.to_charlist([:a, :b])
end
invalid = List.duplicate(?a, 4096) ++ [nil]
assert_raise ArgumentError, ~r/got:\n\n\[97, 97, .*\.\.\.\]\n\z/s, fn ->
List.to_charlist(invalid)
end
end
describe "myers_difference/2" do
@@ -187,9 +187,11 @@ defmodule Protocol.ConsolidationTest do
assert domain == [term()]
assert clauses == [
{[Of.impl(Map)], atom([WithAny.Map])},
{[Of.impl(ImplStruct)], atom([WithAny.Protocol.ConsolidationTest.ImplStruct])},
{[negation(union(Of.impl(ImplStruct), Of.impl(Map)))], atom([WithAny.Any])}
{[Of.impl(Map, :open)], atom([WithAny.Map])},
{[Of.impl(ImplStruct, :open)],
atom([WithAny.Protocol.ConsolidationTest.ImplStruct])},
{[negation(union(Of.impl(ImplStruct, :open), Of.impl(Map, :open)))],
atom([WithAny.Any])}
]
assert %{{:ok, 2} => %{sig: {:strong, nil, clauses}}} = exports
@@ -256,18 +258,34 @@ defmodule Protocol.ConsolidationTest do
assert Inspect in protos
end
test "protocols with expanded path" do
path = to_charlist(Application.app_dir(:elixir, "ebin"))
{:ok, mods} = :file.list_dir(path)
protos = Protocol.extract_protocols([{path, mods}])
assert Enumerable in protos
assert Inspect in protos
end
test "implementations with charlist path" do
protos =
impls =
Protocol.extract_impls(Enumerable, [to_charlist(Application.app_dir(:elixir, "ebin"))])
assert List in protos
assert Function in protos
assert List in impls
assert Function in impls
end
test "implementations with binary path" do
protos = Protocol.extract_impls(Enumerable, [Application.app_dir(:elixir, "ebin")])
assert List in protos
assert Function in protos
impls = Protocol.extract_impls(Enumerable, [Application.app_dir(:elixir, "ebin")])
assert List in impls
assert Function in impls
end
test "implementations with expanded path" do
path = to_charlist(Application.app_dir(:elixir, "ebin"))
{:ok, mods} = :file.list_dir(path)
impls = Protocol.extract_impls(Enumerable, [{path, mods}])
assert List in impls
assert Function in impls
end
end
end
+2 -1
View File
@@ -127,7 +127,8 @@ defmodule RegexTest do
test "compile!/1" do
assert %Regex{} = Regex.compile!("foo")
assert_raise Regex.CompileError, ~r/position 0$/, fn ->
# The exact position changed between Erlang/OTP 28.1 and 28.3
assert_raise Regex.CompileError, ~r/at position/, fn ->
Regex.compile!("*foo")
end
end
+18
View File
@@ -183,6 +183,24 @@ defmodule StreamTest do
assert Stream.chunk_while([1, 2, 3, 4, 5], [], chunk_fun, after_fun) |> Enum.at(0) == [1]
end
test "chunk_while/4 regression case with concat" do
result =
["WrongHeader\nJohn Doe", "skipped"]
|> Stream.take(1)
|> Stream.chunk_while(
"",
fn element, acc ->
{acc, elements} = String.split(acc <> element, "\n") |> List.pop_at(-1)
{:cont, elements, acc}
end,
&{:cont, [&1], []}
)
|> Stream.concat()
|> Enum.to_list()
assert result == ["WrongHeader", "John Doe"]
end
test "concat/1" do
stream = Stream.concat([1..3, [], [4, 5, 6], [], 7..9])
assert is_function(stream)
+1 -5
View File
@@ -169,11 +169,7 @@ defmodule ExUnit.Callbacks do
end
"""
@type child_spec_overrides :: [
restart: :permanent | :transient | :temporary,
shutdown: :brutal_kill | timeout(),
type: :worker | :supervisor
]
@type child_spec_overrides :: Supervisor.child_spec_overrides()
@doc false
def __register__(module) do
+41 -46
View File
@@ -71,61 +71,56 @@ defmodule IEx.Evaluator do
def parse(input, opts, []), do: parse(input, opts, {[], :other})
def parse(@break_trigger, opts, _parser_state) do
:elixir_errors.parse_error(
[line: opts[:line]],
opts[:file],
"incomplete expression",
"",
{~c"", Keyword.get(opts, :line, 1), Keyword.get(opts, :column, 1), 0}
)
end
def parse(input, opts, {buffer, last_op}) do
input = buffer ++ input
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
result =
with {:ok, tokens} <- :elixir.string_to_tokens(input, line, column, file, opts),
{:ok, adjusted_tokens, adjusted_op} <-
adjust_operator(tokens, line, column, file, opts, last_op),
{:ok, forms} <- :elixir.tokens_to_quoted(adjusted_tokens, file, opts) do
last_op =
case forms do
{:=, _, [_, _]} -> :match
_ -> :other
end
forms =
if adjusted_op != nil do
quote do
IEx.Evaluator.assert_no_error!()
unquote(forms)
if List.ends_with?(input, @break_trigger) do
triplet = {~c"", line, column, 0}
:elixir_errors.parse_error([line: line], file, "incomplete expression", "", triplet)
else
result =
with {:ok, tokens} <- :elixir.string_to_tokens(input, line, column, file, opts),
{:ok, adjusted_tokens, adjusted_op} <-
adjust_operator(tokens, line, column, file, opts, last_op),
{:ok, forms} <- :elixir.tokens_to_quoted(adjusted_tokens, file, opts) do
last_op =
case forms do
{:=, _, [_, _]} -> :match
_ -> :other
end
else
forms
end
{:ok, forms, last_op}
forms =
if adjusted_op != nil do
quote do
IEx.Evaluator.assert_no_error!()
unquote(forms)
end
else
forms
end
{:ok, forms, last_op}
end
case result do
{:ok, forms, last_op} ->
{:ok, forms, {[], last_op}}
{:error, {_, _, ""}} ->
{:incomplete, {input, last_op}}
{:error, {location, error, token}} ->
:elixir_errors.parse_error(
location,
file,
error,
token,
{input, line, column, 0}
)
end
case result do
{:ok, forms, last_op} ->
{:ok, forms, {[], last_op}}
{:error, {_, _, ""}} ->
{:incomplete, {input, last_op}}
{:error, {location, error, token}} ->
:elixir_errors.parse_error(
location,
file,
error,
token,
{input, line, column, 0}
)
end
end
+7 -1
View File
@@ -47,7 +47,13 @@ defmodule Logger.Backends.Handler do
%{truncate: truncate, utc_log: utc_log?} = config
level = erlang_level_to_elixir_level(erl_level)
message = Logger.Formatter.format_event(event, truncate)
timestamp = Map.get_lazy(metadata, :time, fn -> :os.system_time(:microsecond) end)
timestamp =
case metadata do
%{time: time} when is_integer(time) and time >= 0 -> time
_ -> :os.system_time(:microsecond)
end
date_time_ms = Logger.Formatter.system_time_to_date_time_ms(timestamp, utc_log?)
metadata = [erl_level: erl_level] ++ erlang_metadata_to_elixir_metadata(metadata)
event = {level, gl, {Logger, message, date_time_ms, metadata}}
@@ -113,6 +113,12 @@ defmodule Logger.Backends.HandlerTest do
Logger.configure_backend(Logger.Backends.Console, metadata: [])
end
test "ignores invalid Erlang metadata" do
assert capture_log(fn -> :logger.info("ok", %{time: "bad"}) end) =~ "ok"
after
Logger.Backends.Internal.configure(Logger.Backends.Console, metadata: [])
end
test "uses reporting callback with Elixir inspection" do
assert capture_log(fn ->
callback = fn %{hello: :world} -> {"~p~n", [:formatted]} end
+7 -1
View File
@@ -287,7 +287,13 @@ defmodule Mix.Compilers.Elixir do
end
defp deps_config_compile_env_apps(deps_config) do
if deps_config[:dbg] != Application.fetch_env!(:elixir, :dbg_callback) do
# Use initial_dbg_callback instead of dbg_callback to ignore runtime modifications.
# Tools like Kino modify dbg_callback at runtime to customize dbg/2 behavior,
# but this should not trigger recompilation since the config hasn't actually changed.
# initial_dbg_callback is set when :elixir app starts, before any runtime modifications.
initial_dbg = :elixir_config.get(:initial_dbg_callback)
if deps_config[:dbg] != initial_dbg do
[:elixir]
else
[]
+9 -4
View File
@@ -126,11 +126,16 @@ defmodule Mix.Compilers.Protocol do
end
defp consolidation_paths do
filter_otp(:code.get_path(), :code.lib_dir())
end
otp = :code.lib_dir()
defp filter_otp(paths, otp) do
Enum.filter(paths, &(not :lists.prefix(otp, &1)))
# Pre-list the paths in each directory for performance
for path <- :code.get_path(), not :lists.prefix(otp, path) do
{path,
case :file.list_dir(path) do
{:ok, files} -> files
_ -> []
end}
end
end
defp consolidate([], _paths, output, _opts) do
+32 -1
View File
@@ -278,9 +278,11 @@ defmodule Mix.Tasks.Compile.ElixirTest do
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
# Change the dbg_callback at runtime
# Simulate a config change by updating both dbg_callback and initial_dbg_callback.
# This represents the case where the user actually changed the config file.
File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time)
Application.put_env(:elixir, :dbg_callback, {__MODULE__, :dbg, []})
:elixir_config.put(:initial_dbg_callback, {__MODULE__, :dbg, []})
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
@@ -289,6 +291,35 @@ defmodule Mix.Tasks.Compile.ElixirTest do
end)
after
Application.put_env(:elixir, :dbg_callback, {Macro, :dbg, []})
:elixir_config.put(:initial_dbg_callback, {Macro, :dbg, []})
end
test "does not recompile when dbg_callback is modified at runtime but initial is unchanged" do
in_fixture("no_mixfile", fn ->
Mix.Project.push(MixTest.Case.Sample)
File.write!("lib/a.ex", """
defmodule A do
def a, do: dbg(:ok)
end
""")
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
# Simulate a tool like Kino modifying dbg_callback at runtime.
# Since initial_dbg_callback remains unchanged, this should NOT trigger recompilation.
original_dbg = Application.fetch_env!(:elixir, :dbg_callback)
:elixir_config.put(:initial_dbg_callback, original_dbg)
Application.put_env(:elixir, :dbg_callback, {SomeDebugTool, :dbg, [original_dbg]})
# Should NOT trigger recompilation since initial_dbg_callback is unchanged
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:noop, []}
refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
end)
after
Application.put_env(:elixir, :dbg_callback, {Macro, :dbg, []})
:elixir_config.put(:initial_dbg_callback, {Macro, :dbg, []})
end
test "recompiles files when config changes export dependencies" do