Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
915d6dca16 | ||
|
|
a983c8c043 | ||
|
|
d33fd8e413 | ||
|
|
02603b147c | ||
|
|
e126dab368 | ||
|
|
38547285f1 | ||
|
|
4f07670d08 | ||
|
|
dcff0ffaea | ||
|
|
97407cde90 | ||
|
|
579fc97b97 | ||
|
|
003ef335e7 | ||
|
|
028cdbb91e |
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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}\
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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}]},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
[]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user