Compare commits

..
38 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
José Valim 794441a255 Release v1.19.4 2025-11-27 16:50:26 +01:00
Eksperimental 727106beb4 Fix typos (#14951) 2025-11-27 16:49:21 +01:00
Wojtek Mach 5e45871b5e mix help app:APP: compile current Mix project (if any) (#14958)
Prior to this patch, it was impossible to get help for the current app:

    nimble_csv$ mix help app:nimble_csv
    Application nimble_csv does not exist or is not loaded

It also helps with projects with depenendencies when they haven't been
compiled yet:

    req$ mix deps.get && mix help app:finch # mix deps.compile haven't been run yet
    Application finch does not exist or is not loaded

Now it works:

    nimble_csv$ mix help app:nimble_csv
    Compiling 1 file (.ex)
    Generated nimble_csv app
    # NimbleCSV

    NimbleCSV is a small and fast parsing and dumping library.

    # NimbleCSV.RFC4180

    A CSV parser that uses comma as separator and double-quotes as escape according
    to RFC4180.

    # NimbleCSV.Spreadsheet

    A parser with spreadsheet friendly settings.
2025-11-27 15:36:13 +01:00
José Valim 0db308cbff Clarify @on_load will be called early on releases, closes #14960 2025-11-27 15:26:18 +01:00
José Valim 35a7e3aa1f Fix typo 2025-11-26 19:53:12 +01:00
José Valim 7340597020 Clarify struct and record fields, closes #14963 2025-11-26 19:52:43 +01:00
José Valim 3b6acc33d2 Fix task supervisor name 2025-11-24 18:27:17 +01:00
José Valim 2ed048acc6 Include hex in the list of escript.build special cases (#14953) 2025-11-24 15:28:33 +01:00
José Valim bf0c203d52 Skip warning on :re.import/1 2025-11-21 14:04:19 -08:00
Łukasz Samson b074e6b159 Fix ex_unit formatter crash when find_diff returns nil (#14939) 2025-11-21 05:34:16 -08:00
Jean Klingler bfe7315fbb File.cp_r reports non-existing dest dir properly (#14929) 2025-11-20 21:43:53 +09:00
Jean Klingler 28f4fdc21b Fix dialyzer warning in Code.format_string!/2 (#14928)
* Fix dialyzer warning in Code.format_string!/2

Tentative fix for https://github.com/elixir-lang/elixir/issues/14927

* Add regression test for IO.inspect/2 opts
2025-11-20 19:41:01 +09:00
José Valim ee1dc0729f Ensure parallel matches in assert propagate type information, closes #14921 2025-11-18 15:20:17 -08:00
José Valim 32e724c87c Ensure logger formatter works with chardata, closes #14921 2025-11-18 14:50:50 -08:00
José Valim 7758084211 Add --min-cycle-label to mix xref graph 2025-11-15 06:36:31 -08:00
Jean Klingler 57d3098c2a Fix since in doc for E regex modifier (#14909) 2025-11-14 07:48:38 +09:00
José Valim 427d21e410 Release v1.19.3 2025-11-13 17:33:43 +01:00
José Valim 24a188a016 Share consulting logic between compile.app and release 2025-11-13 16:11:15 +01:00
Jean Klingler 457d52c89b Add 'E' modifier to Regex for :export option (#14907) 2025-11-13 16:11:15 +01:00
José Valim 6684dbd3f2 Simplify format stacktrace handling 2025-11-13 13:10:59 +01:00
Łukasz Samson ccb2a742ad Fix crash when format_kind_reason is executed with TestModule (#14901)
Closes #14900.
2025-11-13 13:10:58 +01:00
Jean Klingler 92094c9ef1 Write ascii printable charlists as such in .app file (#14906) 2025-11-13 18:42:54 +09:00
Wojtek Mach d4d5da2ccf mix help app:APP: Always try loading apps and support Erlang apps (#14894) 2025-11-08 12:23:04 +01:00
José Valim 666c082105 &Mod.fun/arity are valid app terms, closes #14891 2025-11-08 12:23:03 +01:00
José Valim d2c9d39efe Allow forcing specific compilers 2025-11-08 11:47:24 +01:00
Jean Klingler 97d16038fe Fix how UTF8 env is stored in .app file (#14890) 2025-11-04 16:21:08 +09:00
59 changed files with 974 additions and 305 deletions
+88
View File
@@ -234,6 +234,94 @@ 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
#### Mix
* [mix xref] Add `--min-cycle-label` to help projects adapt to the more precise `mix xref graph` reports in Elixir v1.19. In previous versions, Elixir would break a large compilation cycle into several smaller ones, and therefore developers would check for `--min-cycle-size` on CI. However, the issue is not the size of the cycle (it has no implication in the amount of compiled files), but how many compile-time dependencies (aka compile labels) in a cycle. The new option allows developers to filter on the label parameter
### 2. Bug fixes
#### Elixir
* [File] Ensure `File.cp_r/3` reports non-existing destination properly (instead of source)
#### ExUnit
* [ExUnit] Fix formatter crash when diffing takes too long
* [ExUnit] Ensure parallel matches in `assert` propagate type information
#### Logger
* [Logger] Fix regression where formatter would crash when given chardata (the crash would happen when logging non-ASCII characters)
#### Mix
* [mix help] Ensure `app:APP` works when the project or its dependencies were not yet compiled
* [mix escript.build] Ensure the `hex` application can be included in escripts
## v1.19.3 (2025-11-13)
### 1. Enhancements
#### Elixir
* [Kernel] Support /E modifier for regular expressions in config files
#### Mix
* [mix compile] Allow forcing specific compilers, such as `--force-elixir`, `--force-app`, etc
* [mix help app:APP] Support showing helps for apps in Elixir and Erlang standard libraries
### 2. Bug fixes
#### Elixir
* [IO] Fix dialyzer warning on `IO.inspect :label`
#### ExUnit
* [ExUnit.Case] Fix crash when formatting errors caused by a linked/trapped exit during `setup_all`
#### Mix
* [mix compile.app] Ensure functions in the format `&Mod.fun/arity` can be written to .app files
* [mix compile.app] Ensure strings with Unicode characters can be written to .app files
## v1.19.2 (2025-11-02)
### 1. Enhancements
+1 -1
View File
@@ -1 +1 @@
1.19.2
1.19.6
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.19.2
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.2
set ELIXIR_VERSION=1.19.6
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+1 -1
View File
@@ -1090,7 +1090,7 @@ defmodule Code do
@doc since: "1.6.0"
@spec format_string!(binary, [format_opt]) :: iodata
def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do
line_length = Keyword.get(opts, :line_length, 98)
{line_length, opts} = Keyword.pop(opts, :line_length, 98)
to_quoted_opts =
[
+7 -1
View File
@@ -1224,7 +1224,13 @@ defmodule File do
defp do_cp_r(src, dest, on_conflict, dereference?, acc) when is_list(acc) do
case :elixir_utils.read_link_type(src) do
{:ok, :regular} ->
do_cp_file(src, dest, on_conflict, acc)
case do_cp_file(src, dest, on_conflict, acc) do
# we don't have a way to make a distinction between a non-existing src
# or dest being a non-existing dir in the case of :enoent,
# but we already know that src exists here.
{:error, :enoent, _} -> {:error, :enoent, dest}
other -> other
end
{:ok, :symlink} ->
case :file.read_link(src) do
+26 -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)
@@ -569,6 +589,7 @@ defimpl Inspect, for: Regex do
defp translate_options([:firstline | t], acc), do: translate_options(t, [?f | acc])
defp translate_options([:ungreedy | t], acc), do: translate_options(t, [?U | acc])
defp translate_options([:multiline | t], acc), do: translate_options(t, [?m | acc])
defp translate_options([:export | t], acc), do: translate_options(t, [?E | acc])
defp translate_options([], acc), do: acc
defp translate_options(_t, _acc), do: :error
+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) ->
+3 -2
View File
@@ -959,8 +959,9 @@ defmodule Macro do
backwards compatibility purposes.
In future releases, Elixir may introduce truly required struct
fields, and therefore only one of required or default will be
present. Your code should prepare for such scenario accordingly.
fields, the required field will be removed and default will be
present only if the field is optional. Your code should prepare
for such scenario accordingly.
"""
@doc since: "1.18.0"
@spec struct_info!(module(), Macro.Env.t()) ::
+8 -17
View File
@@ -402,30 +402,21 @@ defmodule Module do
Accepts the function name (as an atom) of a function in the current module.
The function must have an arity of 0 (no arguments). If the function does
not return `:ok`, the loading of the module will be aborted.
For example:
not return `:ok`, the loading of the module will be aborted. Its primary
use case is to load [NIFs](https://www.erlang.org/doc/man/erl_nif):
defmodule MyModule do
@on_load :load_check
@on_load :load_external_code
def load_check do
if some_condition() do
:ok
else
:abort
end
end
def some_condition do
false
def load_external_code do
:erlang.load_nif(~c"path/to/extension.so_or_dll")
end
end
The function given to `on_load` should avoid calling functions from
other modules. If you must call functions in other modules and those
modules are defined within the same project, the called modules must
have the `@compile {:autoload, true}` annotation, so they are loaded
upfront (and not from within the `@on_load` callback).
other modules. This is because, when running a `mix release`,
`on_load` runs extremely early, before any application starts running,
and therefore even systems like the `Logger` and `IO` are not yet available.
### `@vsn`
+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)
+3 -2
View File
@@ -27,8 +27,9 @@ defmodule Record do
## Types
Types can be defined for tuples with the `record/2` macro (only available in
typespecs). This macro will expand to a tuple as seen in the example below:
Types can be defined for tuples with the `record/2` construct (which is only
available in typespecs), with the record name as an atom and a keyword list
of fields and their types as argument:
defmodule MyModule do
require Record
+32 -5
View File
@@ -101,6 +101,13 @@ defmodule Regex do
* `:ungreedy` (U) - inverts the "greediness" of the regexp
(the previous `r` option is deprecated in favor of `U`)
* `:export` (E) (since Elixir 1.19.3) - uses an exported pattern
which can be shared across nodes or through config, at the cost of a runtime
overhead every time to re-import it every time it is executed.
This modifier only has an effect starting on Erlang/OTP 28, and it is ignored
on older versions (i.e. `~r/foo/E == ~r/foo/`). This is because patterns cannot
and do not need to be exported in order to be shared in these versions.
## Captures
Many functions in this module handle what to capture in a regex
@@ -251,7 +258,7 @@ defmodule Regex do
This checks the version stored in the regular expression
and recompiles the regex in case of version mismatch.
"""
# Remove me on Elixir v1.22
# TODO: Deprecate on Elixir v1.22
@doc deprecated: "It can be removed and it has no effect"
@doc since: "1.4.0"
def recompile(%Regex{} = regex) do
@@ -261,7 +268,7 @@ defmodule Regex do
@doc """
Recompiles the existing regular expression and raises `Regex.CompileError` in case of errors.
"""
# Remove me on Elixir v1.22
# TODO: Deprecate on Elixir v1.22
@doc deprecated: "It can be removed and it has no effect"
@doc since: "1.4.0"
def recompile!(regex) do
@@ -271,7 +278,7 @@ defmodule Regex do
@doc """
Returns the version of the underlying Regex engine.
"""
# Remove me on Elixir v1.22
# TODO: Deprecate on Elixir v1.22
@doc deprecated: "Use :re.version() instead"
@doc since: "1.4.0"
def version do
@@ -515,7 +522,7 @@ defmodule Regex do
"""
@spec names(t) :: [String.t()]
def names(%Regex{re_pattern: re_pattern}) do
{:namelist, names} = :re.inspect(re_pattern, :namelist)
{:namelist, names} = :re.inspect(maybe_import_pattern(re_pattern), :namelist)
names
end
@@ -585,10 +592,17 @@ defmodule Regex do
%Regex{source: source, opts: compile_opts} = regex
:re.run(string, source, compile_opts ++ options)
else
_ -> :re.run(string, re_pattern, options)
_ -> :re.run(string, maybe_import_pattern(re_pattern), options)
end
end
@compile {:inline, maybe_import_pattern: 1}
@compile {:no_warn_undefined, {:re, :import, 1}}
defp maybe_import_pattern({:re_exported_pattern, _, _, _, _} = exported),
do: :re.import(exported)
defp maybe_import_pattern(pattern), do: pattern
@typedoc """
Options for regex functions that capture matches.
"""
@@ -1007,6 +1021,16 @@ defmodule Regex do
translate_options(t, [:ungreedy | acc])
end
defp translate_options(<<?E, t::binary>>, acc) do
# on OTP 27-, the E modifier is a no-op since the feature doesn't exist but isn't needed
# (regexes aren't using references and can be shared across nodes or stored in config)
# TODO: remove this check on Erlang/OTP 28+ and update docs
case Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) do
true -> translate_options(t, [:export | acc])
false -> translate_options(t, acc)
end
end
defp translate_options(<<>>, acc), do: acc
defp translate_options(t, _acc), do: {:error, t}
@@ -1022,6 +1046,9 @@ defmodule Regex do
:erlang.system_info(:otp_release) < [?2, ?8] ->
Macro.escape(regex.re_pattern)
:lists.member(:export, regex.opts) ->
Macro.escape(regex.re_pattern)
# OTP 28.1+ introduced the ability to export and import regexes from compiled binaries
Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) ->
{:ok, exported} = :re.compile(regex.source, [:export] ++ regex.opts)
+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
@@ -228,7 +228,7 @@ Now we need to change `loop_acceptor/1` to use `Task.Supervisor` to serve each r
```elixir
defp loop_acceptor(socket) do
{:ok, client} = :gen_tcp.accept(socket)
{:ok, pid} = Task.Supervisor.start_child(KV.BucketSupervisor, fn -> serve(client) end)
{:ok, pid} = Task.Supervisor.start_child(KV.ServerSupervisor, fn -> serve(client) end)
:ok = :gen_tcp.controlling_process(client, pid)
loop_acceptor(socket)
end
+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,
+12
View File
@@ -787,6 +787,18 @@ defmodule FileTest do
end
end
test "cp_r! with file src and dest unknown" do
src = fixture_path("cp_r/a/1.txt")
dest = tmp_path("tmp/unknown/")
message =
"could not copy recursively from #{inspect(src)} to #{inspect(dest)}. #{dest}: no such file or directory"
assert_raise File.CopyError, message, fn ->
File.cp_r!(src, dest)
end
end
test "cp preserves mode" do
File.mkdir_p!(tmp_path("tmp"))
src = fixture_path("cp_mode")
@@ -0,0 +1,14 @@
defmodule Dialyzer.Regressions do
def io_inspect_opts do
IO.inspect(123, label: "foo", limit: :infinity)
end
def format_opts do
Code.format_string!("",
line_length: 120,
force_do_end_blocks: true,
locals_without_parens: true,
migrate: true
)
end
end
+21 -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
@@ -906,6 +921,11 @@ defmodule Inspect.OthersTest do
assert inspect(Regex.compile!("foo", [:ucp])) == ~S'Regex.compile!("foo", [:ucp])'
end
@tag :re_import
test "exported regex" do
assert inspect(~r/foo/E) == "~r/foo/E"
end
test "inspect_fun" do
fun = fn
integer, _opts when is_integer(integer) ->
@@ -185,6 +185,11 @@ defmodule Kernel.DialyzerTest do
assert_dialyze_no_warnings!(context)
end
test "no warning in various non-regression cases", context do
copy_beam!(context, Dialyzer.Regressions)
assert_dialyze_no_warnings!(context)
end
defp copy_beam!(context, module) do
name = "#{module}.beam"
File.cp!(Path.join(context.base_dir, name), Path.join(context.outdir, name))
@@ -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
+16 -1
View File
@@ -52,6 +52,20 @@ defmodule RegexTest do
assert Regex.match?(~r/^b$/m, "a\nb\nc")
end
@tag :re_import
test "export" do
# exported patterns have no structs, so these are structurally equal
assert ~r/foo/E == Regex.compile!("foo", [:export])
assert Regex.match?(~r/foo/E, "foo")
refute Regex.match?(~r/foo/E, "Foo")
assert Regex.run(~r/c(d)/E, "abcd") == ["cd", "d"]
assert Regex.run(~r/e/E, "abcd") == nil
assert Regex.names(~r/(?<FOO>foo)/E) == ["FOO"]
end
test "precedence" do
assert {"aa", :unknown} |> elem(0) =~ ~r/(a)\1/
end
@@ -113,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)
+9 -2
View File
@@ -160,6 +160,7 @@ defmodule ExUnit.Assertions do
end
end
{left, right} = move_match(left, right)
__match__(left, right, code, check, __CALLER__)
end
@@ -380,6 +381,12 @@ defmodule ExUnit.Assertions do
{ExUnit.AssertionError.no_value(), expr}
end
defp move_match(left, {:=, meta, [middle, right]}),
do: move_match({:=, meta, [left, middle]}, right)
defp move_match(left, right),
do: {left, right}
@doc false
def __match__({:when, _, _} = left, right, _, _, _) do
suggestion =
@@ -409,7 +416,7 @@ defmodule ExUnit.Assertions do
case right do
unquote(left) ->
unquote(check)
unquote(mark_as_generated(vars))
{unquote_splicing(mark_as_generated(vars))}
_ ->
left = unquote(Macro.escape(left))
@@ -427,7 +434,7 @@ defmodule ExUnit.Assertions do
quote do
right = unquote(right)
expr = unquote(code)
unquote(vars) = unquote(match_expr)
{unquote_splicing(vars)} = unquote(match_expr)
right
end
end
+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
+7
View File
@@ -964,7 +964,14 @@ defmodule ExUnit.DocTest do
@doc false
defmacro __assert__({:=, _, [left, right]} = assertion) do
{left, right} = move_match(left, right)
code = Macro.escape(assertion, prune_metadata: true)
ExUnit.Assertions.__match__(left, right, code, :ok, __CALLER__)
end
defp move_match(left, {:=, meta, [middle, right]}),
do: move_match({:=, meta, [left, middle]}, right)
defp move_match(left, right),
do: {left, right}
end
+22 -10
View File
@@ -338,7 +338,8 @@ defmodule ExUnit.Formatter do
{formatted_reason, wrapped_stack} =
format_exception(test, struct, wrapped_stack, width, formatter, @counter_padding)
formatted_stack = format_stacktrace(wrapped_stack, test.module, test.name, formatter)
formatted_stack = format_stacktrace(wrapped_stack, test, formatter)
{error_info(header, formatter) <> pad(formatted_reason <> formatted_stack), stack}
:error ->
@@ -512,10 +513,15 @@ defmodule ExUnit.Formatter do
defp format_diff(left, right, context, formatter) do
if has_value?(left) and has_value?(right) do
{result, env} = find_diff(left, right, context)
result = if formatter.(:diff_enabled?, false), do: result
hints = Enum.map(env.hints, &{:hint, format_hint(&1)})
{result, hints}
case find_diff(left, right, context) do
{result, env} ->
result = if formatter.(:diff_enabled?, false), do: result
hints = Enum.map(env.hints, &{:hint, format_hint(&1)})
{result, hints}
nil ->
{nil, []}
end
else
{nil, []}
end
@@ -672,22 +678,28 @@ defmodule ExUnit.Formatter do
|> Algebra.format(width)
end
defp format_stacktrace([], _case, _test, _color) do
defp format_stacktrace(stack, %{module: module, name: name}, color),
do: format_stacktrace(stack, module, name, color)
defp format_stacktrace(stack, %{name: name}, color),
do: format_stacktrace(stack, name, nil, color)
defp format_stacktrace([], _module, _test, _color) do
""
end
defp format_stacktrace(stacktrace, test_case, test, color) do
defp format_stacktrace(stacktrace, module, test, color) do
extra_info("stacktrace:", color) <>
Enum.map_join(stacktrace, fn entry ->
stacktrace_info(format_stacktrace_entry(entry, test_case, test), color)
stacktrace_info(format_stacktrace_entry(entry, module, test), color)
end)
end
defp format_stacktrace_entry({test_case, test, _, location}, test_case, test) do
defp format_stacktrace_entry({module, test, _, location}, module, test) do
format_file_line(location[:file], location[:line], " (test)")
end
defp format_stacktrace_entry(entry, _test_case, _test) do
defp format_stacktrace_entry(entry, _module, _test) do
format_stacktrace_entry(entry)
end
@@ -246,6 +246,16 @@ defmodule ExUnit.AssertionsTest do
end
end
test "assert parallel match" do
assert %URI{} = uri = apply(URI, :parse, ["/foo"])
# This should not warn
assert %URI{uri | path: "/bar"}.path == "/bar"
assert uri = %URI{} = apply(URI, :parse, ["/foo"])
# This should not warn
assert %URI{uri | path: "/bar"}.path == "/bar"
end
test "assert match with pinned variable" do
a = 1
{2, 1} = assert {2, ^a} = Value.tuple()
@@ -465,6 +465,21 @@ defmodule ExUnit.FormatterTest do
"""
end
test "formats setup_all exit stacktraces using module context" do
stacktrace = [
{Hello, :setup_all, [], [file: __ENV__.file, line: 123]},
{Hello, :some_fun, [], [file: __ENV__.file, line: 456]}
]
failure = [{{:EXIT, self()}, {:function_clause, stacktrace}, []}]
formatted =
format_test_all_failure(test_module(), failure, 1, 80, &formatter/2)
assert formatted =~ "Hello.setup_all/0"
assert formatted =~ "Hello.some_fun()"
end
test "formats assertions with operators with no limit" do
failure = [{:error, catch_assertion(assert [1, 2, 3] == [4, 5, 6]), []}]
+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}}
+26 -14
View File
@@ -244,26 +244,38 @@ defmodule Logger.Formatter do
case if(is_function(enabled, 0), do: enabled.(), else: enabled) do
true ->
color = md[:ansi_color] || Map.fetch!(colors, level)
fragment = IO.ANSI.format_fragment(color, true)
data = IO.iodata_to_binary(data)
size = byte_size(data)
cond do
:binary.at(data, size - 2) == ?\r and :binary.at(data, size - 1) == ?\n ->
[fragment, binary_part(data, 0, size - 2), IO.ANSI.reset() | "\r\n"]
:binary.at(data, size - 1) == ?\n ->
[fragment, binary_part(data, 0, size - 1), IO.ANSI.reset(), ?\n]
true ->
[fragment, data | IO.ANSI.reset()]
end
[IO.ANSI.format_fragment(color, true), add_reset(data)]
false ->
data
end
end
defp add_reset(binary) when is_binary(binary) do
size = byte_size(binary)
cond do
binary == "" ->
IO.ANSI.reset()
:binary.at(binary, size - 1) == ?\n ->
if size > 1 and :binary.at(binary, size - 2) == ?\r do
[binary_part(binary, 0, size - 2), IO.ANSI.reset() | "\r\n"]
else
[binary_part(binary, 0, size - 1), IO.ANSI.reset(), ?\n]
end
true ->
[binary | IO.ANSI.reset()]
end
end
defp add_reset([?\r, ?\n]), do: [IO.ANSI.reset(), ?\r, ?\n]
defp add_reset([?\n]), do: [IO.ANSI.reset(), ?\n]
defp add_reset([last]), do: add_reset(last)
defp add_reset([h | t]), do: [h | add_reset(t)]
defp add_reset(rest), do: [rest | IO.ANSI.reset()]
@doc """
Formats the message of a log event.
"""
@@ -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
+20
View File
@@ -105,6 +105,14 @@ defmodule Logger.FormatterTest do
colors: [enabled: true]
)
assert %{level: :warning, msg: {:string, ""}, meta: %{}}
|> format(formatter)
|> IO.chardata_to_string() == "\e[33m\e[0m"
assert %{level: :warning, msg: {:string, "s"}, meta: %{}}
|> format(formatter)
|> IO.chardata_to_string() == "\e[33ms\e[0m"
assert %{level: :warning, msg: {:string, "message"}, meta: %{}}
|> format(formatter)
|> IO.chardata_to_string() == "\e[33mmessage\e[0m"
@@ -116,6 +124,18 @@ defmodule Logger.FormatterTest do
assert %{level: :warning, msg: {:string, "message\r\n"}, meta: %{}}
|> format(formatter)
|> IO.chardata_to_string() == "\e[33mmessage\e[0m\r\n"
assert %{level: :warning, msg: {:string, [?é, ["message", [?\n]]]}, meta: %{}}
|> format(formatter)
|> IO.chardata_to_string() == "\e[33mémessage\e[0m\n"
assert %{level: :warning, msg: {:string, [?é, "message", ?\r, ?\n]}, meta: %{}}
|> format(formatter)
|> IO.chardata_to_string() == "\e[33mémessage\e[0m\r\n"
assert %{level: :warning, msg: {:string, [?é, "message" | "\r\n"]}, meta: %{}}
|> format(formatter)
|> IO.chardata_to_string() == "\e[33mémessage\e[0m\r\n"
end
end
+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
+35 -38
View File
@@ -468,51 +468,48 @@ defmodule Mix.Release do
{sys_config, runtime_config?} =
merge_provider_config(release, sys_config, config_provider_path)
path = Path.join(release.version_path, "sys.config")
args = [runtime_config?, sys_config]
format = "%% coding: utf-8~n%% RUNTIME_CONFIG=~s~n~tw.~n"
File.mkdir_p!(Path.dirname(path))
File.write!(path, IO.chardata_to_string(:io_lib.format(format, args)))
case :file.consult(path) do
{:ok, _} ->
case consultable_sys_config(sys_config) do
{:ok, contents} ->
path = Path.join(release.version_path, "sys.config")
args = [runtime_config?, contents]
format = "%% coding: utf-8~n%% RUNTIME_CONFIG=~s~n~ts.~n"
File.mkdir_p!(Path.dirname(path))
File.write!(path, IO.chardata_to_string(:io_lib.format(format, args)))
:ok
{:error, reason} ->
invalid =
for {app, kv} <- sys_config,
{key, value} <- kv,
not valid_config?(value),
do: """
Application: #{inspect(app)}
Key: #{inspect(key)}
Value: #{inspect(value)}
"""
message =
case invalid do
[] ->
"Could not read configuration file. Reason: #{inspect(reason)}"
_ ->
"Could not read configuration file. It has invalid configuration terms " <>
"such as functions, references, and pids. Please make sure your configuration " <>
"is made of numbers, atoms, strings, maps, tuples and lists. The following entries " <>
"are wrong:\n#{Enum.join(invalid)}"
end
{:error, message} ->
{:error, message}
end
end
defp valid_config?(m) when is_map(m),
do: Enum.all?(Map.delete(m, :__struct__), &valid_config?/1)
defp consultable_sys_config(sys_config) do
contents =
Enum.map_intersperse(sys_config, ?,, fn {app, kv} ->
kv =
Enum.map_intersperse(kv, ?,, fn {key, value} ->
case Mix.Utils.consultable(value) do
{:ok, value} -> [?{, :io_lib.print(key), ?,, value, ?}]
{:error, term, reason} -> throw({:error, app, key, term, reason})
end
end)
defp valid_config?(l) when is_list(l), do: Enum.all?(l, &valid_config?/1)
defp valid_config?(t) when is_tuple(t), do: Enum.all?(Tuple.to_list(t), &valid_config?/1)
defp valid_config?(o), do: is_number(o) or is_atom(o) or is_binary(o)
[?{, :io_lib.print(app), ?,, ?[, kv, ?], ?}]
end)
{:ok, [?[, contents, ?]]}
catch
{:error, app, key, value, reason} ->
message = """
Could not write configuration file because it has invalid terms
Application: #{inspect(app)}
Key: #{inspect(key)}
Invalid value: #{inspect(value)}
Reason: #{reason}
"""
{:error, message}
end
defp merge_provider_config(%{config_providers: []}, sys_config, _), do: {sys_config, false}
+15 -1
View File
@@ -295,10 +295,24 @@ defmodule Mix.Task.Compiler do
end
defp run_compiler(compiler, args) do
result = normalize(Mix.Task.run("compile.#{compiler}", args), compiler)
args_maybe_force = maybe_prepend_force(args, compiler)
result = normalize(Mix.Task.run("compile.#{compiler}", args_maybe_force), compiler)
Enum.reduce(Mix.ProjectStack.pop_after_compiler(compiler), result, & &1.(&2))
end
defp maybe_prepend_force(args, compiler) do
args
|> Enum.any?(fn
"--force-" <> rest -> rest == compiler |> to_string() |> String.replace("_", "-")
_ -> false
end)
|> if do
["--force" | args]
else
args
end
end
# Normalize the compiler result to a diagnostic tuple
defp normalize(result, name) do
case result do
+1 -1
View File
@@ -26,7 +26,7 @@ defmodule Mix.Tasks.Cmd do
Your shell will expand "lib/*" and then pass multiple arguments to
`mix cmd`, which in turn passes them to `echo`. Note that, `mix cmd`
by itself, does not perform any shell expansion. This means that,
if you invoke `mix cmd` programatically, as in:
if you invoke `mix cmd` programmatically, as in:
Mix.Task.run("cmd", ["echo", "lib/*"])
+12 -49
View File
@@ -187,12 +187,22 @@ defmodule Mix.Tasks.Compile.App do
|> add_compile_env(current_properties)
|> add_modules(modules, compile_path)
contents = to_erl_term({:application, app, properties})
contents =
case Mix.Utils.consultable({:application, app, properties}) do
{:ok, contents} ->
contents
{:error, term, reason} ->
Mix.raise(
"\"def application\" has a term which cannot be written to .app files: #{inspect(term)} (#{reason})"
)
end
:application.unload(app)
:application.load({:application, app, properties})
Mix.Project.ensure_structure()
File.write!(target, [contents, ?.])
File.write!(target, IO.chardata_to_string([contents, ?.]))
File.touch!(target, new_mtime)
# If we just created the .app file, it will have touched
@@ -209,53 +219,6 @@ defmodule Mix.Tasks.Compile.App do
end
end
defp to_erl_term(tuple) when is_tuple(tuple) do
[?{, tuple |> Tuple.to_list() |> to_erl_head(), ?}]
end
defp to_erl_term(list) when is_list(list) do
[?[, to_erl_head(list), ?]]
end
defp to_erl_term(map) when is_map(map) do
inner =
Enum.map_intersperse(
:maps.to_list(:maps.iterator(map, :reversed)),
?,,
fn {key, value} -> [to_erl_term(key), "=>", to_erl_term(value)] end
)
[?#, ?{, inner, ?}]
end
defp to_erl_term(map) when is_map(map) do
inner =
Enum.map_intersperse(
:maps.to_list(:maps.iterator(map, :reversed)),
?,,
fn {key, value} -> [to_erl_term(key), "=>", to_erl_term(value)] end
)
[?#, ?{, inner, ?}]
end
defp to_erl_term(term) when is_function(term) or is_reference(term) or is_pid(term) do
Mix.raise(
"\"def application\" has a term which cannot be written to .app files: #{inspect(term)}"
)
end
defp to_erl_term(term) do
:io_lib.print(term)
end
defp to_erl_head([]), do: []
defp to_erl_head([h | t]), do: [to_erl_term(h) | to_erl_tail(t)]
defp to_erl_tail([h | t]), do: [?,, to_erl_term(h) | to_erl_tail(t)]
defp to_erl_tail([]), do: []
defp to_erl_tail(other), do: [?|, to_erl_term(other)]
defp current_app_properties(target) do
case :file.consult(target) do
{:ok, [{:application, _app, properties}]} -> properties
+4 -2
View File
@@ -7,7 +7,7 @@ defmodule Mix.Tasks.Compile do
@shortdoc "Compiles source files"
@moduledoc """
@moduledoc ~S"""
The main entry point to compile source files.
It simply runs the compilers registered in your project and returns
@@ -59,7 +59,9 @@ defmodule Mix.Tasks.Compile do
* `--all-warnings` (`--no-all-warnings`) - prints all warnings, including previous compilations
(default is true except on errors)
* `--erl-config` - path to an Erlang term file that will be loaded as Mix config
* `--force` - forces compilation
* `--force` - forces compilation. You can also specify `--force-#{compiler}` for each compiler
invoked to force specifically that compiler to run (it requires the compiler to respect the
`--force` option, which is advised)
* `--list` - lists all enabled compilers
* `--listeners` - starts Mix listeners (they are started by default,
unless `--no-listeners` or `--no-deps-check` are given)
+2 -1
View File
@@ -271,7 +271,8 @@ defmodule Mix.Tasks.Escript.Build do
[]
end
defp extra_apps_in_app_tree(app) when app in [:eex, :ex_unit, :iex, :logger, :mix] do
# We include hex in here as it is always an archive and it cannot be depended on
defp extra_apps_in_app_tree(app) when app in [:eex, :ex_unit, :iex, :logger, :mix, :hex] do
[app]
end
+15 -1
View File
@@ -112,12 +112,26 @@ defmodule Mix.Tasks.Help do
def run(["app:" <> app]) do
loadpaths!()
app = String.to_atom(app)
# If the application is not available, attempt to load it from Erlang/Elixir
if is_nil(Application.spec(app, :vsn)) do
try do
Mix.ensure_application!(app)
rescue
_ ->
# Otherwise, it may be a dep or the current project not yet compiled
if Mix.Project.get() do
Mix.Task.run("compile")
end
end
end
if modules = Application.spec(app, :modules) do
for module <- modules,
not (module |> Atom.to_string() |> String.starts_with?("Elixir.Mix.Tasks.")),
{:docs_v1, _, :elixir, "text/markdown", %{"en" => <<doc::binary>>}, _, _} <-
{:docs_v1, _, _, "text/markdown", %{"en" => <<doc::binary>>}, _, _} <-
[Code.fetch_docs(module)] do
leading = doc |> String.split(["\n\n", "\r\n\r\n"], parts: 2) |> hd()
"# #{inspect(module)}\n#{leading}\n"
+64 -15
View File
@@ -44,12 +44,22 @@ defmodule Mix.Tasks.Xref do
└── lib/a.ex
Because you have a compile-time dependency, any of the files `lib/a.ex`,
`lib/b.ex`, and `lib/c.ex` depend on will cause the whole cycle to
recompile. Therefore, your first priority to reduce compile times is
to remove such cycles. You can spot them by running:
`lib/b.ex`, and `lib/c.ex` depend on will cause `lib/a.ex` to recompile.
In other words, whenever you have a cycle, **a change to any file in the
cycle will cause all compile-time deps to recompile**. Therefore, your
first priority to reduce constant recompilations is to remove them.
You can spot them by running:
$ mix xref graph --format cycles --label compile-connected
> #### Use the --label option
>
> The job of `mix xref` is to explore relationships between files
> and it is expected that most of your files are either directly
> or indirectly connected. For this reason, it is strongly advised
> to pass the `--label` option to filter the amount of data,
> specifically with `compile-connected` (or `compile`) as values.
Whenever you find a compile-time dependency, such as `lib/a.ex` pointing
to `lib/b.ex`, there are two ways to remove them:
@@ -208,13 +218,11 @@ defmodule Mix.Tasks.Xref do
* `--exclude` - path to exclude. Can be repeated to exclude multiple paths.
* `--label` - only shows relationships with the given label.
The labels are "compile", "export" and "runtime". By default, the `--label`
option does not change how the graph is computed, it simply filters the
printed graph to show only relationships with the given label. However,
you can pass `--only-direct` to trim the graph to only the nodes that
have the direct relationship given by label. There is also a special
label called "compile-connected" that keeps only compile-time files with
at least one transitive dependency. See "Dependency types" section below.
The labels are "compile-connected", "compile", "export" and "runtime".
By default, the `--label` option does not change how the graph is computed,
it simply filters the printed graph to show only relationships with the given
label. However, you can pass `--only-direct` to trim the graph to only the
nodes that have the direct relationship given by label.
* `--group` - provide comma-separated paths to consider as a group. Dependencies
from and into multiple files of the group are considered a single dependency.
@@ -239,6 +247,9 @@ defmodule Mix.Tasks.Xref do
* `--min-cycle-size` - controls the minimum cycle size on formats
like `stats` and `cycles`
* `--min-cycle-label` - controls the minimum number of dependencies
with the given `--label` on a cycle
* `--format` - can be set to one of:
* `pretty` - prints the graph to the terminal using Unicode characters.
@@ -435,6 +446,7 @@ defmodule Mix.Tasks.Xref do
sink: :keep,
source: :keep,
min_cycle_size: :integer,
min_cycle_label: :integer,
output: :string
]
@@ -1218,9 +1230,22 @@ defmodule Mix.Tasks.Xref do
cycles
end
# :compile_connected is the same
min_cycle_label =
if integer = opts[:min_cycle_label] do
if filter == :all do
Mix.raise("--min-cycle-label requires the --label option to be given")
end
integer
else
1
end
# :compile_connected is the same as :compile
if cycle_fn = cycle_filter_fn(filter) do
Enum.filter(cycles, fn {_length, cycle} -> Enum.any?(cycle, cycle_fn) end)
Enum.filter(cycles, fn {_length, cycle} ->
Enum.count_until(cycle, cycle_fn, min_cycle_label) == min_cycle_label
end)
else
cycles
end
@@ -1269,11 +1294,35 @@ defmodule Mix.Tasks.Xref do
shell.info("#{length(cycles)} cycles found. Showing them in decreasing size:\n")
for {length, cycle} <- cycles do
shell.info("Cycle of length #{length}:\n")
meta =
cycle
|> Enum.reduce({0, 0}, fn
{_, :compile}, {compile, export} -> {compile + 1, export}
{_, :export}, {compile, export} -> {compile, export + 1}
{_, _}, {compile, export} -> {compile, export}
end)
|> case do
{0, 0} ->
""
{compile, export} ->
info =
if(compile > 0, do: ["#{compile} compile"], else: []) ++
if(export > 0, do: ["#{export} export"], else: [])
" (" <> Enum.join(info, ", ") <> ")"
end
shell.info("Cycle of length #{length}#{meta}:\n")
for {node, type} <- cycle do
type = if type, do: " (#{type})", else: ""
shell.info(" " <> node <> type)
shell.info(
case type do
:compile -> [:red, " #{node} (compile)"]
:export -> " #{node} (export)"
_ -> " #{node}"
end
)
end
shell.info("")
+73
View File
@@ -907,4 +907,77 @@ defmodule Mix.Utils do
uid
end
end
@doc """
Convert the given terms to a consultable Erlang term.
"""
def consultable(term) do
{:ok, to_erl_term(term)}
catch
{:error, term, reason} -> {:error, term, reason}
end
defp to_erl_term(tuple) when is_tuple(tuple) do
[?{, tuple |> Tuple.to_list() |> to_erl_head(), ?}]
end
defp to_erl_term(list) when is_list(list) do
if List.ascii_printable?(list) do
:io_lib.print(list)
else
[?[, to_erl_head(list), ?]]
end
end
defp to_erl_term(%Regex{re_pattern: {:re_pattern, _, _, _, ref}} = regex)
when is_reference(ref) do
throw({:error, regex, "you must use the /E modifier to store regexes"})
end
defp to_erl_term(map) when is_map(map) do
inner =
Enum.map_intersperse(
:maps.to_list(:maps.iterator(map, :reversed)),
?,,
fn {key, value} -> [to_erl_term(key), "=>", to_erl_term(value)] end
)
[?#, ?{, inner, ?}]
end
defp to_erl_term(map) when is_map(map) do
inner =
Enum.map_intersperse(
:maps.to_list(:maps.iterator(map, :reversed)),
?,,
fn {key, value} -> [to_erl_term(key), "=>", to_erl_term(value)] end
)
[?#, ?{, inner, ?}]
end
defp to_erl_term(function) when is_function(function) do
fun_info = Function.info(function)
if fun_info[:type] == :external and fun_info[:env] == [] do
:io_lib.print(function)
else
throw({:error, function, "only functions in the form &Mod.fun/arity are allowed"})
end
end
defp to_erl_term(term) when is_reference(term) or is_pid(term) do
throw({:error, term, "PIDs and References are not supported"})
end
defp to_erl_term(term) do
:io_lib.print(term)
end
defp to_erl_head([]), do: []
defp to_erl_head([h | t]), do: [to_erl_term(h) | to_erl_tail(t)]
defp to_erl_tail([h | t]), do: [?,, to_erl_term(h) | to_erl_tail(t)]
defp to_erl_tail([]), do: []
defp to_erl_tail(other), do: [?|, to_erl_term(other)]
end
+6 -5
View File
@@ -592,19 +592,20 @@ defmodule Mix.ReleaseTest do
assert make_sys_config(release([]), [foo: [bar: :baz]], "unused/runtime/path") == :ok
contents = File.read!(@sys_config)
assert contents =~ "%% RUNTIME_CONFIG=false"
assert contents =~ "[{foo,[{bar,baz}]}]."
{:ok, contents} = :file.consult(@sys_config)
assert contents == [[foo: [bar: :baz]]]
end
test "writes sys_config with encoding" do
assert make_sys_config(
release([]),
[encoding: {:_μ, :"£", "£", ~c"£"}],
[encoding: [key: {:_μ, :"£", "£", ~c"£"}]],
"unused/runtime/path"
) ==
:ok
{:ok, contents} = :file.consult(@sys_config)
assert contents == [[encoding: {:_μ, :"£", "£", ~c"£"}]]
assert contents == [[encoding: [key: {:_μ, :"£", "£", ~c"£"}]]]
end
test "writes the given sys_config with config providers" do
@@ -649,12 +650,12 @@ defmodule Mix.ReleaseTest do
end
test "errors on bad config" do
assert {:error, "Could not read configuration file." <> _} =
assert {:error, "Could not write configuration file " <> _} =
make_sys_config(release([]), [foo: [bar: self()]], "unused/runtime/path")
env = %{__ENV__ | lexical_tracker: self()}
assert {:error, "Could not read configuration file." <> _} =
assert {:error, "Could not write configuration file " <> _} =
make_sys_config(release([]), [foo: [bar: env]], "unused/runtime/path")
end
end
+24 -2
View File
@@ -110,7 +110,7 @@ defmodule Mix.Tasks.Compile.AppTest do
test "uses custom application settings" do
in_fixture("no_mixfile", fn ->
Mix.Project.push(CustomProject)
env = [foo: [:one, "two", 3, 4], bar: [{} | %{foo: :bar}]]
env = [foo: [:one, "two", 3, 4, "฿", &List.flatten/1], bar: [{} | %{foo: :bar}]]
Process.put(:application,
maxT: :infinity,
@@ -132,7 +132,10 @@ defmodule Mix.Tasks.Compile.AppTest do
assert properties[:applications] ==
[:kernel, :stdlib, :elixir, :logger, :ex_unit, :example_app, :mix]
assert properties[:env] == [foo: [:one, "two", 3, 4], bar: [{} | %{foo: :bar}]]
assert properties[:env] == [
foo: [:one, "two", 3, 4, "฿", &List.flatten/1],
bar: [{} | %{foo: :bar}]
]
refute Keyword.has_key?(properties, :extra_applications)
end)
@@ -260,6 +263,25 @@ defmodule Mix.Tasks.Compile.AppTest do
end)
end
@tag :re_import
test "accepts only regexes without a reference" do
in_fixture("no_mixfile", fn ->
Mix.Project.push(CustomProject)
Process.put(:application, env: [regex: ~r/foo/])
assert_raise Mix.Error, ~r/you must use the \/E modifier to store regexes/, fn ->
Mix.Tasks.Compile.App.run([])
end
Process.put(:application, env: [exported: ~r/foo/E])
Mix.Tasks.Compile.Elixir.run([])
Mix.Tasks.Compile.App.run([])
properties = parse_resource_file(:custom_project)
assert properties[:env] == [exported: ~r/foo/E]
end)
end
test ".app contains description and registered (as required by systools)" do
in_fixture("no_mixfile", fn ->
Mix.Project.push(MixTest.Case.Sample)
+49 -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
@@ -1253,6 +1284,23 @@ defmodule Mix.Tasks.Compile.ElixirTest do
end)
end
test "recompiles with --force-elixir" do
in_fixture("no_mixfile", fn ->
Mix.Project.push(MixTest.Case.Sample)
assert Mix.Task.run("compile") == {:ok, []}
purge([A, B])
# Now we have a noop
Mix.Task.clear()
assert Mix.Task.run("compile") == {:noop, []}
# --force-elixir
Mix.Task.clear()
assert Mix.Task.run("compile", ["--verbose", "--force-elixir"]) == {:ok, []}
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
end)
end
test "compiles files with autoload disabled" do
in_fixture("no_mixfile", fn ->
Mix.Project.push(MixTest.Case.Sample)
+47 -3
View File
@@ -199,20 +199,64 @@ defmodule Mix.Tasks.HelpTest do
in_tmp(context.test, fn ->
output =
capture_io(fn ->
Mix.Tasks.Help.run(["app:mix"])
Mix.Tasks.Help.run(["app:iex"])
end)
assert output =~ "# Mix\n\nMix is a build tool"
assert output =~ "# IEx\n\nElixir's interactive shell."
if System.otp_release() >= "27" do
output =
capture_io(fn ->
Mix.Tasks.Help.run(["app:parsetools"])
end)
assert output =~ "# :leex"
end
end)
end
test "help unknown app:APP", context do
test "help app:UNKNOWN", context do
in_tmp(context.test, fn ->
Mix.Tasks.Help.run(["app:foobar"])
assert_received {:mix_shell, :error, ["Application foobar does not exist or is not loaded"]}
end)
end
defmodule ExampleProject do
def project do
[
app: :sample,
version: "0.1.0"
]
end
end
test "help app:APP for current project", context do
in_tmp(context.test, fn ->
Mix.Project.push(ExampleProject)
File.mkdir_p!("lib")
File.write!("lib/example.ex", ~s'''
defmodule Example do
@moduledoc """
This is an example module.
"""
end
''')
output =
capture_io(fn ->
Mix.Tasks.Help.run(["app:sample"])
end)
assert output =~ """
# Example
This is an example module.
"""
end)
end
test "help Elixir MODULE", context do
in_tmp(context.test, fn ->
output =
+25 -7
View File
@@ -500,7 +500,7 @@ defmodule Mix.Tasks.XrefTest do
assert_graph(["--format", "cycles"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
@@ -512,7 +512,7 @@ defmodule Mix.Tasks.XrefTest do
assert_graph(["--format", "cycles", "--label", "compile"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
@@ -524,7 +524,7 @@ defmodule Mix.Tasks.XrefTest do
assert_graph(["--format", "cycles", "--label", "compile-connected"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
@@ -539,7 +539,7 @@ defmodule Mix.Tasks.XrefTest do
assert_graph(["--format", "cycles", "--fail-above", "0"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
@@ -548,11 +548,11 @@ defmodule Mix.Tasks.XrefTest do
end
end
test "cycles with min_cycle_size matching actual length" do
assert_graph(["--format", "cycles", "--min-cycle-size", "2"], """
test "cycles with min_cycle_label matching actual length" do
assert_graph(["--format", "cycles", "--label", "compile", "--min-cycle-label", "1"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
@@ -560,6 +560,24 @@ defmodule Mix.Tasks.XrefTest do
""")
end
test "cycles with min_cycle_size matching actual length" do
assert_graph(["--format", "cycles", "--min-cycle-size", "2"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
""")
end
test "cycles with min_cycle_label greater than actual length" do
assert_graph(["--format", "cycles", "--label", "compile", "--min-cycle-label", "2"], """
No cycles found
""")
end
test "cycles with min_cycle_size greater than actual length" do
assert_graph(["--format", "cycles", "--min-cycle-size", "3"], """
No cycles found
+11 -1
View File
@@ -43,12 +43,22 @@ cover_exclude =
[]
end
# OTP 28.1+
re_import_exclude =
if Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) do
[]
else
[:re_import]
end
Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__)
CoverageRecorder.maybe_record("mix")
ExUnit.start(
trace: !!System.get_env("TRACE"),
exclude: epmd_exclude ++ os_exclude ++ git_exclude ++ line_exclude ++ cover_exclude,
exclude:
epmd_exclude ++
os_exclude ++ git_exclude ++ line_exclude ++ cover_exclude ++ re_import_exclude,
include: line_include,
assert_receive_timeout: String.to_integer(System.get_env("ELIXIR_ASSERT_TIMEOUT", "300"))
)