Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
915d6dca16 | ||
|
|
a983c8c043 | ||
|
|
d33fd8e413 | ||
|
|
02603b147c | ||
|
|
e126dab368 | ||
|
|
38547285f1 | ||
|
|
4f07670d08 | ||
|
|
dcff0ffaea | ||
|
|
97407cde90 | ||
|
|
579fc97b97 | ||
|
|
003ef335e7 | ||
|
|
028cdbb91e | ||
|
|
794441a255 | ||
|
|
727106beb4 | ||
|
|
5e45871b5e | ||
|
|
0db308cbff | ||
|
|
35a7e3aa1f | ||
|
|
7340597020 | ||
|
|
3b6acc33d2 | ||
|
|
2ed048acc6 | ||
|
|
bf0c203d52 | ||
|
|
b074e6b159 | ||
|
|
bfe7315fbb | ||
|
|
28f4fdc21b | ||
|
|
ee1dc0729f | ||
|
|
32e724c87c | ||
|
|
7758084211 | ||
|
|
57d3098c2a | ||
|
|
427d21e410 | ||
|
|
24a188a016 | ||
|
|
457d52c89b | ||
|
|
6684dbd3f2 | ||
|
|
ccb2a742ad | ||
|
|
92094c9ef1 | ||
|
|
d4d5da2ccf | ||
|
|
666c082105 | ||
|
|
d2c9d39efe | ||
|
|
97d16038fe | ||
|
|
87cbf06ff1 | ||
|
|
caf39255a4 | ||
|
|
6440fb90a3 | ||
|
|
8793c68822 | ||
|
|
49ac038d67 | ||
|
|
3827b73d2d | ||
|
|
dc8a19028f | ||
|
|
357c346a9f | ||
|
|
6df9eb338c | ||
|
|
fa85b95250 | ||
|
|
a89655c434 | ||
|
|
730fd0f824 | ||
|
|
29d3b0cf3a | ||
|
|
ae94e6eb25 | ||
|
|
661e537cda | ||
|
|
4a5c076b7f | ||
|
|
297bc8302b | ||
|
|
f6413152ff | ||
|
|
c2edbaf173 | ||
|
|
ac9a659bc6 | ||
|
|
cc5837caa7 | ||
|
|
1738ed453f | ||
|
|
3e18aafa0a | ||
|
|
bcb73a468b | ||
|
|
613f631487 | ||
|
|
f08a84af10 | ||
|
|
b1a7afd061 | ||
|
|
e9bf5df25a | ||
|
|
2e7bb9b47f | ||
|
|
83c68cecc1 | ||
|
|
184b724483 | ||
|
|
47fea9580a | ||
|
|
6a6678f337 | ||
|
|
ec87c6b111 | ||
|
|
0c595489b6 | ||
|
|
cdbfa09edb | ||
|
|
baa25991fa | ||
|
|
a5c9121a7c |
@@ -309,7 +309,6 @@ jobs:
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: ${{ vars.HEX_AWS_REGION }}
|
||||
AWS_S3_BUCKET: ${{ vars.HEX_AWS_S3_BUCKET }}
|
||||
OTP_GENERIC_VERSION: "25"
|
||||
steps:
|
||||
- name: "Check if variables are set up"
|
||||
if: "${{ ! vars.HEX_AWS_REGION }}"
|
||||
@@ -330,6 +329,8 @@ jobs:
|
||||
run: |
|
||||
ref_name=${{ github.ref_name }}
|
||||
|
||||
oldest_otp=$(find . -type f -name 'elixir-otp-*.zip' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip$/\1/' | sort -n | head -n 1)
|
||||
|
||||
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
|
||||
dest=${zip/elixir/${ref_name}}
|
||||
surrogate_key=${dest/.zip$/}
|
||||
@@ -339,7 +340,7 @@ jobs:
|
||||
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
|
||||
|
||||
if [ "$zip" == "elixir-otp-${OTP_GENERIC_VERSION}.zip" ]; then
|
||||
if [ "$zip" == "elixir-otp-${oldest_otp}.zip" ]; then
|
||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${ref_name}.zip" \
|
||||
--cache-control "public,max-age=3600" \
|
||||
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${ref_name}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
@@ -372,6 +373,8 @@ jobs:
|
||||
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
ref_name=${{ github.ref_name }}
|
||||
|
||||
oldest_otp=$(find . -name 'elixir-otp-*.zip.sha256sum' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip\.sha256sum$/\1/' | sort -n | head -n 1)
|
||||
|
||||
aws s3 cp "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" builds.txt || true
|
||||
touch builds.txt
|
||||
|
||||
@@ -382,7 +385,7 @@ jobs:
|
||||
sed -i "/^${ref_name}-${otp_version} /d" builds.txt
|
||||
echo -e "${ref_name}-${otp_version} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
|
||||
|
||||
if [ "${otp_version}" == "otp-${OTP_GENERIC_VERSION}" ]; then
|
||||
if [ "${otp_version}" == "otp-${oldest_otp}" ]; then
|
||||
sed -i "/^${ref_name} /d" builds.txt
|
||||
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
|
||||
fi
|
||||
|
||||
+139
-1
@@ -234,6 +234,144 @@ 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
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Measure and optimize writing of `.beam` files in the compiler
|
||||
* [Kernel] Optimize rare scenarios where type checking took too long
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Add flag `--no-check-cwd` to skip compiler check to aid debugging
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [IO] Fix dialyzer warning on `IO.inspect :label`
|
||||
* [Kernel] Ensure we warn on deprecated `~~~` unary operator
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Reset ansi escapes before newlines in Logger
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Warn if `elixirc_paths` is not a list of string paths
|
||||
* [mix compile] Address regression where umbrella children were compiled too early and without respecting compilation flags
|
||||
* [mix deps.compile] Improve reliability of `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` across `mix escript.install`, `mix archive.install`, and others
|
||||
|
||||
## v1.19.1 (2025-10-20)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Address Dialyzer warnings when invoking `EEx.compile_string`
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Optimize how types are computed for pretty printing
|
||||
* [Kernel] Optimize how differences are computed in the type system
|
||||
* [Macro] Do not escape options given to `dbg/2`
|
||||
* [Protocol] Improve protocol violation warnings
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Do not attempt to touch deleted files when compilation fails and then resumed with missing files
|
||||
* [mix deps.compile] Do not spawn partitions when all dependencies are local and already compiled
|
||||
|
||||
## v1.19.0 (2025-10-16)
|
||||
|
||||
### 1. Enhancements
|
||||
@@ -337,7 +475,7 @@ This work was performed by [Jonatan Männchen](https://maennchen.dev) and sponso
|
||||
* [mix format] Ensure the formatter does not go over the specified limit in certain corner cases
|
||||
* [mix release] Fix `RELEASE_SYS_CONFIG` for Windows 11
|
||||
* [mix test] Ensure modules are preloaded in `mix test --slowest-modules=N`
|
||||
* [mix xref graph] Provide more consistent output by considering strong connected components only when computing graphs
|
||||
* [mix xref graph] Provide more consistent output by considering strong connected components when computing graphs. This means that, if you were using `mix xref graph --format cycles`, you will now get fewer but larger cycles, as cycle between `A` and `B` and another between `A` and `C` is now considered a single cycle between `A`, `B`, and `C`. Note the cycles themselves are not problematic, unless they have compile-time dependencies in them
|
||||
|
||||
### 3. Soft deprecations (no warnings emitted)
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.19.0
|
||||
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.0
|
||||
set ELIXIR_VERSION=1.19.6
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
|
||||
+3
-1
@@ -236,7 +236,9 @@ defmodule EEx do
|
||||
"""
|
||||
@spec compile_string(String.t(), [compile_opt]) :: Macro.t()
|
||||
def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
|
||||
case tokenize(source, options) do
|
||||
tokenize_opts = Keyword.take(options, [:file, :line, :column, :indentation, :trim])
|
||||
|
||||
case tokenize(source, tokenize_opts) do
|
||||
{:ok, tokens} ->
|
||||
EEx.Compiler.compile(tokens, source, options)
|
||||
|
||||
|
||||
@@ -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 =
|
||||
[
|
||||
|
||||
@@ -706,7 +706,7 @@ defmodule File do
|
||||
path = IO.chardata_to_string(path)
|
||||
|
||||
with {:error, :enoent} <- :elixir_utils.change_universal_time(path, time),
|
||||
:ok <- write(path, "", [:append]),
|
||||
:ok <- write(path, "", [:raw, :append]),
|
||||
do: :elixir_utils.change_universal_time(path, time)
|
||||
end
|
||||
|
||||
@@ -714,7 +714,7 @@ defmodule File do
|
||||
path = IO.chardata_to_string(path)
|
||||
|
||||
with {:error, :enoent} <- :elixir_utils.change_posix_time(path, time),
|
||||
:ok <- write(path, "", [:append]),
|
||||
:ok <- write(path, "", [:raw, :append]),
|
||||
do: :elixir_utils.change_posix_time(path, time)
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -511,7 +511,8 @@ defmodule IO do
|
||||
"""
|
||||
@spec inspect(device, item, inspect_opts) :: item when item: var
|
||||
def inspect(device, item, opts) when is_device(device) and is_list(opts) do
|
||||
label = if label = opts[:label], do: [to_chardata(label), ": "], else: []
|
||||
{label, opts} = Keyword.pop(opts, :label)
|
||||
label = if label, do: [to_chardata(label), ": "], else: []
|
||||
opts = Inspect.Opts.new(opts)
|
||||
doc = Inspect.Algebra.group(Inspect.Algebra.to_doc(item, opts))
|
||||
chardata = Inspect.Algebra.format(doc, opts.width)
|
||||
|
||||
@@ -380,27 +380,73 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, {:compile, path}, timestamp) do
|
||||
File.mkdir_p!(path)
|
||||
Code.prepend_path(path)
|
||||
defp write_module_binaries(result, {:compile, path}, state) when map_size(result) > 0 do
|
||||
profile(state, "writing modules to disk", fn ->
|
||||
File.mkdir_p!(path)
|
||||
Code.prepend_path(path)
|
||||
timestamp = state.beam_timestamp
|
||||
|
||||
for {{:module, module}, {binary, _}} when is_binary(binary) <- result do
|
||||
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
|
||||
File.write!(full_path, binary)
|
||||
if timestamp, do: File.touch!(full_path, timestamp)
|
||||
module
|
||||
end
|
||||
# We fan-out the writes as that improves performance
|
||||
# when writing hundreds of beam files. This is cheap as
|
||||
# we only transfer atoms and binaries across processes.
|
||||
pool_size = min(map_size(result), state.schedulers)
|
||||
|
||||
pool_list =
|
||||
for _ <- 1..pool_size do
|
||||
spawn_link(fn -> write_loop(path, timestamp) end)
|
||||
end
|
||||
|
||||
pool_tuple = List.to_tuple(pool_list)
|
||||
|
||||
{modules, _} =
|
||||
Enum.flat_map_reduce(result, 0, fn
|
||||
{{:module, module}, {binary, _}}, scheduler when is_binary(binary) ->
|
||||
send(elem(pool_tuple, scheduler), {:write, module, binary})
|
||||
{[module], rem(scheduler + 1, pool_size)}
|
||||
|
||||
_, scheduler ->
|
||||
{[], scheduler}
|
||||
end)
|
||||
|
||||
pool_refs =
|
||||
for pid <- pool_list do
|
||||
ref = Process.monitor(pid)
|
||||
send(pid, :done)
|
||||
ref
|
||||
end
|
||||
|
||||
for ref <- pool_refs do
|
||||
receive do
|
||||
{:DOWN, ^ref, _, _, _} -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
modules
|
||||
end)
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, _output, _timestamp) do
|
||||
defp write_module_binaries(result, _output, _state) do
|
||||
for {{:module, module}, {binary, _}} when is_binary(binary) <- result, do: module
|
||||
end
|
||||
|
||||
defp write_loop(path, timestamp) do
|
||||
receive do
|
||||
{:write, module, binary} ->
|
||||
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
|
||||
File.write!(full_path, binary, [:raw])
|
||||
if timestamp, do: File.touch!(full_path, timestamp)
|
||||
write_loop(path, timestamp)
|
||||
|
||||
:done ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
## Verification
|
||||
|
||||
defp verify_modules(result, compile_warnings, dependent_modules, state) do
|
||||
modules = write_module_binaries(result, state.output, state.beam_timestamp)
|
||||
_ = state.after_compile.()
|
||||
modules = write_module_binaries(result, state.output, state)
|
||||
profile(state, "after compile callback", state.after_compile)
|
||||
|
||||
runtime_warnings =
|
||||
if state.verification? do
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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()) ::
|
||||
@@ -2700,7 +2701,7 @@ defmodule Macro do
|
||||
:ok
|
||||
end
|
||||
|
||||
prelude = quote do: options = unquote(Macro.escape(options))
|
||||
prelude = quote do: options = unquote(options)
|
||||
acc = {prelude, dbg_format_header(env)}
|
||||
|
||||
{acc, nil} =
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
defmodule Module.ParallelChecker do
|
||||
@moduledoc false
|
||||
@elixir_checker_version :elixir_erl.checker_version()
|
||||
|
||||
import Kernel, except: [spawn: 3]
|
||||
|
||||
@@ -62,14 +63,14 @@ defmodule Module.ParallelChecker do
|
||||
@doc """
|
||||
Spawns a process that runs the parallel checker.
|
||||
"""
|
||||
def spawn({pid, {checker, table}}, module, module_map, beam_location, log?) do
|
||||
def spawn({pid, {checker, table}}, module, module_map, signatures, beam_location, log?) do
|
||||
# Protocols may have been consolidated. So if we know their beam location,
|
||||
# we discard their module map on purpose and start from file.
|
||||
info =
|
||||
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
|
||||
List.to_string(beam_location)
|
||||
else
|
||||
cache_from_module_map(table, module_map)
|
||||
cache_from_module_map(table, module_map, signatures)
|
||||
end
|
||||
|
||||
inner_spawn(pid, checker, table, module, info, log?)
|
||||
@@ -96,10 +97,12 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
|
||||
with {:ok, binary} <- File.read(location),
|
||||
{:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
|
||||
{:debug_info_v1, backend, data} = chunk,
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
|
||||
cache_from_module_map(table, module_map)
|
||||
{:ok,
|
||||
{_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", checker}]}} <-
|
||||
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
|
||||
{cache_chunk(table, module, contents), module_map_to_module_tuple(module_map)}
|
||||
else
|
||||
_ -> {:not_found, nil}
|
||||
end
|
||||
@@ -177,9 +180,9 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives pairs of module maps and BEAM binaries. In parallel it verifies
|
||||
the modules and adds the ExCk chunk to the binaries. Returns the updated
|
||||
list of warnings from the verification.
|
||||
Receives pairs of module maps and BEAM binaries.
|
||||
|
||||
Returns the updated list of warnings from the verification.
|
||||
"""
|
||||
@spec verify(cache(), [{module(), Path.t()}]) :: [warning()]
|
||||
def verify({checker, table}, runtime_files) do
|
||||
@@ -423,7 +426,7 @@ defmodule Module.ParallelChecker do
|
||||
mode =
|
||||
with {^module, binary, _filename} <- object_code,
|
||||
{:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
|
||||
{:elixir_checker_v3, contents} <- :erlang.binary_to_term(chunk) do
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(chunk) do
|
||||
# The chunk has more information, so that's our preference
|
||||
cache_chunk(table, module, contents)
|
||||
else
|
||||
@@ -474,12 +477,12 @@ defmodule Module.ParallelChecker do
|
||||
if Keyword.has_key?(attributes, :__protocol__), do: :protocol, else: :elixir
|
||||
end
|
||||
|
||||
defp cache_from_module_map(table, map) do
|
||||
defp cache_from_module_map(table, map, signatures) do
|
||||
exports =
|
||||
behaviour_exports(map) ++
|
||||
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
|
||||
|
||||
cache_info(table, map.module, exports, Map.new(map.deprecated), map.signatures)
|
||||
cache_info(table, map.module, exports, Map.new(map.deprecated), signatures)
|
||||
{elixir_mode(map.attributes), module_map_to_module_tuple(map)}
|
||||
end
|
||||
|
||||
|
||||
@@ -979,7 +979,7 @@ defmodule Module.Types.Apply do
|
||||
{mod, fun, arity, converter} = mfac
|
||||
meta = elem(expr, 1)
|
||||
|
||||
{banner, hints, traces} =
|
||||
{banner, traces} =
|
||||
case Keyword.get(meta, :type_check) do
|
||||
:interpolation ->
|
||||
{_, _, [arg]} = expr
|
||||
@@ -990,7 +990,7 @@ defmodule Module.Types.Apply do
|
||||
#{expr_to_string(arg) |> indent(4)}
|
||||
|
||||
it has type:
|
||||
""", [:interpolation], collect_traces(expr, context)}
|
||||
""", collect_traces(expr, context)}
|
||||
|
||||
:generator ->
|
||||
{:<-, _, [_, arg]} = expr
|
||||
@@ -1001,7 +1001,7 @@ defmodule Module.Types.Apply do
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
it has type:
|
||||
""", [:generator], collect_traces(arg, context)}
|
||||
""", collect_traces(arg, context)}
|
||||
|
||||
:into ->
|
||||
{"""
|
||||
@@ -1010,7 +1010,7 @@ defmodule Module.Types.Apply do
|
||||
into: #{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
it has type:
|
||||
""", [:into], collect_traces(expr, context)}
|
||||
""", collect_traces(expr, context)}
|
||||
|
||||
_ ->
|
||||
mfa_or_fa = if mod, do: Exception.format_mfa(mod, fun, arity), else: "#{fun}/#{arity}"
|
||||
@@ -1021,37 +1021,79 @@ defmodule Module.Types.Apply do
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
given types:
|
||||
""", [], collect_traces(expr, context)}
|
||||
""", collect_traces(expr, context)}
|
||||
end
|
||||
|
||||
explanation =
|
||||
{explanation, impls} =
|
||||
cond do
|
||||
reason = empty_arg_reason(converter.(args_types)) ->
|
||||
reason
|
||||
{reason, ""}
|
||||
|
||||
Code.ensure_loaded?(mod) and
|
||||
Keyword.has_key?(mod.module_info(:attributes), :__protocol__) ->
|
||||
if function_exported?(mod, :__protocol__, 1) and
|
||||
mod.__protocol__(:impls) == {:consolidated, []} do
|
||||
"""
|
||||
but the protocol was not yet implemented for any type and therefore will always fail. \
|
||||
This error typically happens within libraries that define protocols and will disappear as \
|
||||
soon as there is one implementation. If you expect the protocol to be implemented later on, \
|
||||
you can define an implementation specific for development/test.
|
||||
"""
|
||||
{"""
|
||||
but the #{inspect(mod)} protocol was not yet implemented \
|
||||
for any type and therefore will always fail.
|
||||
|
||||
This warning will disappear once you define a implementation. \
|
||||
If the protocol is part of a library, you may define a dummy \
|
||||
implementation for development/test.
|
||||
""", ""}
|
||||
else
|
||||
# Protocol errors can be very verbose, so we collapse structs
|
||||
"""
|
||||
but expected a type that implements the #{inspect(mod)} protocol, it must be one of:
|
||||
#{clauses_args_to_quoted_string(clauses, converter, collapse_structs: true)}
|
||||
"""
|
||||
fix =
|
||||
case mod do
|
||||
String.Chars ->
|
||||
"""
|
||||
You either passed the wrong value or you must:
|
||||
|
||||
1. convert the given value to a string explicitly
|
||||
(use inspect/1 if you want to convert any data structure to a string)
|
||||
2. implement the String.Chars protocol
|
||||
"""
|
||||
|
||||
Enumerable ->
|
||||
"""
|
||||
You either passed the wrong value or you must:
|
||||
|
||||
1. convert the given value to an Enumerable explicitly
|
||||
2. implement the Enumerable protocol
|
||||
"""
|
||||
|
||||
_ ->
|
||||
"""
|
||||
You either passed the wrong value or you forgot to implement the protocol.
|
||||
"""
|
||||
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}\
|
||||
""",
|
||||
"""
|
||||
|
||||
#{hint()} the #{inspect(mod)} protocol is implemented for the following types:
|
||||
#{clauses_args_to_quoted_string(clauses, converter, collapse_structs: true)}
|
||||
"""}
|
||||
end
|
||||
|
||||
true ->
|
||||
"""
|
||||
but expected one of:
|
||||
#{clauses_args_to_quoted_string(clauses, converter, [])}
|
||||
"""
|
||||
{"""
|
||||
but expected one of:
|
||||
#{clauses_args_to_quoted_string(clauses, converter, [])}
|
||||
""", ""}
|
||||
end
|
||||
|
||||
%{
|
||||
@@ -1066,7 +1108,7 @@ defmodule Module.Types.Apply do
|
||||
""",
|
||||
explanation,
|
||||
format_traces(traces),
|
||||
format_hints(hints)
|
||||
impls
|
||||
])
|
||||
}
|
||||
end
|
||||
@@ -1252,7 +1294,7 @@ defmodule Module.Types.Apply do
|
||||
common = intersection(actual, expected)
|
||||
|
||||
uncommon_doc =
|
||||
difference(actual, common)
|
||||
difference(actual, expected)
|
||||
|> to_quoted()
|
||||
|> Code.Formatter.to_algebra()
|
||||
|> ansi_red()
|
||||
|
||||
@@ -27,11 +27,8 @@ defmodule Module.Types.Descr do
|
||||
@bit_top (1 <<< 7) - 1
|
||||
@bit_number @bit_integer ||| @bit_float
|
||||
|
||||
# Remark: those use AST for BDDs
|
||||
defmacrop bdd_leaf(arg1, arg2), do: {arg1, arg2}
|
||||
|
||||
defmacrop domain_key(key),
|
||||
do: {:domain_key, key}
|
||||
defmacro bdd_leaf(arg1, arg2), do: {arg1, arg2}
|
||||
defmacro domain_key(key), do: {:domain_key, key}
|
||||
|
||||
@domain_key_types [
|
||||
{:domain_key, :binary},
|
||||
@@ -454,11 +451,7 @@ defmodule Module.Types.Descr do
|
||||
defp intersection(:map, v1, v2), do: map_intersection(v1, v2)
|
||||
defp intersection(:optional, 1, 1), do: 1
|
||||
defp intersection(:tuple, v1, v2), do: tuple_intersection(v1, v2)
|
||||
|
||||
defp intersection(:fun, v1, v2) do
|
||||
bdd = fun_intersection(v1, v2)
|
||||
if bdd == :bdd_bot, do: 0, else: bdd
|
||||
end
|
||||
defp intersection(:fun, v1, v2), do: fun_intersection(v1, v2)
|
||||
|
||||
defp intersection(:dynamic, v1, v2) do
|
||||
descr = dynamic_intersection(v1, v2)
|
||||
@@ -548,11 +541,7 @@ defmodule Module.Types.Descr do
|
||||
defp difference(:map, v1, v2), do: map_difference(v1, v2)
|
||||
defp difference(:optional, 1, 1), do: 0
|
||||
defp difference(:tuple, v1, v2), do: tuple_difference(v1, v2)
|
||||
|
||||
defp difference(:fun, v1, v2) do
|
||||
bdd = fun_difference(v1, v2)
|
||||
if bdd == :bdd_bot, do: 0, else: bdd
|
||||
end
|
||||
defp difference(:fun, v1, v2), do: fun_difference(v1, v2)
|
||||
|
||||
@doc """
|
||||
Compute the negation of a type.
|
||||
@@ -4425,23 +4414,7 @@ defmodule Module.Types.Descr do
|
||||
|
||||
## BDD helpers
|
||||
|
||||
defp bdd_map(bdd, fun) do
|
||||
case bdd do
|
||||
:bdd_bot ->
|
||||
:bdd_bot
|
||||
|
||||
:bdd_top ->
|
||||
:bdd_top
|
||||
|
||||
{_, _} ->
|
||||
fun.(bdd)
|
||||
|
||||
{literal, left, union, right} ->
|
||||
{fun.(literal), bdd_map(left, fun), bdd_map(union, fun), bdd_map(right, fun)}
|
||||
end
|
||||
end
|
||||
|
||||
defp bdd_union(bdd1, bdd2) do
|
||||
def bdd_union(bdd1, bdd2) do
|
||||
case {bdd1, bdd2} do
|
||||
{:bdd_top, _bdd} ->
|
||||
:bdd_top
|
||||
@@ -4475,10 +4448,14 @@ defmodule Module.Types.Descr do
|
||||
{:eq, _, _} ->
|
||||
bdd1
|
||||
end
|
||||
|> case do
|
||||
{_, :bdd_top, _, :bdd_top} -> :bdd_top
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp bdd_difference(bdd1, bdd2) do
|
||||
def bdd_difference(bdd1, bdd2) do
|
||||
case {bdd1, bdd2} do
|
||||
{_bdd, :bdd_top} ->
|
||||
:bdd_bot
|
||||
@@ -4495,19 +4472,58 @@ defmodule Module.Types.Descr do
|
||||
_ ->
|
||||
case bdd_compare(bdd1, bdd2) do
|
||||
{:lt, {lit1, c1, u1, d1}, bdd2} ->
|
||||
{lit1, bdd_difference(bdd_union(c1, u1), bdd2), :bdd_bot,
|
||||
bdd_difference(bdd_union(d1, u1), bdd2)}
|
||||
{lit1, bdd_difference(c1, bdd2), bdd_difference(u1, bdd2), bdd_difference(d1, bdd2)}
|
||||
|
||||
{:gt, bdd1, {lit2, c2, u2, d2}} ->
|
||||
{lit2, bdd_difference(bdd1, bdd_union(c2, u2)), :bdd_bot,
|
||||
bdd_difference(bdd1, bdd_union(d2, u2))}
|
||||
# The proper formula is:
|
||||
#
|
||||
# b1 and not (c2 or u2) : bdd_bot : b1 and not (d2 or u2)
|
||||
#
|
||||
# Both extremes have (b1 and not u2),
|
||||
# so we compute it first and only once.
|
||||
bdd1_minus_u2 = bdd_difference(bdd1, u2)
|
||||
{lit2, bdd_difference(bdd1_minus_u2, c2), :bdd_bot, bdd_difference(bdd1_minus_u2, d2)}
|
||||
|
||||
{:eq, {lit, c1, u1, d1}, {_, c2, u2, d2}} ->
|
||||
cond do
|
||||
u2 == :bdd_bot and d2 == :bdd_bot ->
|
||||
{lit, bdd_difference(c1, c2), bdd_difference(u1, c2), bdd_union(u1, d1)}
|
||||
c2 == :bdd_bot and d2 == :bdd_bot ->
|
||||
{lit, bdd_difference(c1, u2), bdd_difference(u1, u2), bdd_difference(d1, u2)}
|
||||
|
||||
u1 == u2 ->
|
||||
u2 == :bdd_bot ->
|
||||
cond do
|
||||
d2 == :bdd_bot ->
|
||||
{lit, bdd_difference(c1, c2), bdd_difference(u1, c2), bdd_union(u1, d1)}
|
||||
|
||||
c2 == :bdd_bot ->
|
||||
{lit, bdd_union(u1, c1), bdd_difference(u1, d2), bdd_difference(c1, c2)}
|
||||
|
||||
true ->
|
||||
# If d2 or c2 are bottom, we can remove one union.
|
||||
#
|
||||
# For example, if d2 is bottom, we have this BDD:
|
||||
#
|
||||
# {l, (C1 or U1) and not C2, U1 and not C2, D1 or U1}
|
||||
#
|
||||
# Where the constrained part is:
|
||||
#
|
||||
# (l and (C1 or U1) and not C2)
|
||||
#
|
||||
# Which expands to:
|
||||
#
|
||||
# (l and C1 and not C2) or (l and U1 and not C2)
|
||||
#
|
||||
# Given (U1 and not C2) is already part of the uncertain/union,
|
||||
# we can skip (l and U1 and not C2), and we end up with:
|
||||
#
|
||||
# {l, C1 and not C2, U1 and not C2, D1 or U1}
|
||||
#
|
||||
# Which are the formulas used above.
|
||||
{lit, bdd_difference(bdd_union(u1, c1), c2),
|
||||
bdd_difference(bdd_difference(u1, c2), d2),
|
||||
bdd_difference(bdd_union(u1, d1), d2)}
|
||||
end
|
||||
|
||||
u1 == :bdd_bot or u1 == u2 ->
|
||||
{lit, bdd_difference_union(c1, c2, u2), :bdd_bot,
|
||||
bdd_difference_union(d1, d2, u2)}
|
||||
|
||||
@@ -4525,14 +4541,10 @@ defmodule Module.Types.Descr do
|
||||
{:eq, _, _} ->
|
||||
:bdd_bot
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp bdd_compare(bdd1, bdd2) do
|
||||
case {bdd_head(bdd1), bdd_head(bdd2)} do
|
||||
{lit1, lit2} when lit1 < lit2 -> {:lt, bdd_expand(bdd1), bdd2}
|
||||
{lit1, lit2} when lit1 > lit2 -> {:gt, bdd1, bdd_expand(bdd2)}
|
||||
_ -> {:eq, bdd1, bdd2}
|
||||
|> case do
|
||||
{_, :bdd_bot, u, :bdd_bot} -> u
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4543,7 +4555,7 @@ defmodule Module.Types.Descr do
|
||||
defp bdd_difference_union(i, u1, u2),
|
||||
do: bdd_difference(i, bdd_union(u1, u2))
|
||||
|
||||
defp bdd_intersection(bdd1, bdd2) do
|
||||
def bdd_intersection(bdd1, bdd2) do
|
||||
case {bdd1, bdd2} do
|
||||
{:bdd_top, bdd} ->
|
||||
bdd
|
||||
@@ -4568,15 +4580,20 @@ defmodule Module.Types.Descr do
|
||||
bdd_intersection(bdd1, d2)}
|
||||
|
||||
# Notice that (l ? c1, u1, d1) and (l ? c2, u2, d2) is, on paper, equivalent to
|
||||
# [(l /\ c1) \/ u1 \/ (not l /\ d1)] and [(l /\ c2) \/ u2 \/ (not l /\ d2)]
|
||||
# [(l and c1) or u1 or (not l and d1)] and [(l and c2) or u2 or (not l and d2)].
|
||||
#
|
||||
# which is equivalent, by distributivity of intersection over union, to
|
||||
# l /\ [(c1 /\ c2) \/ (c1 /\ u2) \/ (u1 /\ c2)]
|
||||
# \/ (u1 /\ u2)
|
||||
# \/ [(not l) /\ ((d1 /\ u2) \/ (d1 /\ d2) \/ (u1 /\ d2))]
|
||||
#
|
||||
# l and [(c1 and c2) or (c1 and u2) or (u1 and c2)]
|
||||
# or (u1 and u2)
|
||||
# or [(not l) and ((d1 and u2) or (d1 and d2) or (u1 and d2))]
|
||||
#
|
||||
# which is equivalent, by factoring out c1 in the first disjunct, and d1 in the third, to
|
||||
# l /\ [c1 /\ (c2 \/ u2)] \/ (u1 /\ c2)
|
||||
# \/ (u1 /\ u2)
|
||||
# \/ (not l) /\ [d1 /\ (u2 \/ d2) \/ (u1 /\ d2)]
|
||||
#
|
||||
# l and [c1 and (c2 or u2)] or (u1 and c2)
|
||||
# or (u1 and u2)
|
||||
# or (not l) and [d1 and (u2 or d2) or (u1 and d2)]
|
||||
#
|
||||
# This last expression gives the following implementation:
|
||||
{:eq, {lit, c1, u1, d1}, {_, c2, u2, d2}} ->
|
||||
{lit, bdd_union(bdd_intersection_union(c1, c2, u2), bdd_intersection(u1, c2)),
|
||||
@@ -4592,6 +4609,10 @@ defmodule Module.Types.Descr do
|
||||
{:eq, bdd, _} ->
|
||||
bdd
|
||||
end
|
||||
|> case do
|
||||
{_, :bdd_bot, u, :bdd_bot} -> u
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4603,15 +4624,15 @@ defmodule Module.Types.Descr do
|
||||
do: bdd_intersection(i, bdd_union(u1, u2))
|
||||
|
||||
# Lazy negation: eliminate the union, then perform normal negation (switching leaves)
|
||||
defp bdd_negation(:bdd_top), do: :bdd_bot
|
||||
defp bdd_negation(:bdd_bot), do: :bdd_top
|
||||
defp bdd_negation({_, _} = pair), do: {pair, :bdd_bot, :bdd_bot, :bdd_top}
|
||||
def bdd_negation(:bdd_top), do: :bdd_bot
|
||||
def bdd_negation(:bdd_bot), do: :bdd_top
|
||||
def bdd_negation({_, _} = pair), do: {pair, :bdd_bot, :bdd_bot, :bdd_top}
|
||||
|
||||
defp bdd_negation({lit, c, u, d}) do
|
||||
def bdd_negation({lit, c, u, d}) do
|
||||
{lit, bdd_negation(bdd_union(c, u)), :bdd_bot, bdd_negation(bdd_union(d, u))}
|
||||
end
|
||||
|
||||
defp bdd_to_dnf(bdd), do: bdd_to_dnf([], [], [], bdd)
|
||||
def bdd_to_dnf(bdd), do: bdd_to_dnf([], [], [], bdd)
|
||||
|
||||
defp bdd_to_dnf(acc, _pos, _neg, :bdd_bot), do: acc
|
||||
defp bdd_to_dnf(acc, pos, neg, :bdd_top), do: [{pos, neg} | acc]
|
||||
@@ -4630,6 +4651,30 @@ defmodule Module.Types.Descr do
|
||||
|> bdd_to_dnf(pos, [lit | neg], d)
|
||||
end
|
||||
|
||||
defp bdd_compare(bdd1, bdd2) do
|
||||
case {bdd_head(bdd1), bdd_head(bdd2)} do
|
||||
{lit1, lit2} when lit1 < lit2 -> {:lt, bdd_expand(bdd1), bdd2}
|
||||
{lit1, lit2} when lit1 > lit2 -> {:gt, bdd1, bdd_expand(bdd2)}
|
||||
_ -> {:eq, bdd1, bdd2}
|
||||
end
|
||||
end
|
||||
|
||||
defp bdd_map(bdd, fun) do
|
||||
case bdd do
|
||||
:bdd_bot ->
|
||||
:bdd_bot
|
||||
|
||||
:bdd_top ->
|
||||
:bdd_top
|
||||
|
||||
{_, _} ->
|
||||
fun.(bdd)
|
||||
|
||||
{literal, left, union, right} ->
|
||||
{fun.(literal), bdd_map(left, fun), bdd_map(union, fun), bdd_map(right, fun)}
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, bdd_expand: 1, bdd_head: 1}
|
||||
defp bdd_expand({_, _} = pair), do: {pair, :bdd_top, :bdd_bot, :bdd_bot}
|
||||
defp bdd_expand(bdd), do: bdd
|
||||
|
||||
@@ -91,29 +91,6 @@ defmodule Module.Types.Helpers do
|
||||
"var.fun()" (with parentheses) means "var" is an atom()
|
||||
"""
|
||||
|
||||
:interpolation ->
|
||||
"""
|
||||
|
||||
#{hint()} string interpolation uses the String.Chars protocol to \
|
||||
convert a data structure into a string. Either convert the data type into a \
|
||||
string upfront or implement the protocol accordingly
|
||||
"""
|
||||
|
||||
:generator ->
|
||||
"""
|
||||
|
||||
#{hint()} for-comprehensions use the Enumerable protocol to traverse \
|
||||
data structures. Either convert the data type into a list (or another Enumerable) \
|
||||
or implement the protocol accordingly
|
||||
"""
|
||||
|
||||
:into ->
|
||||
"""
|
||||
|
||||
#{hint()} the :into option in for-comprehensions use the Collectable protocol to \
|
||||
build its result. Either pass a valid data type or implement the protocol accordingly
|
||||
"""
|
||||
|
||||
:anonymous_rescue ->
|
||||
"""
|
||||
|
||||
|
||||
@@ -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]))
|
||||
|
||||
+74
-43
@@ -267,6 +267,8 @@ defmodule Protocol do
|
||||
|
||||
@optional_callbacks __deriving__: 2
|
||||
|
||||
@elixir_checker_version :elixir_erl.checker_version()
|
||||
|
||||
@doc false
|
||||
defmacro def(signature)
|
||||
|
||||
@@ -451,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
|
||||
@@ -480,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) ++ [?.]
|
||||
|
||||
@@ -494,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
|
||||
|
||||
@@ -564,31 +574,34 @@ defmodule Protocol do
|
||||
# Ensure the types are sorted so the compiled beam is deterministic
|
||||
types = Enum.sort(types)
|
||||
|
||||
with {:ok, any, definitions, signatures, compile_info} <- beam_protocol(protocol),
|
||||
{:ok, definitions, signatures} <-
|
||||
consolidate(protocol, any, definitions, signatures, types),
|
||||
do: compile(definitions, signatures, compile_info)
|
||||
with {:ok, any, definitions, checker, compile_info} <- beam_protocol(protocol),
|
||||
{:ok, definitions, checker} <-
|
||||
consolidate(protocol, any, definitions, checker, types),
|
||||
do: compile(definitions, checker, compile_info)
|
||||
end
|
||||
|
||||
defp beam_protocol(protocol) do
|
||||
chunk_ids = [:debug_info, [?D, ?o, ?c, ?s]]
|
||||
chunk_ids = [:debug_info, [?E, ?x, ?C, ?k], [?D, ?o, ?c, ?s]]
|
||||
opts = [:allow_missing_chunks]
|
||||
|
||||
case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do
|
||||
{:ok, {^protocol, [{:debug_info, debug_info} | chunks]}} ->
|
||||
{:ok, {^protocol, [{:debug_info, debug_info}, {_, checker} | chunks]}} ->
|
||||
{:debug_info_v1, _backend, {:elixir_v1, module_map, specs}} = debug_info
|
||||
%{attributes: attributes, definitions: definitions} = module_map
|
||||
|
||||
# Protocols in precompiled archives may not have signatures, so we default to an empty map.
|
||||
# TODO: Remove this on Elixir v1.23.
|
||||
signatures = Map.get(module_map, :signatures, %{})
|
||||
|
||||
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
|
||||
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
|
||||
|
||||
case attributes[:__protocol__] do
|
||||
[fallback_to_any: any] ->
|
||||
{:ok, any, definitions, signatures, {module_map, specs, chunks}}
|
||||
checker =
|
||||
with true <- is_binary(checker),
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
|
||||
contents
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
|
||||
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
|
||||
{:ok, any, definitions, checker, {module_map, specs, chunks}}
|
||||
|
||||
_ ->
|
||||
{:error, :not_a_protocol}
|
||||
@@ -607,7 +620,7 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
# Consolidate the protocol for faster implementations and fine-grained type information.
|
||||
defp consolidate(protocol, fallback_to_any?, definitions, signatures, types) do
|
||||
defp consolidate(protocol, fallback_to_any?, definitions, checker, types) do
|
||||
case List.keytake(definitions, {:__protocol__, 1}, 0) do
|
||||
{protocol_def, definitions} ->
|
||||
types = if fallback_to_any?, do: types, else: List.delete(types, Any)
|
||||
@@ -623,25 +636,37 @@ defmodule Protocol do
|
||||
protocol_def = change_protocol(protocol_def, types)
|
||||
impl_for = change_impl_for(impl_for, protocol, types)
|
||||
struct_impl_for = change_struct_impl_for(struct_impl_for, protocol, types, structs)
|
||||
new_signatures = new_signatures(definitions, protocol_funs, protocol, types)
|
||||
|
||||
definitions = [protocol_def, impl_for, impl_for!, struct_impl_for] ++ definitions
|
||||
signatures = Enum.into(new_signatures, signatures)
|
||||
{:ok, definitions, signatures}
|
||||
|
||||
checker =
|
||||
if checker do
|
||||
update_in(checker.exports, fn exports ->
|
||||
signatures = new_signatures(definitions, protocol_funs, protocol, types, structs)
|
||||
|
||||
for {fun, info} <- exports do
|
||||
if sig = Map.get(signatures, fun) do
|
||||
{fun, %{info | sig: sig}}
|
||||
else
|
||||
{fun, info}
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
{:ok, definitions, checker}
|
||||
|
||||
nil ->
|
||||
{:error, :not_a_protocol}
|
||||
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!} =
|
||||
@@ -656,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)
|
||||
|
||||
@@ -680,10 +711,12 @@ defmodule Protocol do
|
||||
{fun_arity, {:strong, nil, [{[domain | rest], Descr.dynamic()}]}}
|
||||
end
|
||||
|
||||
[
|
||||
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
|
||||
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
|
||||
] ++ new_signatures
|
||||
Map.new(
|
||||
[
|
||||
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
|
||||
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
|
||||
] ++ new_signatures
|
||||
)
|
||||
end
|
||||
|
||||
defp get_protocol_functions({_name, _kind, _meta, clauses}) do
|
||||
@@ -752,11 +785,9 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
# Finally compile the module and emit its bytecode.
|
||||
defp compile(definitions, signatures, {module_map, specs, docs_chunk}) do
|
||||
# Protocols in precompiled archives may not have signatures, so we default to an empty map.
|
||||
# TODO: Remove this on Elixir v1.23.
|
||||
module_map = %{module_map | definitions: definitions} |> Map.put(:signatures, signatures)
|
||||
{:ok, :elixir_erl.consolidate(module_map, specs, docs_chunk)}
|
||||
defp compile(definitions, checker, {module_map, specs, docs_chunk}) do
|
||||
module_map = %{module_map | definitions: definitions}
|
||||
{:ok, :elixir_erl.consolidate(module_map, checker, specs, docs_chunk)}
|
||||
end
|
||||
|
||||
## Definition callbacks
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -86,7 +86,7 @@ iex> john = %User{name: "John", age: 27}
|
||||
iex> updates = [name: "Jane", age: 30]
|
||||
[name: "Jane", age: 30]
|
||||
iex> struct!(john, updates)
|
||||
%User{age: 27, name: "Jane"}
|
||||
%User{age: 30, name: "Jane"}
|
||||
```
|
||||
|
||||
`struct!/2` will raise an error if you try to set invalid fields:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -171,7 +171,7 @@ Version | Deprecated feature | Replaced by (ava
|
||||
[v1.5] | `Atom.to_char_list/1` | `Atom.to_charlist/1` (v1.3)
|
||||
[v1.5] | `Enum.filter_map/3` | `Enum.filter/2` + `Enum.map/2` or `for/1` comprehensions (v1.0)
|
||||
[v1.5] | `Float.to_char_list/1` | `Float.to_charlist/1` (v1.3)
|
||||
[v1.5] | `GenEvent` module | `Supervisor` and `GenServer` (v1.0);<br/>[`GenStage`](https://hex.pm/packages/gen_stage) (v1.3);<br/>[`:gen_event`](`:gen_event`) (Erlang/OTP 17)
|
||||
[v1.5] | `GenEvent` module | `Supervisor` and `GenServer` (v1.0)
|
||||
[v1.5] | `<%=` in middle and end expressions in `EEx` | Use `<%` (`<%=` is allowed only in start expressions) (v1.0)
|
||||
[v1.5] | `:as_char_lists` value in `t:Inspect.Opts.t/0` type | `:as_charlists` value (v1.3)
|
||||
[v1.5] | `:char_lists` key in `t:Inspect.Opts.t/0` type | `:charlists` key (v1.3)
|
||||
|
||||
@@ -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,11 +5,14 @@
|
||||
%% Compiler backend to Erlang.
|
||||
|
||||
-module(elixir_erl).
|
||||
-export([elixir_to_erl/1, elixir_to_erl/2, definition_to_anonymous/5, compile/1, consolidate/3,
|
||||
get_ann/1, debug_info/4, scope/2, format_error/1]).
|
||||
-export([elixir_to_erl/1, elixir_to_erl/2, definition_to_anonymous/5, compile/2, consolidate/4,
|
||||
get_ann/1, debug_info/4, scope/2, checker_version/0, format_error/1]).
|
||||
-include("elixir.hrl").
|
||||
-define(typespecs, 'Elixir.Kernel.Typespec').
|
||||
|
||||
checker_version() ->
|
||||
elixir_checker_v3.
|
||||
|
||||
%% debug_info callback
|
||||
|
||||
debug_info(elixir_v1, _Module, none, _Opts) ->
|
||||
@@ -132,14 +135,14 @@ scope(_Meta, ExpandCaptures) ->
|
||||
|
||||
%% Static compilation hook, used in protocol consolidation
|
||||
|
||||
consolidate(Map, TypeSpecs, DocsChunk) ->
|
||||
{Prefix, Forms, Def, _Defmacro, _Macros} = dynamic_form(Map, nil),
|
||||
CheckerChunk = checker_chunk(Map, Def, chunk_opts(Map)),
|
||||
consolidate(Map, Checker, TypeSpecs, DocsChunk) ->
|
||||
{Prefix, Forms, _Def, _Defmacro, _Macros} = dynamic_form(Map, nil),
|
||||
CheckerChunk = checker_chunk(Checker, chunk_opts(Map)),
|
||||
load_form(Map, Prefix, Forms, TypeSpecs, DocsChunk ++ CheckerChunk).
|
||||
|
||||
%% Dynamic compilation hook, used in regular compiler
|
||||
|
||||
compile(#{module := Module, anno := Anno} = BaseMap) ->
|
||||
compile(#{module := Module, anno := Anno} = BaseMap, Signatures) ->
|
||||
Map =
|
||||
case elixir_erl_compiler:env_compiler_options() of
|
||||
[] -> BaseMap;
|
||||
@@ -161,7 +164,7 @@ compile(#{module := Module, anno := Anno} = BaseMap) ->
|
||||
|
||||
ChunkOpts = chunk_opts(Map),
|
||||
DocsChunk = docs_chunk(Map, Set, Module, Anno, Def, Defmacro, Types, Callbacks, ChunkOpts),
|
||||
CheckerChunk = checker_chunk(Map, Def, ChunkOpts),
|
||||
CheckerChunk = checker_chunk(Map, Def, Signatures, ChunkOpts),
|
||||
load_form(Map, Prefix, Forms, TypeSpecs, DocsChunk ++ CheckerChunk).
|
||||
|
||||
chunk_opts(Map) ->
|
||||
@@ -639,9 +642,13 @@ signature_to_binary(_, Name, Signature) ->
|
||||
Doc = 'Elixir.Inspect.Algebra':format('Elixir.Code':quoted_to_algebra(Quoted), infinity),
|
||||
'Elixir.IO':iodata_to_binary(Doc).
|
||||
|
||||
checker_chunk(Map, Def, ChunkOpts) ->
|
||||
#{deprecated := Deprecated, defines_behaviour := DefinesBehaviour,
|
||||
signatures := Signatures, attributes := Attributes} = Map,
|
||||
checker_chunk(nil, _ChunkOpts) ->
|
||||
[];
|
||||
checker_chunk(Contents, ChunkOpts) ->
|
||||
[{<<"ExCk">>, term_to_binary({checker_version(), Contents}, ChunkOpts)}].
|
||||
|
||||
checker_chunk(Map, Def, Signatures, ChunkOpts) ->
|
||||
#{deprecated := Deprecated, defines_behaviour := DefinesBehaviour, attributes := Attributes} = Map,
|
||||
DeprecatedMap = maps:from_list(Deprecated),
|
||||
|
||||
Exports =
|
||||
@@ -662,7 +669,7 @@ checker_chunk(Map, Def, ChunkOpts) ->
|
||||
end
|
||||
},
|
||||
|
||||
[{<<"ExCk">>, term_to_binary({elixir_checker_v3, Contents}, ChunkOpts)}].
|
||||
checker_chunk(Contents, ChunkOpts).
|
||||
|
||||
prepend_behaviour_info(true, Def) -> [{{behaviour_info, 1}, []} | Def];
|
||||
prepend_behaviour_info(false, Def) -> Def.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -219,14 +219,13 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
|
||||
deprecated => get_deprecated(DataBag),
|
||||
defines_behaviour => defines_behaviour(DataBag),
|
||||
impls => Impls,
|
||||
unreachable => Unreachable,
|
||||
signatures => Signatures
|
||||
unreachable => Unreachable
|
||||
},
|
||||
|
||||
compile_error_if_tainted(DataSet, E),
|
||||
Binary = elixir_erl:compile(ModuleMap),
|
||||
Binary = elixir_erl:compile(ModuleMap, Signatures),
|
||||
Autoload = Forceload or proplists:get_value(autoload, CompileOpts, false),
|
||||
spawn_parallel_checker(CheckerInfo, Module, ModuleMap, BeamLocation),
|
||||
spawn_parallel_checker(CheckerInfo, Module, ModuleMap, Signatures, BeamLocation),
|
||||
{Binary, PersistedAttributes, Autoload}
|
||||
end),
|
||||
|
||||
@@ -581,15 +580,15 @@ checker_info() ->
|
||||
_ -> 'Elixir.Module.ParallelChecker':get()
|
||||
end.
|
||||
|
||||
spawn_parallel_checker({_, nil}, _Module, _ModuleMap, _BeamLocation) ->
|
||||
spawn_parallel_checker({_, nil}, _Module, _ModuleMap, _Signatures, _BeamLocation) ->
|
||||
ok;
|
||||
spawn_parallel_checker(CheckerInfo, Module, ModuleMap, BeamLocation) ->
|
||||
spawn_parallel_checker(CheckerInfo, Module, ModuleMap, Signatures, BeamLocation) ->
|
||||
Log =
|
||||
case erlang:get(elixir_code_diagnostics) of
|
||||
{_, false} -> false;
|
||||
_ -> true
|
||||
end,
|
||||
'Elixir.Module.ParallelChecker':spawn(CheckerInfo, Module, ModuleMap, BeamLocation, Log).
|
||||
'Elixir.Module.ParallelChecker':spawn(CheckerInfo, Module, ModuleMap, Signatures, BeamLocation, Log).
|
||||
|
||||
make_module_available(Module, Binary, Loaded) ->
|
||||
case get(elixir_module_binaries) of
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -876,8 +876,18 @@ handle_unary_op(Rest, Line, Column, Kind, Length, Op, Scope, Tokens) ->
|
||||
Token = {identifier, {Line, Column, nil}, Op},
|
||||
tokenize(Remaining, Line, Column + Length + Extra, Scope, [Token | Tokens]);
|
||||
{Remaining, Extra} ->
|
||||
NewScope =
|
||||
%% TODO: Remove these deprecations on Elixir v2.0
|
||||
case Op of
|
||||
'~~~' ->
|
||||
Msg = "~~~ is deprecated. Use Bitwise.bnot/1 instead for clarity",
|
||||
prepend_warning(Line, Column, Msg, Scope);
|
||||
_ ->
|
||||
Scope
|
||||
end,
|
||||
|
||||
Token = {Kind, {Line, Column, nil}, Op},
|
||||
tokenize(Remaining, Line, Column + Length + Extra, Scope, [Token | Tokens])
|
||||
tokenize(Remaining, Line, Column + Length + Extra, NewScope, [Token | Tokens])
|
||||
end.
|
||||
|
||||
handle_op([$: | Rest], Line, Column, _Kind, Length, Op, Scope, Tokens) when ?is_space(hd(Rest)) ->
|
||||
@@ -897,10 +907,6 @@ handle_op(Rest, Line, Column, Kind, Length, Op, Scope, Tokens) ->
|
||||
Msg = "^^^ is deprecated. It is typically used as xor but it has the wrong precedence, use Bitwise.bxor/2 instead",
|
||||
prepend_warning(Line, Column, Msg, Scope);
|
||||
|
||||
'~~~' ->
|
||||
Msg = "~~~ is deprecated. Use Bitwise.bnot/1 instead for clarity",
|
||||
prepend_warning(Line, Column, Msg, Scope);
|
||||
|
||||
'<|>' ->
|
||||
Msg = "<|> is deprecated. Use another pipe-like operator",
|
||||
prepend_warning(Line, Column, Msg, Scope);
|
||||
|
||||
@@ -127,7 +127,7 @@ change_posix_time(Name, Time) when is_integer(Time) ->
|
||||
change_universal_time(Name, {{Y, M, D}, {H, Min, Sec}}=Time)
|
||||
when is_integer(Y), is_integer(M), is_integer(D),
|
||||
is_integer(H), is_integer(Min), is_integer(Sec) ->
|
||||
file:write_file_info(Name, #file_info{mtime=Time}, [{time, universal}]).
|
||||
file:write_file_info(Name, #file_info{mtime=Time}, [raw, {time, universal}]).
|
||||
|
||||
relative_to_cwd(Path) ->
|
||||
try elixir_config:get(relative_paths) of
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -1484,6 +1484,11 @@ defmodule KernelTest do
|
||||
assert output =~ "3"
|
||||
end
|
||||
|
||||
test "prints the given expression with complex options" do
|
||||
output = capture_io(fn -> assert dbg(123, [] ++ []) == 123 end)
|
||||
assert output =~ "kernel_test.exs"
|
||||
end
|
||||
|
||||
test "doesn't print any colors if :syntax_colors is []" do
|
||||
output =
|
||||
capture_io(fn ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,8 +16,6 @@ defmodule Module.Types.DescrTest do
|
||||
|
||||
import Module.Types.Descr, except: [fun: 1]
|
||||
|
||||
defmacrop domain_key(key), do: {:domain_key, key}
|
||||
|
||||
defp number(), do: union(integer(), float())
|
||||
defp empty_tuple(), do: tuple([])
|
||||
defp tuple_of_size_at_least(n) when is_integer(n), do: open_tuple(List.duplicate(term(), n))
|
||||
@@ -2506,6 +2504,20 @@ defmodule Module.Types.DescrTest do
|
||||
assert subtype?(map2, map1)
|
||||
end
|
||||
|
||||
test "map intersection and then difference" do
|
||||
actual = open_map(__struct__: atom(), __exception__: atom([true]))
|
||||
|
||||
expected =
|
||||
for i <- 1..50 do
|
||||
name = :"name_#{i}"
|
||||
closed_map([__struct__: atom([name])] ++ [{name, binary()}])
|
||||
end
|
||||
|> Enum.reduce(&union/2)
|
||||
|
||||
common = intersection(actual, expected)
|
||||
difference(actual, common)
|
||||
end
|
||||
|
||||
test "struct difference" do
|
||||
entries =
|
||||
[
|
||||
|
||||
@@ -23,10 +23,10 @@ defmodule Module.Types.InferTest do
|
||||
[]
|
||||
)
|
||||
|
||||
{:ok, {_, [debug_info: chunk]}} = :beam_lib.chunks(binary, [:debug_info])
|
||||
{:debug_info_v1, backend, data} = chunk
|
||||
{:ok, %{signatures: signatures}} = backend.debug_info(:elixir_v1, module, data, [])
|
||||
signatures
|
||||
version = :elixir_erl.checker_version()
|
||||
{:ok, {_, [{~c"ExCk", chunk}]}} = :beam_lib.chunks(binary, [~c"ExCk"])
|
||||
{^version, data} = :erlang.binary_to_term(chunk)
|
||||
for {fun, %{sig: sig}} <- data.exports, into: %{}, do: {fun, sig}
|
||||
end
|
||||
|
||||
test "infer types from patterns", config do
|
||||
|
||||
@@ -449,10 +449,9 @@ defmodule Module.Types.IntegrationTest do
|
||||
|
||||
-:hello-
|
||||
|
||||
but the protocol was not yet implemented for any type and therefore will always fail. \
|
||||
This error typically happens within libraries that define protocols and will disappear as \
|
||||
soon as there is one implementation. If you expect the protocol to be implemented later on, \
|
||||
you can define an implementation specific for development/test.
|
||||
but the NoImplProtocol protocol was not yet implemented for any type and therefore will always fail.
|
||||
|
||||
This warning will disappear once you define a implementation. If the protocol is part of a library, you may define a dummy implementation for development/test.
|
||||
|
||||
typing violation found at:
|
||||
│
|
||||
@@ -478,30 +477,6 @@ defmodule Module.Types.IntegrationTest do
|
||||
}
|
||||
|
||||
warnings = [
|
||||
"""
|
||||
warning: incompatible value given to string interpolation:
|
||||
|
||||
data
|
||||
|
||||
it has type:
|
||||
|
||||
-dynamic(%Range{})-
|
||||
|
||||
but expected a type that implements the String.Chars protocol, it must be one of:
|
||||
|
||||
dynamic(
|
||||
%Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or
|
||||
%Version.Requirement{}
|
||||
) or atom() or binary() or empty_list() or float() or integer() or non_empty_list(term(), term())
|
||||
|
||||
where "data" was given the type:
|
||||
|
||||
# type: dynamic(%Range{})
|
||||
# from: a.ex:3:24
|
||||
_.._//_ = data
|
||||
|
||||
hint: string interpolation uses the String.Chars protocol to convert a data structure into a string. Either convert the data type into a string upfront or implement the protocol accordingly
|
||||
""",
|
||||
"""
|
||||
warning: incompatible types given to String.Chars.to_string/1:
|
||||
|
||||
@@ -511,18 +486,47 @@ defmodule Module.Types.IntegrationTest do
|
||||
|
||||
-dynamic(%Range{})-
|
||||
|
||||
but expected a type that implements the String.Chars protocol, it must be one of:
|
||||
but expected a type that implements the String.Chars protocol.
|
||||
You either passed the wrong value or you must:
|
||||
|
||||
dynamic(
|
||||
%Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or
|
||||
%Version.Requirement{}
|
||||
) or atom() or binary() or empty_list() or float() or integer() or non_empty_list(term(), term())
|
||||
1. convert the given value to a string explicitly
|
||||
(use inspect/1 if you want to convert any data structure to a string)
|
||||
2. implement the String.Chars protocol
|
||||
|
||||
where "data" was given the type:
|
||||
|
||||
# type: dynamic(%Range{})
|
||||
# from: a.ex:2:24
|
||||
_.._//_ = data
|
||||
|
||||
hint: the String.Chars protocol is implemented for the following types:
|
||||
|
||||
dynamic(
|
||||
%Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or
|
||||
%Version.Requirement{}
|
||||
) or atom() or binary() or empty_list() or float() or integer() or non_empty_list(term(), term())
|
||||
""",
|
||||
"""
|
||||
warning: incompatible value given to string interpolation:
|
||||
|
||||
data
|
||||
|
||||
it has type:
|
||||
|
||||
-dynamic(%Range{})-
|
||||
|
||||
but expected a type that implements the String.Chars protocol.
|
||||
You either passed the wrong value or you must:
|
||||
|
||||
1. convert the given value to a string explicitly
|
||||
(use inspect/1 if you want to convert any data structure to a string)
|
||||
2. implement the String.Chars protocol
|
||||
|
||||
where "data" was given the type:
|
||||
|
||||
# type: dynamic(%Range{})
|
||||
# from: a.ex:3:24
|
||||
_.._//_ = data
|
||||
"""
|
||||
]
|
||||
|
||||
@@ -551,12 +555,11 @@ defmodule Module.Types.IntegrationTest do
|
||||
|
||||
-dynamic(%Date{})-
|
||||
|
||||
but expected a type that implements the Enumerable protocol, it must be one of:
|
||||
but expected a type that implements the Enumerable protocol.
|
||||
You either passed the wrong value or you must:
|
||||
|
||||
dynamic(
|
||||
%Date.Range{} or %File.Stream{} or %GenEvent.Stream{} or %HashDict{} or %HashSet{} or
|
||||
%IO.Stream{} or %MapSet{} or %Range{} or %Stream{}
|
||||
) or empty_list() or fun() or non_empty_list(term(), term()) or non_struct_map()
|
||||
1. convert the given value to an Enumerable explicitly
|
||||
2. implement the Enumerable protocol
|
||||
|
||||
where "date" was given the type:
|
||||
|
||||
@@ -564,7 +567,12 @@ defmodule Module.Types.IntegrationTest do
|
||||
# from: a.ex:2:24
|
||||
%Date{} = date
|
||||
|
||||
hint: for-comprehensions use the Enumerable protocol to traverse data structures. Either convert the data type into a list (or another Enumerable) or implement the protocol accordingly
|
||||
hint: the Enumerable protocol is implemented for the following types:
|
||||
|
||||
dynamic(
|
||||
%Date.Range{} or %File.Stream{} or %GenEvent.Stream{} or %HashDict{} or %HashSet{} or
|
||||
%IO.Stream{} or %MapSet{} or %Range{} or %Stream{}
|
||||
) or empty_list() or fun() or non_empty_list(term(), term()) or non_struct_map()
|
||||
""",
|
||||
"""
|
||||
warning: incompatible value given to :into option in for-comprehension:
|
||||
@@ -572,6 +580,16 @@ defmodule Module.Types.IntegrationTest do
|
||||
into: Date.utc_today()
|
||||
|
||||
it has type:
|
||||
|
||||
-dynamic(%Date{})-
|
||||
|
||||
but expected a type that implements the Collectable protocol.
|
||||
You either passed the wrong value or you forgot to implement the protocol.
|
||||
|
||||
hint: the Collectable protocol is implemented for the following types:
|
||||
|
||||
dynamic(%File.Stream{} or %HashDict{} or %HashSet{} or %IO.Stream{} or %MapSet{}) or binary() or
|
||||
empty_list() or non_empty_list(term(), term()) or non_struct_map()
|
||||
""",
|
||||
"""
|
||||
warning: incompatible value given to :into option in for-comprehension:
|
||||
@@ -582,12 +600,13 @@ defmodule Module.Types.IntegrationTest do
|
||||
|
||||
-integer()-
|
||||
|
||||
but expected a type that implements the Collectable protocol, it must be one of:
|
||||
but expected a type that implements the Collectable protocol.
|
||||
You either passed the wrong value or you forgot to implement the protocol.
|
||||
|
||||
hint: the Collectable protocol is implemented for the following types:
|
||||
|
||||
dynamic(%File.Stream{} or %HashDict{} or %HashSet{} or %IO.Stream{} or %MapSet{}) or binary() or
|
||||
empty_list() or non_empty_list(term(), term()) or non_struct_map()
|
||||
|
||||
hint: the :into option in for-comprehensions use the Collectable protocol to build its result. Either pass a valid data type or implement the protocol accordingly
|
||||
"""
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -16,6 +16,10 @@ tokenize_error(String) ->
|
||||
{error, Error, _, _, _} = elixir_tokenizer:tokenize(String, 1, []),
|
||||
Error.
|
||||
|
||||
tokenize_warnings(String) ->
|
||||
{ok, _Line, _Column, Warnings, Result, []} = elixir_tokenizer:tokenize(String, 1, []),
|
||||
{lists:reverse(Result), Warnings}.
|
||||
|
||||
type_test() ->
|
||||
[{int, {1, 1, 1}, "1"},
|
||||
{type_op, {1, 3, nil}, '::'},
|
||||
@@ -279,3 +283,13 @@ sigil_heredoc_test() ->
|
||||
invalid_sigil_delimiter_test() ->
|
||||
{[{line, 1}, {column, 1}], "invalid sigil delimiter: ", Message} = tokenize_error("~s\\"),
|
||||
true = lists:prefix("\"\\\" (column 3, code point U+005C)", lists:flatten(Message)).
|
||||
|
||||
deprecated_operators_test() ->
|
||||
{
|
||||
[{xor_op, {1, 1, nil}, '^^^'}, {int, {1, 4, 1}, "1"}],
|
||||
[{{1, 1}, "^^^ is deprecated. It is typically used as xor but it has the wrong precedence, use Bitwise.bxor/2 instead"}]
|
||||
} = tokenize_warnings("^^^1"),
|
||||
{
|
||||
[{unary_op, {1, 1, nil}, '~~~'}, {int, {1, 4, 1}, "1"}],
|
||||
[{{1, 1}, "~~~ is deprecated. Use Bitwise.bnot/1 instead for clarity"}]
|
||||
} = tokenize_warnings("~~~1").
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -70,15 +70,15 @@ defmodule ExUnit.CaptureLogTest do
|
||||
end)
|
||||
|
||||
assert logged
|
||||
assert logged =~ "[info] one\n"
|
||||
assert logged =~ "[warning] two\n"
|
||||
assert logged =~ "[debug] three\n"
|
||||
assert logged =~ "[error] one\n"
|
||||
assert logged =~ "[info] one"
|
||||
assert logged =~ "[warning] two"
|
||||
assert logged =~ "[debug] three"
|
||||
assert logged =~ "[error] one"
|
||||
|
||||
receive do
|
||||
{:nested, logged} ->
|
||||
assert logged =~ "[error] one\n"
|
||||
refute logged =~ "[warning] two\n"
|
||||
assert logged =~ "[error] one"
|
||||
refute logged =~ "[warning] two"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -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]), []}]
|
||||
|
||||
|
||||
@@ -375,9 +375,9 @@ defmodule ExUnitTest do
|
||||
end
|
||||
|
||||
output = capture_io(&ExUnit.run/0)
|
||||
refute output =~ "[debug] one\n"
|
||||
assert output =~ "[debug] two\n"
|
||||
assert output =~ "[debug] three\n"
|
||||
refute output =~ "[debug] one"
|
||||
assert output =~ "[debug] two"
|
||||
assert output =~ "[debug] three"
|
||||
end
|
||||
|
||||
test "supports multi errors" 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
|
||||
|
||||
|
||||
@@ -126,10 +126,6 @@ defmodule Logger do
|
||||
|
||||
* `:registered_name` - the process registered name as an atom
|
||||
|
||||
* `:process_label` - (available from Erlang/OTP 27+) an arbitrary term
|
||||
which can be added to a process with `Process.set_label/1` for
|
||||
debugging purposes
|
||||
|
||||
* `:domain` - a list of domains for the logged message. For example,
|
||||
all Elixir reports default to `[:elixir]`. Erlang reports may start
|
||||
with `[:otp]` or `[:sasl]`
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -244,13 +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)
|
||||
[IO.ANSI.format_fragment(color, true), data | IO.ANSI.reset()]
|
||||
[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
|
||||
|
||||
@@ -97,6 +97,46 @@ defmodule Logger.FormatterTest do
|
||||
|> format(formatter)
|
||||
|> IO.chardata_to_string() =~ ~r"\d\d\d message"
|
||||
end
|
||||
|
||||
test "handles colorize with newlines" do
|
||||
{_, formatter} =
|
||||
new(
|
||||
format: "$message",
|
||||
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"
|
||||
|
||||
assert %{level: :warning, msg: {:string, "message\n"}, meta: %{}}
|
||||
|> format(formatter)
|
||||
|> IO.chardata_to_string() == "\e[33mmessage\e[0m\n"
|
||||
|
||||
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
|
||||
|
||||
describe "compile + format" do
|
||||
|
||||
@@ -228,7 +228,7 @@ defmodule Logger.TranslatorTest do
|
||||
\*\* \(RuntimeError\) oops
|
||||
.*
|
||||
Process Label: {:any, \"term\"}
|
||||
Last message \(from #PID<\d+\.\d+\.\d+>\): :error
|
||||
Last message \(from #PID<\d+\.\d+\.\d+>\): :error\
|
||||
"""s
|
||||
end
|
||||
|
||||
@@ -301,7 +301,7 @@ defmodule Logger.TranslatorTest do
|
||||
.*
|
||||
Last message \(from #PID<\d+\.\d+\.\d+>\): :error_on_down
|
||||
State: :ok
|
||||
Client #PID<\d+\.\d+\.\d+> is dead
|
||||
Client #PID<\d+\.\d+\.\d+> is dead\
|
||||
"""s
|
||||
end
|
||||
|
||||
@@ -317,7 +317,7 @@ defmodule Logger.TranslatorTest do
|
||||
\*\* \(RuntimeError\) oops
|
||||
.*
|
||||
Last message: {:"\$gen_cast", :error}
|
||||
State: :ok
|
||||
State: :ok\
|
||||
"""s
|
||||
end
|
||||
|
||||
@@ -384,7 +384,7 @@ defmodule Logger.TranslatorTest do
|
||||
\*\* \(RuntimeError\) oops
|
||||
.*
|
||||
Process Label: {:any, "term"}
|
||||
Last message: :error
|
||||
Last message: :error\
|
||||
"""s
|
||||
end
|
||||
|
||||
@@ -399,7 +399,7 @@ defmodule Logger.TranslatorTest do
|
||||
\*\* \(RuntimeError\) oops
|
||||
.*
|
||||
Last message: :error
|
||||
State: :ok
|
||||
State: :ok\
|
||||
"""s
|
||||
|
||||
assert_receive {:event, {:report, %{label: {:gen_event, :terminate}}}, metadata}
|
||||
@@ -464,7 +464,7 @@ defmodule Logger.TranslatorTest do
|
||||
.*
|
||||
Process Label: {:any, \"term\"}
|
||||
Queue: .*
|
||||
Postponed: \[\]
|
||||
Postponed: \[\]\
|
||||
"""s
|
||||
end
|
||||
|
||||
@@ -543,7 +543,7 @@ defmodule Logger.TranslatorTest do
|
||||
Postponed: \[\]
|
||||
State: {:started, :ok}
|
||||
Callback mode: :state_functions, state_enter: false
|
||||
Client #PID<\d+\.\d+\.\d+> is dead
|
||||
Client #PID<\d+\.\d+\.\d+> is dead\
|
||||
"""s
|
||||
end
|
||||
|
||||
@@ -561,7 +561,7 @@ defmodule Logger.TranslatorTest do
|
||||
Queue: .*
|
||||
Postponed: \[\]
|
||||
State: {:started, :ok}
|
||||
Callback mode: :state_functions, state_enter: false
|
||||
Callback mode: :state_functions, state_enter: false\
|
||||
"""s
|
||||
end
|
||||
|
||||
@@ -619,7 +619,7 @@ defmodule Logger.TranslatorTest do
|
||||
\*\* \(RuntimeError\) oops
|
||||
.*
|
||||
Function: &Logger.TranslatorTest.task\/1
|
||||
Args: \[#PID<\d+\.\d+\.\d+>\]
|
||||
Args: \[#PID<\d+\.\d+\.\d+>\]\
|
||||
"""s
|
||||
|
||||
assert_receive {:event, {:report, %{label: {Task.Supervisor, :terminating}}}, task_metadata}
|
||||
@@ -640,7 +640,7 @@ defmodule Logger.TranslatorTest do
|
||||
\*\* \(UndefinedFunctionError\) function :module_does_not_exist.undef/0 is undefined \(module :module_does_not_exist is not available\)
|
||||
.*
|
||||
Function: &:module_does_not_exist.undef/0
|
||||
Args: \[\]
|
||||
Args: \[\]\
|
||||
"""s
|
||||
|
||||
assert_receive {:event, {:report, %{label: {Task.Supervisor, :terminating}}}, task_metadata}
|
||||
@@ -660,7 +660,7 @@ defmodule Logger.TranslatorTest do
|
||||
\*\* \(UndefinedFunctionError\) function Logger.TranslatorTest.undef/0 is undefined or private
|
||||
.*
|
||||
Function: &Logger.TranslatorTest.undef/0
|
||||
Args: \[\]
|
||||
Args: \[\]\
|
||||
"""s
|
||||
|
||||
assert_receive {:event, {:report, %{label: {Task.Supervisor, :terminating}}}, task_metadata}
|
||||
@@ -688,7 +688,7 @@ defmodule Logger.TranslatorTest do
|
||||
\*\* \(ErlangError\) Erlang error: :foo
|
||||
.*
|
||||
Function: &:erlang\.error/1
|
||||
Args: \[%ErlangError{.*}\]
|
||||
Args: \[%ErlangError{.*}\]\
|
||||
"""s
|
||||
|
||||
assert_receive {:event, {:report, %{label: {Task.Supervisor, :terminating}}}, task_metadata}
|
||||
@@ -708,7 +708,7 @@ defmodule Logger.TranslatorTest do
|
||||
\*\* \(ArgumentError\) argument error
|
||||
.*
|
||||
Function: &:erlang\.error/1
|
||||
Args: \[:badarg\]
|
||||
Args: \[:badarg\]\
|
||||
"""s
|
||||
|
||||
assert_receive {:event, {:report, %{label: {Task.Supervisor, :terminating}}}, task_metadata}
|
||||
@@ -728,7 +728,7 @@ defmodule Logger.TranslatorTest do
|
||||
\*\* \(stop\) :abnormal
|
||||
.*
|
||||
Function: &:erlang\.exit/1
|
||||
Args: \[:abnormal\]
|
||||
Args: \[:abnormal\]\
|
||||
"""s
|
||||
|
||||
assert_receive {:event, {:report, %{label: {Task.Supervisor, :terminating}}}, task_metadata}
|
||||
@@ -755,26 +755,22 @@ defmodule Logger.TranslatorTest do
|
||||
.*
|
||||
Process Label: {:any, \"term\"}
|
||||
Function: &Logger.TranslatorTest.task\/2
|
||||
Args: \[#PID<\d+\.\d+\.\d+>\, .*]
|
||||
Args: \[#PID<\d+\.\d+\.\d+>\, .*]\
|
||||
"""s
|
||||
end
|
||||
|
||||
test "translates application start" do
|
||||
assert capture_log(fn ->
|
||||
Application.start(:eex)
|
||||
:ok = Application.start(:eex)
|
||||
Application.stop(:eex)
|
||||
end) =~ """
|
||||
Application eex started at #{inspect(node())}
|
||||
"""
|
||||
end) =~ "Application eex started at #{inspect(node())}"
|
||||
end
|
||||
|
||||
test "translates application stop" do
|
||||
assert capture_log(fn ->
|
||||
:ok = Application.start(:eex)
|
||||
Application.stop(:eex)
|
||||
end) =~ """
|
||||
Application eex exited: :stopped
|
||||
"""
|
||||
end) =~ "Application eex exited: :stopped"
|
||||
end
|
||||
|
||||
test "translates bare process crashes" do
|
||||
@@ -810,7 +806,7 @@ defmodule Logger.TranslatorTest do
|
||||
\*\* \(RuntimeError\) oops
|
||||
.*
|
||||
Initial Call: Logger.TranslatorTest.task/2
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>\]
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>\]\
|
||||
"""s
|
||||
|
||||
assert_receive {:event, {:report, %{label: {:proc_lib, :crash}}}, process_metadata}
|
||||
@@ -854,7 +850,7 @@ defmodule Logger.TranslatorTest do
|
||||
\*\* \(RuntimeError\) oops
|
||||
.*
|
||||
Initial Call: Logger.TranslatorTest.task/2
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>\]
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>\]\
|
||||
"""s
|
||||
|
||||
assert_receive {:event, {:report, %{label: {:proc_lib, :crash}}}, process_metadata}
|
||||
@@ -881,7 +877,7 @@ defmodule Logger.TranslatorTest do
|
||||
.*
|
||||
Initial Call: Logger.TranslatorTest.task/2
|
||||
Process Label: {:any, \"term\"}
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>\]
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>\]\
|
||||
"""s
|
||||
|
||||
assert_receive {:event, {:report, %{label: {:proc_lib, :crash}}}, process_metadata}
|
||||
@@ -905,7 +901,7 @@ defmodule Logger.TranslatorTest do
|
||||
\[error\] Process #PID<\d+\.\d+\.\d+> terminating
|
||||
\*\* \(RuntimeError\) oops
|
||||
.*
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>\]
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>\]\
|
||||
"""s
|
||||
|
||||
assert_receive {:event, {:report, %{label: {:proc_lib, :crash}}}, process_metadata}
|
||||
@@ -926,7 +922,7 @@ defmodule Logger.TranslatorTest do
|
||||
#PID<\d+\.\d+\.\d+>
|
||||
Initial Call: Logger.TranslatorTest.sleep/1
|
||||
Current Call: Logger.TranslatorTest.sleep/1
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>, #PID<\d+\.\d+\.\d+>\]
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>, #PID<\d+\.\d+\.\d+>\]\
|
||||
"""
|
||||
end
|
||||
|
||||
@@ -949,7 +945,7 @@ defmodule Logger.TranslatorTest do
|
||||
Logger.TranslatorTest \(#PID<\d+\.\d+\.\d+>\)
|
||||
Initial Call: Logger.TranslatorTest.sleep/1
|
||||
Current Call: Logger.TranslatorTest.sleep/1
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>, #PID<\d+\.\d+\.\d+>\]
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>, #PID<\d+\.\d+\.\d+>\]\
|
||||
"""
|
||||
end
|
||||
|
||||
@@ -1016,7 +1012,7 @@ defmodule Logger.TranslatorTest do
|
||||
Start Call: Task.start_link\(Logger.TranslatorTest, :sleep, \[#PID<\d+\.\d+\.\d+>\]\)
|
||||
Restart: :permanent
|
||||
Shutdown: 5000
|
||||
Type: :worker
|
||||
Type: :worker\
|
||||
"""
|
||||
end
|
||||
|
||||
@@ -1031,7 +1027,7 @@ defmodule Logger.TranslatorTest do
|
||||
end) =~ ~r"""
|
||||
\[error\] Child Logger.TranslatorTest of Supervisor #PID<\d+\.\d+\.\d+> \(Supervisor\.Default\) failed to start
|
||||
\*\* \(exit\) :stop
|
||||
Start Call: Logger.TranslatorTest.error\(\)
|
||||
Start Call: Logger.TranslatorTest.error\(\)\
|
||||
"""
|
||||
end
|
||||
|
||||
@@ -1048,7 +1044,7 @@ defmodule Logger.TranslatorTest do
|
||||
\*\* \(exit\) an exception was raised:
|
||||
\*\* \(UndefinedFunctionError\) function Logger.TranslatorTest.undef/0 is undefined or private
|
||||
.*
|
||||
Start Call: Logger.TranslatorTest.undef\(\)
|
||||
Start Call: Logger.TranslatorTest.undef\(\)\
|
||||
"""s
|
||||
|
||||
assert_receive {:event, {:report, %{label: {:supervisor, :start_error}}}, _child_metadata}
|
||||
@@ -1065,7 +1061,7 @@ defmodule Logger.TranslatorTest do
|
||||
\[error\] Child Task of Supervisor #PID<\d+\.\d+\.\d+> \(Supervisor\.Default\) terminated
|
||||
\*\* \(exit\) :stop
|
||||
Pid: #PID<\d+\.\d+\.\d+>
|
||||
Start Call: Task.start_link\(Kernel, :exit, \[:stop\]\)
|
||||
Start Call: Task.start_link\(Kernel, :exit, \[:stop\]\)\
|
||||
"""
|
||||
|
||||
assert_receive {:event, {:report, %{label: {Task.Supervisor, :terminating}}}, task_metadata}
|
||||
@@ -1087,7 +1083,7 @@ defmodule Logger.TranslatorTest do
|
||||
end) =~ ~r"""
|
||||
\[error\] Child Task of Supervisor #PID<\d+\.\d+\.\d+> \(Supervisor\.Default\) caused shutdown
|
||||
\*\* \(exit\) :reached_max_restart_intensity
|
||||
Start Call: Task.start_link\(Kernel, :exit, \[:stop\]\)
|
||||
Start Call: Task.start_link\(Kernel, :exit, \[:stop\]\)\
|
||||
"""
|
||||
|
||||
assert_receive {:event, {:report, %{label: {Task.Supervisor, :terminating}}}, task_metadata}
|
||||
@@ -1108,7 +1104,7 @@ defmodule Logger.TranslatorTest do
|
||||
\[error\] Child Logger.TranslatorTest of Supervisor #PID<\d+\.\d+\.\d+> \(Supervisor\.Default\) shut down abnormally
|
||||
\*\* \(exit\) :stop
|
||||
Pid: #PID<\d+\.\d+\.\d+>
|
||||
Start Call: Logger.TranslatorTest.abnormal\(\)
|
||||
Start Call: Logger.TranslatorTest.abnormal\(\)\
|
||||
"""
|
||||
|
||||
assert_receive {:event, {:report, %{label: {:supervisor, :shutdown_error}}}, _child_metadata}
|
||||
@@ -1128,7 +1124,7 @@ defmodule Logger.TranslatorTest do
|
||||
Start Call: Logger.TranslatorTest.abnormal\(\)
|
||||
Restart: :permanent
|
||||
Shutdown: 5000
|
||||
Type: :worker
|
||||
Type: :worker\
|
||||
"""
|
||||
|
||||
assert_receive {:event, {:report, %{label: {:supervisor, :shutdown_error}}}, _child_metadata}
|
||||
@@ -1147,7 +1143,7 @@ defmodule Logger.TranslatorTest do
|
||||
\[error\] Child :undefined of Supervisor #PID<\d+\.\d+\.\d+> \(Supervisor\.Default\) shut down abnormally
|
||||
\*\* \(exit\) :stop
|
||||
Pid: #PID<\d+\.\d+\.\d+>
|
||||
Start Call: Logger.TranslatorTest.abnormal\(\)
|
||||
Start Call: Logger.TranslatorTest.abnormal\(\)\
|
||||
"""
|
||||
|
||||
assert_receive {:event, {:report, %{label: {:supervisor, :shutdown_error}}}, _child_metadata}
|
||||
@@ -1168,7 +1164,7 @@ defmodule Logger.TranslatorTest do
|
||||
\[error\] Child :undefined of Supervisor #PID<\d+\.\d+\.\d+> \(Supervisor\.Default\) shut down abnormally
|
||||
\*\* \(exit\) :stop
|
||||
Pid: #PID<\d+\.\d+\.\d+>
|
||||
Start Call: Logger.TranslatorTest.abnormal\(:extra, :args\)
|
||||
Start Call: Logger.TranslatorTest.abnormal\(:extra, :args\)\
|
||||
"""
|
||||
|
||||
assert_receive {:event, {:report, %{label: {:supervisor, :shutdown_error}}}, _child_metadata}
|
||||
@@ -1187,7 +1183,7 @@ defmodule Logger.TranslatorTest do
|
||||
\[error\] Child :undefined of Supervisor Logger.TranslatorTest shut down abnormally
|
||||
\*\* \(exit\) :stop
|
||||
Pid: #PID<\d+\.\d+\.\d+>
|
||||
Start Call: Logger.TranslatorTest.abnormal\(\)
|
||||
Start Call: Logger.TranslatorTest.abnormal\(\)\
|
||||
"""
|
||||
|
||||
assert_receive {:event, {:report, %{label: {:supervisor, :shutdown_error}}}, _child_metadata}
|
||||
@@ -1202,7 +1198,7 @@ defmodule Logger.TranslatorTest do
|
||||
end) =~ ~r"""
|
||||
\[info\] Child of Supervisor #PID<\d+\.\d+\.\d+> \(Logger\.TranslatorTest\.MyBridge\) started
|
||||
Pid: #PID<\d+\.\d+\.\d+>
|
||||
Start Call: Logger.TranslatorTest.MyBridge.init\(:normal\)
|
||||
Start Call: Logger.TranslatorTest.MyBridge.init\(:normal\)\
|
||||
"""
|
||||
end
|
||||
|
||||
@@ -1216,7 +1212,7 @@ defmodule Logger.TranslatorTest do
|
||||
\[error\] Child of Supervisor #PID<\d+\.\d+\.\d+> \(Logger\.TranslatorTest\.MyBridge\) terminated
|
||||
\*\* \(exit\) :stop
|
||||
Pid: #PID<\d+\.\d+\.\d+>
|
||||
Start Module: Logger.TranslatorTest.MyBridge
|
||||
Start Module: Logger.TranslatorTest.MyBridge\
|
||||
"""
|
||||
|
||||
assert_receive {:event, {:report, %{label: {:proc_lib, :crash}}}, task_metadata}
|
||||
|
||||
+10
-12
@@ -8,18 +8,18 @@ defmodule Mix.CLI do
|
||||
@doc """
|
||||
Runs Mix according to the command line arguments.
|
||||
"""
|
||||
def main(args \\ System.argv()) do
|
||||
def main(args \\ System.argv(), mix_exs \\ System.get_env("MIX_EXS") || "mix.exs") do
|
||||
if env_variable_activated?("MIX_DEBUG") do
|
||||
IO.puts("-> Running mix CLI")
|
||||
{time, res} = :timer.tc(&main/2, [args, true])
|
||||
{time, res} = :timer.tc(&main/3, [args, mix_exs, true])
|
||||
IO.puts(["<- Ran mix CLI in ", Integer.to_string(div(time, 1000)), "ms"])
|
||||
res
|
||||
else
|
||||
main(args, false)
|
||||
main(args, mix_exs, false)
|
||||
end
|
||||
end
|
||||
|
||||
defp main(args, debug?) do
|
||||
defp main(args, mix_exs, debug?) do
|
||||
Mix.start()
|
||||
|
||||
if debug?, do: Mix.debug(true)
|
||||
@@ -46,13 +46,13 @@ defmodule Mix.CLI do
|
||||
display_version()
|
||||
|
||||
nil ->
|
||||
proceed(args)
|
||||
proceed(args, mix_exs)
|
||||
end
|
||||
end
|
||||
|
||||
defp proceed(args) do
|
||||
defp proceed(args, mix_exs) do
|
||||
load_dot_config()
|
||||
load_mix_exs(args)
|
||||
load_mix_exs(args, mix_exs)
|
||||
project = Mix.Project.get()
|
||||
{task, args} = get_task(args, project)
|
||||
ensure_hex(task)
|
||||
@@ -60,14 +60,12 @@ defmodule Mix.CLI do
|
||||
run_task(task, args)
|
||||
end
|
||||
|
||||
defp load_mix_exs(args) do
|
||||
file = System.get_env("MIX_EXS") || "mix.exs"
|
||||
|
||||
if File.regular?(file) do
|
||||
defp load_mix_exs(args, mix_exs) do
|
||||
if is_binary(mix_exs) and File.regular?(mix_exs) do
|
||||
Mix.ProjectStack.post_config(state_loader: {:cli, List.first(args)})
|
||||
old_undefined = Code.get_compiler_option(:no_warn_undefined)
|
||||
Code.put_compiler_option(:no_warn_undefined, :all)
|
||||
Code.compile_file(file)
|
||||
Code.compile_file(mix_exs)
|
||||
Code.put_compiler_option(:no_warn_undefined, old_undefined)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
defmodule Mix.Compilers.Elixir do
|
||||
@moduledoc false
|
||||
|
||||
@manifest_vsn 28
|
||||
@manifest_vsn 29
|
||||
@checkpoint_vsn 4
|
||||
|
||||
import Record
|
||||
@@ -49,8 +49,8 @@ defmodule Mix.Compilers.Elixir do
|
||||
timestamp = System.os_time(:second)
|
||||
all_paths = Mix.Utils.extract_files(srcs, [:ex])
|
||||
|
||||
{all_modules, all_sources, all_local_exports, old_parents, old_cache_key, old_deps_config,
|
||||
old_project_mtime, old_config_mtime, old_protocols_and_impls} =
|
||||
{all_modules, all_sources, all_local_exports, old_parents, old_cache_key, old_cwd,
|
||||
old_deps_config, old_project_mtime, old_config_mtime, old_protocols_and_impls} =
|
||||
parse_manifest(manifest, dest)
|
||||
|
||||
# In case we aborted in the middle of a verification,
|
||||
@@ -100,7 +100,8 @@ defmodule Mix.Compilers.Elixir do
|
||||
|
||||
{force?, stale, new_deps_config} =
|
||||
cond do
|
||||
!!opts[:force] or is_nil(old_deps_config) or old_cache_key != new_cache_key ->
|
||||
!!opts[:force] or is_nil(old_deps_config) or old_cache_key != new_cache_key or
|
||||
(Keyword.get(opts, :check_cwd, true) and old_cwd != File.cwd!()) ->
|
||||
{true, stale, deps_config(local_deps)}
|
||||
|
||||
deps_changed? or compile_env_apps != [] ->
|
||||
@@ -286,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
|
||||
[]
|
||||
@@ -313,7 +320,7 @@ defmodule Mix.Compilers.Elixir do
|
||||
rescue
|
||||
_ -> {[], []}
|
||||
else
|
||||
{@manifest_vsn, modules, sources, _, _, _, _, _, _, _} -> {modules, sources}
|
||||
{@manifest_vsn, modules, sources, _, _, _, _, _, _, _, _} -> {modules, sources}
|
||||
_ -> {[], []}
|
||||
end
|
||||
end
|
||||
@@ -432,8 +439,9 @@ defmodule Mix.Compilers.Elixir do
|
||||
has_any_key?(modules_to_recompile, modules) ->
|
||||
# Mark the source as changed so the combination of a timestamp
|
||||
# plus removed beam files (which are removed by update_stale_entries)
|
||||
# causes it to be recompiled
|
||||
File.touch!(source, timestamp + 1)
|
||||
# causes it to be recompiled. Note we don't raise use touch! because
|
||||
# in case of checkpoints the file may have been removed.
|
||||
File.touch(source, timestamp + 1)
|
||||
[source]
|
||||
|
||||
size != last_size or
|
||||
@@ -880,7 +888,7 @@ defmodule Mix.Compilers.Elixir do
|
||||
|
||||
## Manifest handling
|
||||
|
||||
@default_manifest {%{}, %{}, %{}, [], nil, nil, 0, 0, {%{}, %{}}}
|
||||
@default_manifest {%{}, %{}, %{}, [], nil, nil, nil, 0, 0, {%{}, %{}}}
|
||||
|
||||
# Similar to read_manifest, but for internal consumption and with data migration support.
|
||||
defp parse_manifest(manifest, compile_path) do
|
||||
@@ -890,9 +898,9 @@ defmodule Mix.Compilers.Elixir do
|
||||
_ ->
|
||||
@default_manifest
|
||||
else
|
||||
{@manifest_vsn, modules, sources, local_exports, parent, cache_key, deps_config,
|
||||
{@manifest_vsn, modules, sources, local_exports, parent, cache_key, cwd, deps_config,
|
||||
project_mtime, config_mtime, protocols_and_impls} ->
|
||||
{modules, sources, local_exports, parent, cache_key, deps_config, project_mtime,
|
||||
{modules, sources, local_exports, parent, cache_key, cwd, deps_config, project_mtime,
|
||||
config_mtime, protocols_and_impls}
|
||||
|
||||
# {vsn, %{module => record}, sources, ...} v22-?
|
||||
@@ -951,8 +959,8 @@ defmodule Mix.Compilers.Elixir do
|
||||
File.mkdir_p!(Path.dirname(manifest))
|
||||
|
||||
term =
|
||||
{@manifest_vsn, modules, sources, exports, parents, cache_key, deps_config, project_mtime,
|
||||
config_mtime, protocols_and_impls}
|
||||
{@manifest_vsn, modules, sources, exports, parents, cache_key, File.cwd!(), deps_config,
|
||||
project_mtime, config_mtime, protocols_and_impls}
|
||||
|
||||
manifest_data = :erlang.term_to_binary(term, [:compressed])
|
||||
File.write!(manifest, manifest_data)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -767,7 +767,16 @@ defmodule Mix.Project do
|
||||
"""
|
||||
@spec build_path(keyword) :: Path.t()
|
||||
def build_path(config \\ config()) do
|
||||
System.get_env("MIX_BUILD_PATH") || config[:deps_build_path] || do_build_path(config)
|
||||
cond do
|
||||
deps_build_path = config[:deps_build_path] ->
|
||||
deps_build_path
|
||||
|
||||
build_path = System.get_env("MIX_BUILD_PATH") ->
|
||||
Path.expand(build_path)
|
||||
|
||||
true ->
|
||||
do_build_path(config)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_build_path(config) do
|
||||
|
||||
+35
-38
@@ -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}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/*"])
|
||||
|
||||
|
||||
@@ -187,11 +187,22 @@ defmodule Mix.Tasks.Compile.App do
|
||||
|> add_compile_env(current_properties)
|
||||
|> add_modules(modules, compile_path)
|
||||
|
||||
contents = :io_lib.format("~p.~n", [{: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, IO.chardata_to_string(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
|
||||
|
||||
@@ -71,7 +71,11 @@ defmodule Mix.Tasks.Compile.Elixir do
|
||||
* `--long-verification-threshold N` - sets the "long verification" threshold
|
||||
(in seconds) to `N` (see the docs for `Kernel.ParallelCompiler.compile/2`)
|
||||
* `--no-verification` - disables code verification, such as unused functions,
|
||||
deprecation warnings, and type checking. It must be used alongside `MIX_DEBUG=1`
|
||||
deprecation warnings, and type checking. It must be used solely for debugging
|
||||
alongside `MIX_DEBUG=1`
|
||||
* `--no-check-cwd` - (since v1.19.2) Elixir stores absolute paths in .beam files, which means
|
||||
that relocating the project root triggers a full build. Pass this option if you don't want
|
||||
the current working directory to be checked
|
||||
* `--purge-consolidation-path-if-stale PATH` - deletes and purges modules in the
|
||||
given protocol consolidation path if compilation is required
|
||||
* `--profile` - if set to `time`, outputs timing information of compilation steps
|
||||
@@ -109,7 +113,8 @@ defmodule Mix.Tasks.Compile.Elixir do
|
||||
profile: :string,
|
||||
all_warnings: :boolean,
|
||||
verification: :boolean,
|
||||
tracer: :keep
|
||||
tracer: :keep,
|
||||
check_cwd: :boolean
|
||||
]
|
||||
|
||||
@impl true
|
||||
@@ -121,13 +126,13 @@ defmodule Mix.Tasks.Compile.Elixir do
|
||||
dest = Mix.Project.compile_path(project)
|
||||
srcs = project[:elixirc_paths]
|
||||
|
||||
if not is_list(srcs) do
|
||||
Mix.raise(":elixirc_paths should be a list of paths, got: #{inspect(srcs)}")
|
||||
if not (is_list(srcs) and Enum.all?(srcs, &is_binary/1)) do
|
||||
Mix.shell().error(":elixirc_paths should be a list of string paths, got: #{inspect(srcs)}")
|
||||
end
|
||||
|
||||
manifest = manifest()
|
||||
base = xref_exclude_opts(project[:elixirc_options] || [], project)
|
||||
cache_key = {base, srcs, File.cwd!(), "--no-optional-deps" in args}
|
||||
cache_key = {base, srcs, "--no-optional-deps" in args}
|
||||
|
||||
opts =
|
||||
base
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -102,16 +102,20 @@ defmodule Mix.Tasks.Deps.Compile do
|
||||
|
||||
count = System.get_env("MIX_OS_DEPS_COMPILE_PARTITION_COUNT", "0") |> String.to_integer()
|
||||
|
||||
compiled? =
|
||||
if count > 1 and length(deps) > 1 do
|
||||
Mix.shell().info("mix deps.compile running across #{count} OS processes")
|
||||
Mix.Tasks.Deps.Partition.server(deps, count, force?)
|
||||
else
|
||||
config = Mix.Project.deps_config()
|
||||
true in Enum.map(deps, &compile_single(&1, force?, config))
|
||||
end
|
||||
# If all dependencies are local and ok, do not bother starting
|
||||
# processes as it will likely slow everything down.
|
||||
if count > 1 and match?([_, _ | _], deps) and
|
||||
not Enum.all?(deps, &(Mix.Dep.ok?(&1) and not &1.scm.fetchable?())) do
|
||||
Mix.shell().info("mix deps.compile running across #{count} OS processes")
|
||||
|
||||
if compiled?, do: Mix.Task.run("will_recompile"), else: :ok
|
||||
if Mix.Tasks.Deps.Partition.server(deps, count, force?) do
|
||||
# Each partition will trigger will_recompile but we need to trigger it here too
|
||||
Mix.Task.run("will_recompile")
|
||||
end
|
||||
else
|
||||
config = Mix.Project.deps_config()
|
||||
Enum.each(deps, &compile_single(&1, force?, config))
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -166,8 +170,14 @@ defmodule Mix.Tasks.Deps.Compile do
|
||||
# We should touch fetchable dependencies even if they
|
||||
# did not compile otherwise they will always be marked
|
||||
# as stale, even when there is nothing to do.
|
||||
fetchable? = touch_fetchable(scm, opts[:build])
|
||||
compiled? and fetchable?
|
||||
fetchable? = touch_fetchable(scm, opts)
|
||||
|
||||
if compiled? and fetchable? do
|
||||
Mix.Task.run("will_recompile")
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp check_unavailable!(app, scm, {:unavailable, path}) do
|
||||
@@ -189,9 +199,9 @@ defmodule Mix.Tasks.Deps.Compile do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp touch_fetchable(scm, path) do
|
||||
defp touch_fetchable(scm, opts) do
|
||||
if scm.fetchable?() do
|
||||
Mix.Dep.ElixirSCM.update(Path.join(path, ".mix"), scm)
|
||||
Mix.Dep.ElixirSCM.update(Path.join(opts[:build], ".mix"), scm)
|
||||
true
|
||||
else
|
||||
false
|
||||
|
||||
@@ -141,7 +141,11 @@ defmodule Mix.Tasks.Deps.Loadpaths do
|
||||
defp partition([dep | deps], not_ok, compile) do
|
||||
cond do
|
||||
Mix.Dep.compilable?(dep) or (Mix.Dep.ok?(dep) and local?(dep)) ->
|
||||
partition(deps, not_ok, [dep | compile])
|
||||
if from_umbrella?(dep) do
|
||||
partition(deps, not_ok, compile)
|
||||
else
|
||||
partition(deps, not_ok, [dep | compile])
|
||||
end
|
||||
|
||||
Mix.Dep.ok?(dep) ->
|
||||
partition(deps, not_ok, compile)
|
||||
@@ -155,6 +159,14 @@ defmodule Mix.Tasks.Deps.Loadpaths do
|
||||
{Enum.reverse(not_ok), Enum.reverse(compile)}
|
||||
end
|
||||
|
||||
# Children from the umbrella are compiled sequentially by the umbrella project itself.
|
||||
# We could start compiling them as dependencies, and therefore in parallel, but because
|
||||
# `mix compile` in an umbrella is expected to return the diagnostics of all children,
|
||||
# we would need a pass after compilation to compite diagnostics.
|
||||
defp from_umbrella?(dep) do
|
||||
dep.opts[:from_umbrella]
|
||||
end
|
||||
|
||||
defp reload_deps(deps) do
|
||||
deps
|
||||
|> Enum.map(& &1.app)
|
||||
|
||||
@@ -7,8 +7,6 @@ defmodule Mix.Tasks.Deps.Partition do
|
||||
|
||||
## Server
|
||||
|
||||
@deps_partition_install_mix_exs ~c"deps.partition.mix.exs"
|
||||
|
||||
def server(deps, count, force?) do
|
||||
{:ok, socket} = :gen_tcp.listen(0, [:binary, packet: :line, active: false])
|
||||
|
||||
@@ -29,41 +27,18 @@ defmodule Mix.Tasks.Deps.Partition do
|
||||
ansi_flag = if IO.ANSI.enabled?(), do: ~c"--color", else: ~c"--no-color"
|
||||
force_flag = if force?, do: ~c"--force", else: ~c"--no-force"
|
||||
|
||||
env_vars =
|
||||
if Mix.install?() do
|
||||
blob =
|
||||
Mix.Project.config()
|
||||
|> :erlang.term_to_binary()
|
||||
|> :binary.bin_to_list()
|
||||
|> Enum.join(",")
|
||||
|
||||
# We replicate the initialization logic from Mix.install/2 as part of mix.exs
|
||||
File.write!(@deps_partition_install_mix_exs, """
|
||||
config = <<#{blob}>>
|
||||
project = :erlang.binary_to_term(config)
|
||||
|
||||
if compile_config = project[:compile_config] do
|
||||
Application.put_all_env(compile_config, persistent: true)
|
||||
end
|
||||
|
||||
Mix.ProjectStack.push(Mix.InstallProject, project, "nofile")
|
||||
""")
|
||||
|
||||
[{~c"MIX_EXS", @deps_partition_install_mix_exs}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
args = [
|
||||
ansi_flag,
|
||||
~c"-e",
|
||||
~c"Mix.CLI.main",
|
||||
~c"deps.partition",
|
||||
~c"Mix.Tasks.Deps.Partition.client",
|
||||
~c"--",
|
||||
force_flag,
|
||||
~c"--port",
|
||||
Integer.to_charlist(port),
|
||||
~c"--host",
|
||||
~c"127.0.0.1"
|
||||
~c"127.0.0.1",
|
||||
~c"--config",
|
||||
Mix.ProjectStack.peek() |> :erlang.term_to_binary() |> Base.url_encode64()
|
||||
]
|
||||
|
||||
options = [
|
||||
@@ -77,7 +52,6 @@ defmodule Mix.Tasks.Deps.Partition do
|
||||
{~c"MIX_OS_CONCURRENCY_LOCK", ~c"false"},
|
||||
{~c"MIX_ENV", Atom.to_charlist(Mix.env())},
|
||||
{~c"MIX_TARGET", Atom.to_charlist(Mix.target())}
|
||||
| env_vars
|
||||
]
|
||||
]
|
||||
|
||||
@@ -241,16 +215,36 @@ defmodule Mix.Tasks.Deps.Partition do
|
||||
|
||||
## Client
|
||||
|
||||
@switches [port: :integer, host: :string, force: :boolean, index: :string]
|
||||
@switches [port: :integer, host: :string, force: :boolean, index: :string, config: :string]
|
||||
|
||||
@impl true
|
||||
def run(args) do
|
||||
def client do
|
||||
# If stdin closes, we shutdown the VM
|
||||
spawn(fn ->
|
||||
_ = IO.gets("")
|
||||
System.halt(0)
|
||||
end)
|
||||
|
||||
args = System.argv()
|
||||
{opts, []} = OptionParser.parse!(args, strict: @switches)
|
||||
peek = Keyword.fetch!(opts, :config) |> Base.url_decode64!() |> :erlang.binary_to_term()
|
||||
|
||||
# This is specific to Mix.install/2 and how it handles compile-time config
|
||||
if compile_config = peek.config[:compile_config] do
|
||||
Application.put_all_env(compile_config, persistent: true)
|
||||
end
|
||||
|
||||
partition_args =
|
||||
opts
|
||||
|> Keyword.take([:host, :port, :index, :force])
|
||||
|> OptionParser.to_argv()
|
||||
|
||||
Mix.start()
|
||||
Mix.ProjectStack.push(peek.name, peek.config, peek.file)
|
||||
Mix.CLI.main(["deps.partition" | partition_args], nil)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def run(args) do
|
||||
{opts, []} = OptionParser.parse!(args, strict: @switches)
|
||||
host = Keyword.fetch!(opts, :host)
|
||||
port = Keyword.fetch!(opts, :port)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ defmodule Mix.Tasks.Format do
|
||||
to be used by this task. For example, `["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]`.
|
||||
Patterns are expanded with `Path.wildcard/2`.
|
||||
|
||||
* `:excludes` (a list of paths and patterns) - specifies the files to exclude from the
|
||||
* `:excludes` (a list of paths and patterns) (since v1.19.0) - specifies the files to exclude from the
|
||||
list of inputs to this task. For example, `["config/runtime.exs", "test/**/*.{ex,exs}"]`.
|
||||
Patterns are expanded with `Path.wildcard/2`.
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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("")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -46,7 +46,7 @@ defmodule Mix.ProjectTest do
|
||||
|
||||
test "considers MIX_BUILD_PATH" do
|
||||
System.put_env("MIX_BUILD_PATH", "_build")
|
||||
assert Mix.Project.build_path() == "_build"
|
||||
assert Mix.Project.build_path() == Path.join(File.cwd!(), "_build")
|
||||
after
|
||||
System.delete_env("MIX_BUILD_PATH")
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,24 +7,6 @@ Code.require_file("../../test_helper.exs", __DIR__)
|
||||
defmodule Mix.Tasks.Compile.AppTest do
|
||||
use MixTest.Case
|
||||
|
||||
defmodule CustomProject do
|
||||
def project do
|
||||
[
|
||||
app: :custom_project,
|
||||
version: "0.2.0",
|
||||
description: "Some UTF-8 dëscriptión"
|
||||
]
|
||||
end
|
||||
|
||||
def application do
|
||||
[
|
||||
maxT: :infinity,
|
||||
applications: [:example_app, mix: :optional],
|
||||
extra_applications: [:logger, ex_unit: :optional]
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defmodule CustomDeps do
|
||||
def project do
|
||||
[app: :custom_deps, version: "0.2.0", deps: deps()]
|
||||
@@ -51,9 +33,13 @@ defmodule Mix.Tasks.Compile.AppTest do
|
||||
end
|
||||
end
|
||||
|
||||
defmodule InvalidProject do
|
||||
defmodule CustomProject do
|
||||
def project do
|
||||
[app: :invalid_project, version: "0.3.0"]
|
||||
[
|
||||
app: :custom_project,
|
||||
version: "0.3.0",
|
||||
description: "Some UTF-8 dëscriptión"
|
||||
]
|
||||
end
|
||||
|
||||
def application do
|
||||
@@ -124,13 +110,21 @@ 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, "฿", &List.flatten/1], bar: [{} | %{foo: :bar}]]
|
||||
|
||||
Process.put(:application,
|
||||
maxT: :infinity,
|
||||
applications: [:example_app, mix: :optional],
|
||||
extra_applications: [:logger, ex_unit: :optional],
|
||||
env: env
|
||||
)
|
||||
|
||||
Mix.Tasks.Compile.Elixir.run([])
|
||||
Mix.Tasks.Compile.App.run([])
|
||||
|
||||
properties = parse_resource_file(:custom_project)
|
||||
assert Application.spec(:custom_project, :vsn) == ~c"0.2.0"
|
||||
assert properties[:vsn] == ~c"0.2.0"
|
||||
assert Application.spec(:custom_project, :vsn) == ~c"0.3.0"
|
||||
assert properties[:vsn] == ~c"0.3.0"
|
||||
assert properties[:maxT] == :infinity
|
||||
assert properties[:optional_applications] == [:ex_unit, :mix]
|
||||
assert properties[:description] == ~c"Some UTF-8 dëscriptión"
|
||||
@@ -138,6 +132,11 @@ 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, "฿", &List.flatten/1],
|
||||
bar: [{} | %{foo: :bar}]
|
||||
]
|
||||
|
||||
refute Keyword.has_key?(properties, :extra_applications)
|
||||
end)
|
||||
end
|
||||
@@ -160,7 +159,7 @@ defmodule Mix.Tasks.Compile.AppTest do
|
||||
|
||||
test "validates properties" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(InvalidProject)
|
||||
Mix.Project.push(CustomProject)
|
||||
|
||||
Process.put(:application, [:not_a_keyword, applications: []])
|
||||
message = "Application configuration returned from application/0 should be a keyword list"
|
||||
@@ -252,6 +251,34 @@ defmodule Mix.Tasks.Compile.AppTest do
|
||||
assert_raise Mix.Error, message, fn ->
|
||||
Mix.Tasks.Compile.App.run([])
|
||||
end
|
||||
|
||||
Process.put(:application, env: [foo: make_ref()])
|
||||
|
||||
message =
|
||||
~r"\"def application\" has a term which cannot be written to \.app files: #Reference"
|
||||
|
||||
assert_raise Mix.Error, message, fn ->
|
||||
Mix.Tasks.Compile.App.run([])
|
||||
end
|
||||
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
|
||||
|
||||
|
||||
@@ -118,6 +118,26 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "does not recompiles project if cwd changes and --no-check-cwd is given" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(MixTest.Case.Sample)
|
||||
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
Mix.Project.pop()
|
||||
|
||||
old_cwd = File.cwd!()
|
||||
|
||||
in_tmp("new_cwd", fn ->
|
||||
Mix.Project.push(MixTest.Case.Sample)
|
||||
File.cp_r!(old_cwd, File.cwd!())
|
||||
|
||||
purge([A, B])
|
||||
Mix.Task.clear()
|
||||
assert {:noop, _} = Mix.Tasks.Compile.Elixir.run(["--verbose", "--no-check-cwd"])
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
test "recompiles files using Mix.Project if mix.exs changes" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(MixTest.Case.Sample, __ENV__.file)
|
||||
@@ -258,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"]}
|
||||
@@ -269,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
|
||||
@@ -684,9 +735,6 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
Mix.State.clear_cache()
|
||||
purge([GitRepo.MixProject, PathOnGitRepo.MixProject, PathOnGitRepo.Hello])
|
||||
|
||||
# Unload the git repo application so we can pick the new modules definition
|
||||
:ok = Application.unload(:git_repo)
|
||||
|
||||
Mix.Tasks.Deps.Update.run(["--all"])
|
||||
assert File.read!("mix.lock") =~ last
|
||||
|
||||
@@ -1236,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)
|
||||
|
||||
@@ -341,48 +341,67 @@ defmodule Mix.Tasks.EscriptTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "escript.install from Git" do
|
||||
in_fixture("git_repo", fn ->
|
||||
File.mkdir_p!("config")
|
||||
for count <- [1, 2] do
|
||||
test "escript.install from Git (with MIX_OS_DEPS_COMPILE_PARTITION_COUNT=#{count})" do
|
||||
System.put_env("MIX_OS_DEPS_COMPILE_PARTITION_COUNT", "#{unquote(count)}")
|
||||
|
||||
File.write!("config/config.exs", """
|
||||
import Config
|
||||
config :git_repo, :escript_config, true
|
||||
""")
|
||||
in_fixture("deps_status", fn ->
|
||||
File.mkdir_p!("config")
|
||||
File.mkdir_p!("lib")
|
||||
|
||||
File.write!("lib/git_repo.ex", """
|
||||
require Application
|
||||
true = Application.compile_env!(:git_repo, :escript_config)
|
||||
File.write!("config/config.exs", """
|
||||
import Config
|
||||
config :source_repo, :escript_config, true
|
||||
""")
|
||||
|
||||
defmodule GitRepo.Escript do
|
||||
def main(_argv) do
|
||||
IO.puts("TEST")
|
||||
File.write!("lib/source_repo.ex", """
|
||||
require Application
|
||||
true = Application.compile_env!(:source_repo, :escript_config)
|
||||
|
||||
defmodule SourceRepo.Escript do
|
||||
def main(_argv) do
|
||||
IO.puts("TEST")
|
||||
end
|
||||
end
|
||||
end
|
||||
""")
|
||||
""")
|
||||
|
||||
File.write!("mix.exs", """
|
||||
defmodule GitRepo.MixProject do
|
||||
use Mix.Project
|
||||
File.write!("mix.exs", """
|
||||
defmodule SourceRepo.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[app: :git_repo, version: "0.1.0", escript: [main_module: GitRepo.Escript]]
|
||||
def project do
|
||||
[
|
||||
app: :source_repo,
|
||||
version: "0.1.0",
|
||||
escript: [main_module: SourceRepo.Escript],
|
||||
deps: [
|
||||
{:git_repo, path: "#{MixTest.Case.fixture_path("git_repo")}"},
|
||||
{:git_rebar, path: "#{MixTest.Case.fixture_path("git_rebar")}", manager: :rebar3},
|
||||
{:ok, path: "deps/ok"},
|
||||
]
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
""")
|
||||
""")
|
||||
|
||||
System.cmd("git", ~w[add .])
|
||||
System.cmd("git", ~w[commit -m "ok"])
|
||||
System.cmd("git", ~w[init])
|
||||
System.cmd("git", ~w[add .])
|
||||
System.cmd("git", ~w[commit -m "ok"])
|
||||
|
||||
send(self(), {:mix_shell_input, :yes?, true})
|
||||
Mix.Tasks.Escript.Install.run(["git", File.cwd!()])
|
||||
assert_received {:mix_shell, :info, ["Generated escript git_repo with MIX_ENV=prod"]}
|
||||
ExUnit.CaptureIO.capture_io(fn ->
|
||||
send(self(), {:mix_shell_input, :yes?, true})
|
||||
Mix.Tasks.Escript.Install.run(["git", File.cwd!()])
|
||||
end)
|
||||
|
||||
escript_path = Path.join([tmp_path(".mix"), "escripts", "git_repo"])
|
||||
assert System.cmd("escript", [escript_path]) == {"TEST\n", 0}
|
||||
end)
|
||||
after
|
||||
purge([GitRepo.Escript, GitRepo.MixProject, Mix.Local.Installer.MixProject])
|
||||
assert_received {:mix_shell, :info, ["Generated escript source_repo with MIX_ENV=prod"]}
|
||||
|
||||
escript_path = Path.join([tmp_path(".mix"), "escripts", "source_repo"])
|
||||
assert System.cmd("escript", [escript_path]) == {"TEST\n", 0}
|
||||
end)
|
||||
after
|
||||
System.delete_env("MIX_OS_DEPS_COMPILE_PARTITION_COUNT")
|
||||
purge([SourceRepo.Escript, SourceRepo.MixProject, Mix.Local.Installer.MixProject])
|
||||
end
|
||||
end
|
||||
|
||||
defp push_project_with_config(module, config \\ []) do
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -66,7 +66,7 @@ defmodule Mix.UmbrellaTest do
|
||||
# Ensure we can compile and run checks
|
||||
Mix.Task.run("deps.compile")
|
||||
Mix.Task.run("deps.loadpaths")
|
||||
Mix.Task.run("compile", ["--verbose"])
|
||||
Mix.Task.run("compile")
|
||||
|
||||
# Extra applications are picked even for umbrellas
|
||||
assert :code.where_is_file(~c"runtime_tools.app") != :non_existing
|
||||
@@ -83,6 +83,13 @@ defmodule Mix.UmbrellaTest do
|
||||
assert_received {:mix_shell, :info, [":foo env is dev"]}
|
||||
assert_received {:mix_shell, :info, [":bar env is dev"]}
|
||||
|
||||
# Ensure we can compile --force
|
||||
Mix.Task.clear()
|
||||
Mix.Task.run("compile", ["--force"])
|
||||
assert_received {:mix_shell, :info, ["Generated foo app"]}
|
||||
assert_received {:mix_shell, :info, ["Generated bar app"]}
|
||||
|
||||
# Ensure we can start even with --no-compile
|
||||
Mix.Task.clear()
|
||||
Mix.Task.run("app.start", ["--no-compile"])
|
||||
end)
|
||||
@@ -282,23 +289,6 @@ defmodule Mix.UmbrellaTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "compile for umbrella as dependency with os partitions" do
|
||||
System.put_env("MIX_OS_DEPS_COMPILE_PARTITION_COUNT", "2")
|
||||
|
||||
in_fixture("umbrella_dep", fn ->
|
||||
Mix.Project.in_project(:umbrella_dep, ".", fn _ ->
|
||||
output = ExUnit.CaptureIO.capture_io(fn -> Mix.Task.run("compile") end)
|
||||
assert output =~ ~r/\d> :foo env is prod/
|
||||
assert output =~ ~r/\d> :bar env is prod/
|
||||
|
||||
assert_received {:mix_shell, :info, ["mix deps.compile running across 2 OS processes"]}
|
||||
assert Bar.bar() == "hello world"
|
||||
end)
|
||||
end)
|
||||
after
|
||||
System.delete_env("MIX_OS_DEPS_COMPILE_PARTITION_COUNT")
|
||||
end
|
||||
|
||||
defmodule CycleDeps do
|
||||
def project do
|
||||
[
|
||||
|
||||
@@ -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"))
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user