Compare commits

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

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

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

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

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

This reverts commit a7c008f2cd.

* Add regression test case
2025-12-04 18:06:12 +09:00
José Valim 003ef335e7 Fix #iex:break as part of multi-line prompts, closes #14992 2025-12-04 00:10:07 +01:00
José Valim 028cdbb91e Support fetching abstract code for modules compiled with Elixir v1.14 and earlier, closes #14987 2025-12-03 20:00:37 +01:00
José Valim 794441a255 Release v1.19.4 2025-11-27 16:50:26 +01:00
Eksperimental 727106beb4 Fix typos (#14951) 2025-11-27 16:49:21 +01:00
Wojtek Mach 5e45871b5e mix help app:APP: compile current Mix project (if any) (#14958)
Prior to this patch, it was impossible to get help for the current app:

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

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

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

Now it works:

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

    NimbleCSV is a small and fast parsing and dumping library.

    # NimbleCSV.RFC4180

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

    # NimbleCSV.Spreadsheet

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

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

* Add regression test for IO.inspect/2 opts
2025-11-20 19:41:01 +09:00
José Valim ee1dc0729f Ensure parallel matches in assert propagate type information, closes #14921 2025-11-18 15:20:17 -08:00
José Valim 32e724c87c Ensure logger formatter works with chardata, closes #14921 2025-11-18 14:50:50 -08:00
José Valim 7758084211 Add --min-cycle-label to mix xref graph 2025-11-15 06:36:31 -08:00
Jean Klingler 57d3098c2a Fix since in doc for E regex modifier (#14909) 2025-11-14 07:48:38 +09:00
José Valim 427d21e410 Release v1.19.3 2025-11-13 17:33:43 +01:00
José Valim 24a188a016 Share consulting logic between compile.app and release 2025-11-13 16:11:15 +01:00
Jean Klingler 457d52c89b Add 'E' modifier to Regex for :export option (#14907) 2025-11-13 16:11:15 +01:00
José Valim 6684dbd3f2 Simplify format stacktrace handling 2025-11-13 13:10:59 +01:00
Łukasz Samson ccb2a742ad Fix crash when format_kind_reason is executed with TestModule (#14901)
Closes #14900.
2025-11-13 13:10:58 +01:00
Jean Klingler 92094c9ef1 Write ascii printable charlists as such in .app file (#14906) 2025-11-13 18:42:54 +09:00
Wojtek Mach d4d5da2ccf mix help app:APP: Always try loading apps and support Erlang apps (#14894) 2025-11-08 12:23:04 +01:00
José Valim 666c082105 &Mod.fun/arity are valid app terms, closes #14891 2025-11-08 12:23:03 +01:00
José Valim d2c9d39efe Allow forcing specific compilers 2025-11-08 11:47:24 +01:00
Jean Klingler 97d16038fe Fix how UTF8 env is stored in .app file (#14890) 2025-11-04 16:21:08 +09:00
José Valim 87cbf06ff1 Release v1.19.2 2025-11-02 11:50:21 +01:00
José Valim caf39255a4 Ensure relative MIX_BUILD_PATH is discarded on deps
Closes #14875.
2025-11-02 10:17:18 +01:00
José Valim 6440fb90a3 Ensure mix escript.install works with deps.partition, closes #14885 2025-11-02 09:50:18 +01:00
José Valim 8793c68822 Remove line breaks from deprecations table 2025-10-30 21:59:52 +01:00
Gonzalo R 49ac038d67 docs(logger): :process_label is not really ever included in the metadata as of now (#14880) 2025-10-30 17:48:20 +01:00
Leslieandwolfgang 3827b73d2d Fix struct update example in guide (#14877)
Co-authored-by: wolfgang <wolfgang.yin@gmail.com>
2025-10-30 22:53:07 +09:00
José Valim dc8a19028f Commit test changes to MIX_BUILD_PATH 2025-10-30 08:27:25 +01:00
José Valim 357c346a9f Ensure MIX_BUILD_PATH is expanded 2025-10-30 08:13:48 +01:00
Jean Klingler 6df9eb338c Document since for --no-check-cwd + fix typo (#14874) 2025-10-30 07:39:03 +09:00
José Valim fa85b95250 Pass the scm when touching compilation manifest for deps 2025-10-29 23:28:40 +01:00
José Valim a89655c434 Ensure latest version of applications are loaded after compilation 2025-10-29 14:35:23 +01:00
Jean Klingler 730fd0f824 Warn on deprecated ~~~ unary op in tokenizer (#14870) 2025-10-29 19:27:48 +09:00
José Valim 29d3b0cf3a Rely on term_to_binary/1 format to protect from incomplete file writes 2025-10-28 20:27:22 +01:00
José Valim ae94e6eb25 Add checksum to manifest files
Closes #14866.
2025-10-28 12:01:03 +01:00
José Valim 661e537cda Add --no-check-pwd to mix compile 2025-10-28 12:01:02 +01:00
José Valim 4a5c076b7f Warn if elixirc_paths is not a list of string paths, closes #14862 2025-10-28 08:11:04 +01:00
Jean Klingler 297bc8302b Fix dialyzer warning on IO.inspect :label (#14865) 2025-10-28 16:06:40 +09:00
José Valim f6413152ff Ensure flags are passed down to umbrella children
Closes #14852.
Closes #14860.
2025-10-26 13:26:45 +01:00
José Valim c2edbaf173 Raise on invalid .app files (#14861)
Since regular expressions now have regexes,
they can end-up in .app files, and then apps
can no longer boot.

This changes makes it so they fail loudly.
2025-10-24 23:56:06 +02:00
José Valim ac9a659bc6 Reset ansi escapes before newlines in Logger
Closes #14855
Closes #14841
2025-10-24 12:19:22 +02:00
José Valim cc5837caa7 Pass signatures on the side instead of module map 2025-10-23 00:18:29 +02:00
José Valim 1738ed453f Do not include signatures twice in beam files 2025-10-23 00:01:32 +02:00
José Valim 3e18aafa0a Measure and optimize writing of .beam files in the compiler
This should provide meaningful benefits when working with
containers and network mounted drives.
2025-10-22 10:47:03 +02:00
José Valim bcb73a468b Clarify CHANGELOG, closes #14844 2025-10-21 14:26:48 +02:00
José Valim 613f631487 Simplify always true and always false bdd nodes 2025-10-21 12:51:11 +02:00
José Valim f08a84af10 Make bdd functions public to aid debugging 2025-10-21 12:40:29 +02:00
José Valim b1a7afd061 Release v1.19.1 2025-10-20 17:30:38 +02:00
Travis Vander Hoop e9bf5df25a Update documentation for formatter's :excludes option (#14842) 2025-10-18 20:35:08 +02:00
José Valim 2e7bb9b47f Do not spawn partitions when all dependencies are local and ok, closes #14843 2025-10-18 19:07:28 +02:00
José Valim 83c68cecc1 Simplify computation of mismatched parts for error messages
Before we were doing:

    common = intersection(actual, expected)
    uncommon = difference(actual, common)

But the second clause is:

    actual and not (actual and expected)

Which is literally the same as:

    actual and not expected

But much faster as it avoids the large nesting of BDDs.

Closes #14836.
2025-10-18 18:54:39 +02:00
José Valim 184b724483 More optimizations for differences
* when a1 < a2
* when a1 == a2 and c2 == bottom and d2 == bottom
* when a1 == a2 and u2 == bottom
2025-10-18 18:33:18 +02:00
José Valim 47fea9580a Perform expensive operation once 2025-10-18 18:32:44 +02:00
José Valim 6a6678f337 Do not attempt to touch deleted files 2025-10-18 18:32:32 +02:00
José Valim ec87c6b111 Improve protocol violation warnings 2025-10-18 18:32:28 +02:00
José Valim 0c595489b6 Improve protocol type error to list possible root causes 2025-10-18 18:32:22 +02:00
Eric Meadows-Jönsson cdbfa09edb Fix hex upload of Elixir build without -otp- suffix (#14840)
OTP 25 is no longer supported so we didn't upload a generic build.
Instead find the oldest version instead of hardcoding it.
2025-10-17 22:21:14 +02:00
José Valim baa25991fa Do not escape dbg options, closes #14839 2025-10-17 22:15:22 +02:00
Daniil Kulchenko a5c9121a7c Fix EEx.compile_string passing invalid options to tokenize (#14835)
EEx.compile_string/2 was passing all options to tokenize/2, but
tokenize/2 only accepts tokenize_opt (:file, :line, :column,
:indentation, :trim). This caused dialyzer to correctly flag calls
with :engine or :parser_options as type errors in Elixir 1.19+.

The fix filters options before passing to tokenize/2, keeping only
the valid tokenize_opt keys, while still passing the full options
list to EEx.Compiler.compile/3 which needs :engine, :parser_options,
and custom engine options.

Fixes #14834
2025-10-16 23:49:53 +02:00
91 changed files with 1735 additions and 694 deletions
+6 -3
View File
@@ -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
View File
@@ -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
View File
@@ -1 +1 @@
1.19.0
1.19.6
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -1090,7 +1090,7 @@ defmodule Code do
@doc since: "1.6.0"
@spec format_string!(binary, [format_opt]) :: iodata
def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do
line_length = Keyword.get(opts, :line_length, 98)
{line_length, opts} = Keyword.pop(opts, :line_length, 98)
to_quoted_opts =
[
+9 -3
View File
@@ -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
+26 -5
View File
@@ -389,13 +389,20 @@ defimpl Inspect, for: List do
close = color_doc("]", :list, opts)
cond do
lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
inspected =
case Identifier.escape(IO.chardata_to_string(term), ?", printable_limit) do
{escaped, ""} -> [?~, ?c, ?", escaped, ?"]
{escaped, _} -> [?~, ?c, ?", escaped, ?", " ++ ..."]
(lists == :as_charlists and unicode_list?(term, printable_limit)) or
(lists == :infer and List.ascii_printable?(term, printable_limit)) ->
{split, tail} =
if is_integer(printable_limit) do
case Enum.split(term, printable_limit) do
{split, []} -> {split, []}
{split, _} -> {split, " ++ ..."}
end
else
{term, []}
end
{escaped, _} = Identifier.escape(IO.chardata_to_string(split), ?")
inspected = [?~, ?c, ?", escaped, ?" | tail]
color_doc(IO.iodata_to_binary(inspected), :charlist, opts)
keyword?(term) ->
@@ -409,6 +416,19 @@ defimpl Inspect, for: List do
end
end
defp unicode_list?(_, 0), do: true
defp unicode_list?([char | rest], counter)
when char in 0..0xD7FF or char in 0xE000..0x10FFFF,
do: unicode_list?(rest, decrement(counter))
defp unicode_list?([], _counter), do: true
defp unicode_list?(_, _counter), do: false
@compile {:inline, decrement: 1}
defp decrement(:infinity), do: :infinity
defp decrement(counter), do: counter - 1
@doc false
def keyword({key, value}, opts) do
key = color_doc(Macro.inspect_atom(:key, key), :atom, opts)
@@ -569,6 +589,7 @@ defimpl Inspect, for: Regex do
defp translate_options([:firstline | t], acc), do: translate_options(t, [?f | acc])
defp translate_options([:ungreedy | t], acc), do: translate_options(t, [?U | acc])
defp translate_options([:multiline | t], acc), do: translate_options(t, [?m | acc])
defp translate_options([:export | t], acc), do: translate_options(t, [?E | acc])
defp translate_options([], acc), do: acc
defp translate_options(_t, _acc), do: :error
+2 -2
View File
@@ -21,8 +21,8 @@ defmodule Inspect.Opts do
is `:decimal` and if it is printable, otherwise in bit syntax. See
`String.printable?/1` to learn when a string is printable.
* `:charlists` - when `:as_charlists` all lists will be printed as charlists,
non-printable elements will be escaped.
* `:charlists` - when `:as_charlists` all charlists will be printed as charlists,
non-printable code points will be escaped. Other lists will be printed as lists.
When `:as_lists` all lists will be printed as lists.
+2 -1
View File
@@ -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)
+58 -12
View File
@@ -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
+2 -2
View File
@@ -1137,7 +1137,7 @@ defmodule List do
Please check the given list or call inspect/1 to get the list representation, got:
#{inspect(list)}
#{inspect(list, charlists: :as_lists)}
"""
else
result when is_binary(result) ->
@@ -1189,7 +1189,7 @@ defmodule List do
Please check the given list or call inspect/1 to get the list representation, got:
#{inspect(list)}
#{inspect(list, charlists: :as_lists)}
"""
else
result when is_list(result) ->
+4 -3
View File
@@ -959,8 +959,9 @@ defmodule Macro do
backwards compatibility purposes.
In future releases, Elixir may introduce truly required struct
fields, and therefore only one of required or default will be
present. Your code should prepare for such scenario accordingly.
fields, the required field will be removed and default will be
present only if the field is optional. Your code should prepare
for such scenario accordingly.
"""
@doc since: "1.18.0"
@spec struct_info!(module(), Macro.Env.t()) ::
@@ -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} =
+8 -17
View File
@@ -402,30 +402,21 @@ defmodule Module do
Accepts the function name (as an atom) of a function in the current module.
The function must have an arity of 0 (no arguments). If the function does
not return `:ok`, the loading of the module will be aborted.
For example:
not return `:ok`, the loading of the module will be aborted. Its primary
use case is to load [NIFs](https://www.erlang.org/doc/man/erl_nif):
defmodule MyModule do
@on_load :load_check
@on_load :load_external_code
def load_check do
if some_condition() do
:ok
else
:abort
end
end
def some_condition do
false
def load_external_code do
:erlang.load_nif(~c"path/to/extension.so_or_dll")
end
end
The function given to `on_load` should avoid calling functions from
other modules. If you must call functions in other modules and those
modules are defined within the same project, the called modules must
have the `@compile {:autoload, true}` annotation, so they are loaded
upfront (and not from within the `@on_load` callback).
other modules. This is because, when running a `mix release`,
`on_load` runs extremely early, before any application starts running,
and therefore even systems like the `Logger` and `IO` are not yet available.
### `@vsn`
+15 -12
View File
@@ -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
+66 -24
View File
@@ -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()
+106 -61
View File
@@ -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
-23
View File
@@ -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 ->
"""
+15 -4
View File
@@ -141,14 +141,25 @@ defmodule Module.Types.Of do
{Any, term()}
]
@doc """
Currently, for protocol implementations, we only store
the open struct definition. This is because we don't want
to reconsolidate whenever the struct changes, but at the
moment we can't store references either. Ideally struct
types on protocol dispatches would be lazily resolved.
"""
def impl(for, mode \\ :closed)
for {for, type} <- impls do
def impl(unquote(for)), do: unquote(Macro.escape(type))
def impl(unquote(for), _mode), do: unquote(Macro.escape(type))
end
def impl(struct) do
# Elixir did not strictly require the implementation to be available, so we need a fallback.
def impl(struct, mode) do
# Elixir did not strictly require the implementation to be available,
# so we need to deal with such cases accordingly.
# TODO: Assume implementation is available on Elixir v2.0.
if info = Code.ensure_loaded?(struct) && struct.__info__(:struct) do
# A warning is emitted since v1.19+.
if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do
struct_type(struct, info)
else
open_map(__struct__: atom([struct]))
+74 -43
View File
@@ -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
+3 -2
View File
@@ -27,8 +27,9 @@ defmodule Record do
## Types
Types can be defined for tuples with the `record/2` macro (only available in
typespecs). This macro will expand to a tuple as seen in the example below:
Types can be defined for tuples with the `record/2` construct (which is only
available in typespecs), with the record name as an atom and a keyword list
of fields and their types as argument:
defmodule MyModule do
require Record
+32 -5
View File
@@ -101,6 +101,13 @@ defmodule Regex do
* `:ungreedy` (U) - inverts the "greediness" of the regexp
(the previous `r` option is deprecated in favor of `U`)
* `:export` (E) (since Elixir 1.19.3) - uses an exported pattern
which can be shared across nodes or through config, at the cost of a runtime
overhead every time to re-import it every time it is executed.
This modifier only has an effect starting on Erlang/OTP 28, and it is ignored
on older versions (i.e. `~r/foo/E == ~r/foo/`). This is because patterns cannot
and do not need to be exported in order to be shared in these versions.
## Captures
Many functions in this module handle what to capture in a regex
@@ -251,7 +258,7 @@ defmodule Regex do
This checks the version stored in the regular expression
and recompiles the regex in case of version mismatch.
"""
# Remove me on Elixir v1.22
# TODO: Deprecate on Elixir v1.22
@doc deprecated: "It can be removed and it has no effect"
@doc since: "1.4.0"
def recompile(%Regex{} = regex) do
@@ -261,7 +268,7 @@ defmodule Regex do
@doc """
Recompiles the existing regular expression and raises `Regex.CompileError` in case of errors.
"""
# Remove me on Elixir v1.22
# TODO: Deprecate on Elixir v1.22
@doc deprecated: "It can be removed and it has no effect"
@doc since: "1.4.0"
def recompile!(regex) do
@@ -271,7 +278,7 @@ defmodule Regex do
@doc """
Returns the version of the underlying Regex engine.
"""
# Remove me on Elixir v1.22
# TODO: Deprecate on Elixir v1.22
@doc deprecated: "Use :re.version() instead"
@doc since: "1.4.0"
def version do
@@ -515,7 +522,7 @@ defmodule Regex do
"""
@spec names(t) :: [String.t()]
def names(%Regex{re_pattern: re_pattern}) do
{:namelist, names} = :re.inspect(re_pattern, :namelist)
{:namelist, names} = :re.inspect(maybe_import_pattern(re_pattern), :namelist)
names
end
@@ -585,10 +592,17 @@ defmodule Regex do
%Regex{source: source, opts: compile_opts} = regex
:re.run(string, source, compile_opts ++ options)
else
_ -> :re.run(string, re_pattern, options)
_ -> :re.run(string, maybe_import_pattern(re_pattern), options)
end
end
@compile {:inline, maybe_import_pattern: 1}
@compile {:no_warn_undefined, {:re, :import, 1}}
defp maybe_import_pattern({:re_exported_pattern, _, _, _, _} = exported),
do: :re.import(exported)
defp maybe_import_pattern(pattern), do: pattern
@typedoc """
Options for regex functions that capture matches.
"""
@@ -1007,6 +1021,16 @@ defmodule Regex do
translate_options(t, [:ungreedy | acc])
end
defp translate_options(<<?E, t::binary>>, acc) do
# on OTP 27-, the E modifier is a no-op since the feature doesn't exist but isn't needed
# (regexes aren't using references and can be shared across nodes or stored in config)
# TODO: remove this check on Erlang/OTP 28+ and update docs
case Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) do
true -> translate_options(t, [:export | acc])
false -> translate_options(t, acc)
end
end
defp translate_options(<<>>, acc), do: acc
defp translate_options(t, _acc), do: {:error, t}
@@ -1022,6 +1046,9 @@ defmodule Regex do
:erlang.system_info(:otp_release) < [?2, ?8] ->
Macro.escape(regex.re_pattern)
:lists.member(:export, regex.opts) ->
Macro.escape(regex.re_pattern)
# OTP 28.1+ introduced the ability to export and import regexes from compiled binaries
Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) ->
{:ok, exported} = :re.compile(regex.source, [:export] ++ regex.opts)
+22 -22
View File
@@ -964,17 +964,13 @@ defmodule Stream do
after_fun.(user_acc)
:erlang.raise(kind, reason, __STACKTRACE__)
else
{:suspended, [val], next} ->
do_transform_user(val, user_acc, :cont, next, inner_acc, funs)
{:suspended, vals, next} ->
do_transform_user(:lists.reverse(vals), user_acc, :cont, next, inner_acc, funs)
{_, result} ->
{_, vals} ->
# Do not attempt to call the resource again, it has either done or halted
next = fn _ -> {:done, []} end
case result do
[val] -> do_transform_user(val, user_acc, :last, next, inner_acc, funs)
[] -> do_transform(user_acc, :last, next, inner_acc, funs)
end
do_transform_user(:lists.reverse(vals), user_acc, :last, next, inner_acc, funs)
end
end
@@ -989,7 +985,7 @@ defmodule Stream do
after_fun.(user_acc)
:erlang.raise(kind, reason, __STACKTRACE__)
else
result -> do_transform_result(result, :halt, next, inner_acc, funs)
result -> do_transform_result(result, [], :halt, next, inner_acc, funs)
end
else
do_transform(user_acc, :halt, next, inner_acc, funs)
@@ -1002,7 +998,11 @@ defmodule Stream do
{:halted, elem(inner_acc, 1)}
end
defp do_transform_user(val, user_acc, next_op, next, inner_acc, funs) do
defp do_transform_user([], user_acc, next_op, next, inner_acc, funs) do
do_transform(user_acc, next_op, next, inner_acc, funs)
end
defp do_transform_user([val | vals], user_acc, next_op, next, inner_acc, funs) do
{user, _, _, _, after_fun} = funs
try do
@@ -1013,20 +1013,20 @@ defmodule Stream do
after_fun.(user_acc)
:erlang.raise(kind, reason, __STACKTRACE__)
else
result -> do_transform_result(result, next_op, next, inner_acc, funs)
result -> do_transform_result(result, vals, next_op, next, inner_acc, funs)
end
end
defp do_transform_result(result, next_op, next, inner_acc, funs) do
defp do_transform_result(result, vals, next_op, next, inner_acc, funs) do
{_, fun, inner, _, after_fun} = funs
case result do
{[], user_acc} ->
do_transform(user_acc, next_op, next, inner_acc, funs)
do_transform_user(vals, user_acc, next_op, next, inner_acc, funs)
{list, user_acc} when is_list(list) ->
reduce = &Enumerable.List.reduce(list, &1, fun)
do_transform_inner_list(user_acc, next_op, next, inner_acc, reduce, funs)
do_transform_inner_list(vals, user_acc, next_op, next, inner_acc, reduce, funs)
{:halt, user_acc} ->
next.({:halt, []})
@@ -1035,11 +1035,11 @@ defmodule Stream do
{other, user_acc} ->
reduce = &Enumerable.reduce(other, &1, inner)
do_transform_inner_enum(user_acc, next_op, next, inner_acc, reduce, funs)
do_transform_inner_enum(vals, user_acc, next_op, next, inner_acc, reduce, funs)
end
end
defp do_transform_inner_list(user_acc, next_op, next, inner_acc, reduce, funs) do
defp do_transform_inner_list(vals, user_acc, next_op, next, inner_acc, reduce, funs) do
{_, _, _, _, after_fun} = funs
try do
@@ -1051,7 +1051,7 @@ defmodule Stream do
:erlang.raise(kind, reason, __STACKTRACE__)
else
{:done, acc} ->
do_transform(user_acc, next_op, next, {:cont, acc}, funs)
do_transform_user(vals, user_acc, next_op, next, {:cont, acc}, funs)
{:halted, acc} ->
next.({:halt, []})
@@ -1059,12 +1059,12 @@ defmodule Stream do
{:halted, acc}
{:suspended, acc, continuation} ->
resume = &do_transform_inner_list(user_acc, next_op, next, &1, continuation, funs)
resume = &do_transform_inner_list(vals, user_acc, next_op, next, &1, continuation, funs)
{:suspended, acc, resume}
end
end
defp do_transform_inner_enum(user_acc, next_op, next, {op, inner_acc}, reduce, funs) do
defp do_transform_inner_enum(vals, user_acc, next_op, next, {op, inner_acc}, reduce, funs) do
{_, _, _, _, after_fun} = funs
try do
@@ -1078,7 +1078,7 @@ defmodule Stream do
# The user wanted to cont/suspend but the stream halted,
# so we continue with the user intention.
{:halted, [inner_op | acc]} when op != :halt and inner_op != :halt ->
do_transform(user_acc, next_op, next, {inner_op, acc}, funs)
do_transform_user(vals, user_acc, next_op, next, {inner_op, acc}, funs)
{:halted, [_ | acc]} ->
next.({:halt, []})
@@ -1086,10 +1086,10 @@ defmodule Stream do
{:halted, acc}
{:done, [_ | acc]} ->
do_transform(user_acc, next_op, next, {:cont, acc}, funs)
do_transform_user(vals, user_acc, next_op, next, {:cont, acc}, funs)
{:suspended, [_ | acc], continuation} ->
resume = &do_transform_inner_enum(user_acc, next_op, next, &1, continuation, funs)
resume = &do_transform_inner_enum(vals, user_acc, next_op, next, &1, continuation, funs)
{:suspended, acc, resume}
end
end
+1 -1
View File
@@ -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)
+6
View File
@@ -64,6 +64,11 @@ start(_Type, _Args) ->
application:set_env(elixir, ansi_enabled, prim_tty:isatty(stdout) == true)
end,
%% Store the initial dbg_callback value before any runtime modifications.
%% This allows Mix compiler to detect config changes vs runtime changes
%% (e.g., Kino wrapping dbg_callback at runtime should not trigger recompilation).
{ok, InitialDbgCallback} = application:get_env(elixir, dbg_callback),
Tokenizer = case code:ensure_loaded('Elixir.String.Tokenizer') of
{module, Mod} -> Mod;
_ -> elixir_tokenizer
@@ -90,6 +95,7 @@ start(_Type, _Args) ->
{docs, true},
{ignore_already_consolidated, false},
{ignore_module_conflict, false},
{initial_dbg_callback, InitialDbgCallback},
{infer_signatures, [elixir]},
{on_undefined_variable, raise},
{parser_options, [{columns, true}]},
+18 -11
View File
@@ -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.
+5
View File
@@ -566,6 +566,11 @@ extract_bit_type({'-', _, [L, R]}, Acc) ->
extract_bit_type({unit, _, [Arg]}, Acc) ->
[{unit, Arg} | Acc];
extract_bit_type({Other, _, nil}, Acc) ->
[Other | Acc];
%% TODO: Remove me on Elixir v2.0.
%% Elixir v1.14 and earlier emitted an empty list
%% and may still be processed via debug_info.
extract_bit_type({Other, _, []}, Acc) ->
[Other | Acc].
%% Optimizations that are specific to Erlang and change
+2 -1
View File
@@ -27,8 +27,9 @@ each_clause({'catch', Meta, Raw, Expr}, S) ->
{Args, Guards} = elixir_utils:extract_splat_guards(Raw),
Match =
%% TODO: Remove me on Elixir v2.0.
%% Elixir v1.17 and earlier emitted single argument
%% and may still be processed via debug_info
%% and may still be processed via debug_info.
case Args of
[X] -> [throw, X];
[X, Y] -> [X, Y]
+6 -7
View File
@@ -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
+8
View File
@@ -1298,6 +1298,14 @@ warn_pipe(_Token, _) ->
ok.
%% TODO: Make this an error on v2.0
warn_no_parens_after_do_op({{in_op, Location, Op, _InLocation}, _}) ->
{Line, _, _} = Location,
warn(
Line,
"missing parentheses on expression following operator \"" ++ atom_to_list(Op) ++ "\", "
"you must add parentheses to avoid ambiguities"
);
warn_no_parens_after_do_op({{_Type, Location, Op}, _}) ->
{Line, _, _} = Location,
+11 -5
View File
@@ -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);
+1 -1
View File
@@ -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
+12
View File
@@ -787,6 +787,18 @@ defmodule FileTest do
end
end
test "cp_r! with file src and dest unknown" do
src = fixture_path("cp_r/a/1.txt")
dest = tmp_path("tmp/unknown/")
message =
"could not copy recursively from #{inspect(src)} to #{inspect(dest)}. #{dest}: no such file or directory"
assert_raise File.CopyError, message, fn ->
File.cp_r!(src, dest)
end
end
test "cp preserves mode" do
File.mkdir_p!(tmp_path("tmp"))
src = fixture_path("cp_mode")
@@ -0,0 +1,14 @@
defmodule Dialyzer.Regressions do
def io_inspect_opts do
IO.inspect(123, label: "foo", limit: :infinity)
end
def format_opts do
Code.format_string!("",
line_length: 120,
force_do_end_blocks: true,
locals_without_parens: true,
migrate: true
)
end
end
+21 -1
View File
@@ -282,10 +282,15 @@ defmodule Inspect.ListTest do
test "printable limit" do
assert inspect(~c"hello world", printable_limit: 4) == ~s(~c"hell" ++ ...)
assert inspect(~c"hello", printable_limit: :infinity) == ~s(~c"hello")
# Non printable characters after the limit don't matter
assert inspect(~c"hello world" ++ [0], printable_limit: 4) == ~s(~c"hell" ++ ...)
# Non printable strings aren't affected by printable limit
assert inspect([0, 1, 2, 3, 4], printable_limit: 3) == ~s([0, 1, 2, 3, 4])
# Ensure a non-printable value after limit does not crash
assert inspect(~c"hello world" ++ [nil], printable_limit: 4) == ~s(~c"hell" ++ ...)
# Ensure a non-printable value after default limit does not hang
assert String.ends_with?(inspect(List.duplicate(?a, 5000) ++ [nil]), "\" ++ ...")
end
test "keyword" do
@@ -310,10 +315,20 @@ defmodule Inspect.ListTest do
assert inspect([0], charlists: :infer) == "[0]"
end
test "opt as strings" do
test "opt as charlists" do
assert inspect(~c"john" ++ [0] ++ ~c"doe", charlists: :as_charlists) == ~s(~c"john\\0doe")
assert inspect(~c"john", charlists: :as_charlists) == ~s(~c"john")
assert inspect([0], charlists: :as_charlists) == ~s(~c"\\0")
assert inspect([nil], charlists: :as_charlists) == "[nil]"
assert inspect([0xD800], charlists: :as_charlists) == "[55296]"
assert inspect([1_114_112], charlists: :as_charlists) == "[1114112]"
assert inspect([[?a]], charlists: :as_charlists) == ~s([~c"a"])
assert inspect([?a, ?b, ?c | nil], charlists: :as_charlists, printable_limit: 4) ==
"[97, 98, 99 | nil]"
assert inspect([?a, ?b, ?c | nil], charlists: :as_charlists, printable_limit: 3) ==
~s(~c"abc" ++ ...)
end
test "opt as lists" do
@@ -906,6 +921,11 @@ defmodule Inspect.OthersTest do
assert inspect(Regex.compile!("foo", [:ucp])) == ~S'Regex.compile!("foo", [:ucp])'
end
@tag :re_import
test "exported regex" do
assert inspect(~r/foo/E) == "~r/foo/E"
end
test "inspect_fun" do
fun = fn
integer, _opts when is_integer(integer) ->
@@ -185,6 +185,11 @@ defmodule Kernel.DialyzerTest do
assert_dialyze_no_warnings!(context)
end
test "no warning in various non-regression cases", context do
copy_beam!(context, Dialyzer.Regressions)
assert_dialyze_no_warnings!(context)
end
defp copy_beam!(context, module) do
name = "#{module}.beam"
File.cp!(Path.join(context.base_dir, name), Path.join(context.outdir, name))
@@ -1953,6 +1953,18 @@ defmodule Kernel.WarningTest do
)
end
test "do+end with not in operator without explicit parentheses" do
assert_warn_eval(
["nofile:3\n", "missing parentheses on expression following operator \"not in\""],
"""
quote do
case do
end not in no_parens 1, 2
end
"""
)
end
test "variable is being expanded to function call (on_undefined_variable: warn)" do
capture_io(:stderr, fn ->
Code.put_compiler_option(:on_undefined_variable, :warn)
+5
View File
@@ -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 ->
+12
View File
@@ -331,6 +331,12 @@ defmodule ListTest do
assert_raise ArgumentError, ~r"cannot convert the given list to a string", fn ->
List.to_string([:a, :b])
end
invalid = List.duplicate(?a, 4096) ++ [nil]
assert_raise ArgumentError, ~r/got:\n\n\[97, 97, .*\.\.\.\]\n\z/s, fn ->
List.to_string(invalid)
end
end
test "to_charlist/1" do
@@ -349,6 +355,12 @@ defmodule ListTest do
assert_raise ArgumentError, ~r"cannot convert the given list to a charlist", fn ->
List.to_charlist([:a, :b])
end
invalid = List.duplicate(?a, 4096) ++ [nil]
assert_raise ArgumentError, ~r/got:\n\n\[97, 97, .*\.\.\.\]\n\z/s, fn ->
List.to_charlist(invalid)
end
end
describe "myers_difference/2" do
@@ -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
+16 -1
View File
@@ -52,6 +52,20 @@ defmodule RegexTest do
assert Regex.match?(~r/^b$/m, "a\nb\nc")
end
@tag :re_import
test "export" do
# exported patterns have no structs, so these are structurally equal
assert ~r/foo/E == Regex.compile!("foo", [:export])
assert Regex.match?(~r/foo/E, "foo")
refute Regex.match?(~r/foo/E, "Foo")
assert Regex.run(~r/c(d)/E, "abcd") == ["cd", "d"]
assert Regex.run(~r/e/E, "abcd") == nil
assert Regex.names(~r/(?<FOO>foo)/E) == ["FOO"]
end
test "precedence" do
assert {"aa", :unknown} |> elem(0) =~ ~r/(a)\1/
end
@@ -113,7 +127,8 @@ defmodule RegexTest do
test "compile!/1" do
assert %Regex{} = Regex.compile!("foo")
assert_raise Regex.CompileError, ~r/position 0$/, fn ->
# The exact position changed between Erlang/OTP 28.1 and 28.3
assert_raise Regex.CompileError, ~r/at position/, fn ->
Regex.compile!("*foo")
end
end
+18
View File
@@ -183,6 +183,24 @@ defmodule StreamTest do
assert Stream.chunk_while([1, 2, 3, 4, 5], [], chunk_fun, after_fun) |> Enum.at(0) == [1]
end
test "chunk_while/4 regression case with concat" do
result =
["WrongHeader\nJohn Doe", "skipped"]
|> Stream.take(1)
|> Stream.chunk_while(
"",
fn element, acc ->
{acc, elements} = String.split(acc <> element, "\n") |> List.pop_at(-1)
{:cont, elements, acc}
end,
&{:cont, [&1], []}
)
|> Stream.concat()
|> Enum.to_list()
assert result == ["WrongHeader", "John Doe"]
end
test "concat/1" do
stream = Stream.concat([1..3, [], [4, 5, 6], [], 7..9])
assert is_function(stream)
+14
View File
@@ -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").
+9 -2
View File
@@ -160,6 +160,7 @@ defmodule ExUnit.Assertions do
end
end
{left, right} = move_match(left, right)
__match__(left, right, code, check, __CALLER__)
end
@@ -380,6 +381,12 @@ defmodule ExUnit.Assertions do
{ExUnit.AssertionError.no_value(), expr}
end
defp move_match(left, {:=, meta, [middle, right]}),
do: move_match({:=, meta, [left, middle]}, right)
defp move_match(left, right),
do: {left, right}
@doc false
def __match__({:when, _, _} = left, right, _, _, _) do
suggestion =
@@ -409,7 +416,7 @@ defmodule ExUnit.Assertions do
case right do
unquote(left) ->
unquote(check)
unquote(mark_as_generated(vars))
{unquote_splicing(mark_as_generated(vars))}
_ ->
left = unquote(Macro.escape(left))
@@ -427,7 +434,7 @@ defmodule ExUnit.Assertions do
quote do
right = unquote(right)
expr = unquote(code)
unquote(vars) = unquote(match_expr)
{unquote_splicing(vars)} = unquote(match_expr)
right
end
end
+1 -5
View File
@@ -169,11 +169,7 @@ defmodule ExUnit.Callbacks do
end
"""
@type child_spec_overrides :: [
restart: :permanent | :transient | :temporary,
shutdown: :brutal_kill | timeout(),
type: :worker | :supervisor
]
@type child_spec_overrides :: Supervisor.child_spec_overrides()
@doc false
def __register__(module) do
+7
View File
@@ -964,7 +964,14 @@ defmodule ExUnit.DocTest do
@doc false
defmacro __assert__({:=, _, [left, right]} = assertion) do
{left, right} = move_match(left, right)
code = Macro.escape(assertion, prune_metadata: true)
ExUnit.Assertions.__match__(left, right, code, :ok, __CALLER__)
end
defp move_match(left, {:=, meta, [middle, right]}),
do: move_match({:=, meta, [left, middle]}, right)
defp move_match(left, right),
do: {left, right}
end
+22 -10
View File
@@ -338,7 +338,8 @@ defmodule ExUnit.Formatter do
{formatted_reason, wrapped_stack} =
format_exception(test, struct, wrapped_stack, width, formatter, @counter_padding)
formatted_stack = format_stacktrace(wrapped_stack, test.module, test.name, formatter)
formatted_stack = format_stacktrace(wrapped_stack, test, formatter)
{error_info(header, formatter) <> pad(formatted_reason <> formatted_stack), stack}
:error ->
@@ -512,10 +513,15 @@ defmodule ExUnit.Formatter do
defp format_diff(left, right, context, formatter) do
if has_value?(left) and has_value?(right) do
{result, env} = find_diff(left, right, context)
result = if formatter.(:diff_enabled?, false), do: result
hints = Enum.map(env.hints, &{:hint, format_hint(&1)})
{result, hints}
case find_diff(left, right, context) do
{result, env} ->
result = if formatter.(:diff_enabled?, false), do: result
hints = Enum.map(env.hints, &{:hint, format_hint(&1)})
{result, hints}
nil ->
{nil, []}
end
else
{nil, []}
end
@@ -672,22 +678,28 @@ defmodule ExUnit.Formatter do
|> Algebra.format(width)
end
defp format_stacktrace([], _case, _test, _color) do
defp format_stacktrace(stack, %{module: module, name: name}, color),
do: format_stacktrace(stack, module, name, color)
defp format_stacktrace(stack, %{name: name}, color),
do: format_stacktrace(stack, name, nil, color)
defp format_stacktrace([], _module, _test, _color) do
""
end
defp format_stacktrace(stacktrace, test_case, test, color) do
defp format_stacktrace(stacktrace, module, test, color) do
extra_info("stacktrace:", color) <>
Enum.map_join(stacktrace, fn entry ->
stacktrace_info(format_stacktrace_entry(entry, test_case, test), color)
stacktrace_info(format_stacktrace_entry(entry, module, test), color)
end)
end
defp format_stacktrace_entry({test_case, test, _, location}, test_case, test) do
defp format_stacktrace_entry({module, test, _, location}, module, test) do
format_file_line(location[:file], location[:line], " (test)")
end
defp format_stacktrace_entry(entry, _test_case, _test) do
defp format_stacktrace_entry(entry, _module, _test) do
format_stacktrace_entry(entry)
end
@@ -246,6 +246,16 @@ defmodule ExUnit.AssertionsTest do
end
end
test "assert parallel match" do
assert %URI{} = uri = apply(URI, :parse, ["/foo"])
# This should not warn
assert %URI{uri | path: "/bar"}.path == "/bar"
assert uri = %URI{} = apply(URI, :parse, ["/foo"])
# This should not warn
assert %URI{uri | path: "/bar"}.path == "/bar"
end
test "assert match with pinned variable" do
a = 1
{2, 1} = assert {2, ^a} = Value.tuple()
@@ -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]), []}]
+3 -3
View File
@@ -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
+41 -46
View File
@@ -71,61 +71,56 @@ defmodule IEx.Evaluator do
def parse(input, opts, []), do: parse(input, opts, {[], :other})
def parse(@break_trigger, opts, _parser_state) do
:elixir_errors.parse_error(
[line: opts[:line]],
opts[:file],
"incomplete expression",
"",
{~c"", Keyword.get(opts, :line, 1), Keyword.get(opts, :column, 1), 0}
)
end
def parse(input, opts, {buffer, last_op}) do
input = buffer ++ input
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
result =
with {:ok, tokens} <- :elixir.string_to_tokens(input, line, column, file, opts),
{:ok, adjusted_tokens, adjusted_op} <-
adjust_operator(tokens, line, column, file, opts, last_op),
{:ok, forms} <- :elixir.tokens_to_quoted(adjusted_tokens, file, opts) do
last_op =
case forms do
{:=, _, [_, _]} -> :match
_ -> :other
end
forms =
if adjusted_op != nil do
quote do
IEx.Evaluator.assert_no_error!()
unquote(forms)
if List.ends_with?(input, @break_trigger) do
triplet = {~c"", line, column, 0}
:elixir_errors.parse_error([line: line], file, "incomplete expression", "", triplet)
else
result =
with {:ok, tokens} <- :elixir.string_to_tokens(input, line, column, file, opts),
{:ok, adjusted_tokens, adjusted_op} <-
adjust_operator(tokens, line, column, file, opts, last_op),
{:ok, forms} <- :elixir.tokens_to_quoted(adjusted_tokens, file, opts) do
last_op =
case forms do
{:=, _, [_, _]} -> :match
_ -> :other
end
else
forms
end
{:ok, forms, last_op}
forms =
if adjusted_op != nil do
quote do
IEx.Evaluator.assert_no_error!()
unquote(forms)
end
else
forms
end
{:ok, forms, last_op}
end
case result do
{:ok, forms, last_op} ->
{:ok, forms, {[], last_op}}
{:error, {_, _, ""}} ->
{:incomplete, {input, last_op}}
{:error, {location, error, token}} ->
:elixir_errors.parse_error(
location,
file,
error,
token,
{input, line, column, 0}
)
end
case result do
{:ok, forms, last_op} ->
{:ok, forms, {[], last_op}}
{:error, {_, _, ""}} ->
{:incomplete, {input, last_op}}
{:error, {location, error, token}} ->
:elixir_errors.parse_error(
location,
file,
error,
token,
{input, line, column, 0}
)
end
end
-4
View File
@@ -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]`
+7 -1
View File
@@ -47,7 +47,13 @@ defmodule Logger.Backends.Handler do
%{truncate: truncate, utc_log: utc_log?} = config
level = erlang_level_to_elixir_level(erl_level)
message = Logger.Formatter.format_event(event, truncate)
timestamp = Map.get_lazy(metadata, :time, fn -> :os.system_time(:microsecond) end)
timestamp =
case metadata do
%{time: time} when is_integer(time) and time >= 0 -> time
_ -> :os.system_time(:microsecond)
end
date_time_ms = Logger.Formatter.system_time_to_date_time_ms(timestamp, utc_log?)
metadata = [erl_level: erl_level] ++ erlang_metadata_to_elixir_metadata(metadata)
event = {level, gl, {Logger, message, date_time_ms, metadata}}
+26 -1
View File
@@ -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
+40
View File
@@ -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
+36 -40
View File
@@ -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
View File
@@ -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
+21 -13
View File
@@ -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)
+9 -4
View File
@@ -126,11 +126,16 @@ defmodule Mix.Compilers.Protocol do
end
defp consolidation_paths do
filter_otp(:code.get_path(), :code.lib_dir())
end
otp = :code.lib_dir()
defp filter_otp(paths, otp) do
Enum.filter(paths, &(not :lists.prefix(otp, &1)))
# Pre-list the paths in each directory for performance
for path <- :code.get_path(), not :lists.prefix(otp, path) do
{path,
case :file.list_dir(path) do
{:ok, files} -> files
_ -> []
end}
end
end
defp consolidate([], _paths, output, _opts) do
+10 -1
View File
@@ -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
View File
@@ -468,51 +468,48 @@ defmodule Mix.Release do
{sys_config, runtime_config?} =
merge_provider_config(release, sys_config, config_provider_path)
path = Path.join(release.version_path, "sys.config")
args = [runtime_config?, sys_config]
format = "%% coding: utf-8~n%% RUNTIME_CONFIG=~s~n~tw.~n"
File.mkdir_p!(Path.dirname(path))
File.write!(path, IO.chardata_to_string(:io_lib.format(format, args)))
case :file.consult(path) do
{:ok, _} ->
case consultable_sys_config(sys_config) do
{:ok, contents} ->
path = Path.join(release.version_path, "sys.config")
args = [runtime_config?, contents]
format = "%% coding: utf-8~n%% RUNTIME_CONFIG=~s~n~ts.~n"
File.mkdir_p!(Path.dirname(path))
File.write!(path, IO.chardata_to_string(:io_lib.format(format, args)))
:ok
{:error, reason} ->
invalid =
for {app, kv} <- sys_config,
{key, value} <- kv,
not valid_config?(value),
do: """
Application: #{inspect(app)}
Key: #{inspect(key)}
Value: #{inspect(value)}
"""
message =
case invalid do
[] ->
"Could not read configuration file. Reason: #{inspect(reason)}"
_ ->
"Could not read configuration file. It has invalid configuration terms " <>
"such as functions, references, and pids. Please make sure your configuration " <>
"is made of numbers, atoms, strings, maps, tuples and lists. The following entries " <>
"are wrong:\n#{Enum.join(invalid)}"
end
{:error, message} ->
{:error, message}
end
end
defp valid_config?(m) when is_map(m),
do: Enum.all?(Map.delete(m, :__struct__), &valid_config?/1)
defp consultable_sys_config(sys_config) do
contents =
Enum.map_intersperse(sys_config, ?,, fn {app, kv} ->
kv =
Enum.map_intersperse(kv, ?,, fn {key, value} ->
case Mix.Utils.consultable(value) do
{:ok, value} -> [?{, :io_lib.print(key), ?,, value, ?}]
{:error, term, reason} -> throw({:error, app, key, term, reason})
end
end)
defp valid_config?(l) when is_list(l), do: Enum.all?(l, &valid_config?/1)
defp valid_config?(t) when is_tuple(t), do: Enum.all?(Tuple.to_list(t), &valid_config?/1)
defp valid_config?(o), do: is_number(o) or is_atom(o) or is_binary(o)
[?{, :io_lib.print(app), ?,, ?[, kv, ?], ?}]
end)
{:ok, [?[, contents, ?]]}
catch
{:error, app, key, value, reason} ->
message = """
Could not write configuration file because it has invalid terms
Application: #{inspect(app)}
Key: #{inspect(key)}
Invalid value: #{inspect(value)}
Reason: #{reason}
"""
{:error, message}
end
defp merge_provider_config(%{config_providers: []}, sys_config, _), do: {sys_config, false}
+15 -1
View File
@@ -295,10 +295,24 @@ defmodule Mix.Task.Compiler do
end
defp run_compiler(compiler, args) do
result = normalize(Mix.Task.run("compile.#{compiler}", args), compiler)
args_maybe_force = maybe_prepend_force(args, compiler)
result = normalize(Mix.Task.run("compile.#{compiler}", args_maybe_force), compiler)
Enum.reduce(Mix.ProjectStack.pop_after_compiler(compiler), result, & &1.(&2))
end
defp maybe_prepend_force(args, compiler) do
args
|> Enum.any?(fn
"--force-" <> rest -> rest == compiler |> to_string() |> String.replace("_", "-")
_ -> false
end)
|> if do
["--force" | args]
else
args
end
end
# Normalize the compiler result to a diagnostic tuple
defp normalize(result, name) do
case result do
+1 -1
View File
@@ -26,7 +26,7 @@ defmodule Mix.Tasks.Cmd do
Your shell will expand "lib/*" and then pass multiple arguments to
`mix cmd`, which in turn passes them to `echo`. Note that, `mix cmd`
by itself, does not perform any shell expansion. This means that,
if you invoke `mix cmd` programatically, as in:
if you invoke `mix cmd` programmatically, as in:
Mix.Task.run("cmd", ["echo", "lib/*"])
+13 -2
View File
@@ -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
+10 -5
View File
@@ -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
+4 -2
View File
@@ -7,7 +7,7 @@ defmodule Mix.Tasks.Compile do
@shortdoc "Compiles source files"
@moduledoc """
@moduledoc ~S"""
The main entry point to compile source files.
It simply runs the compilers registered in your project and returns
@@ -59,7 +59,9 @@ defmodule Mix.Tasks.Compile do
* `--all-warnings` (`--no-all-warnings`) - prints all warnings, including previous compilations
(default is true except on errors)
* `--erl-config` - path to an Erlang term file that will be loaded as Mix config
* `--force` - forces compilation
* `--force` - forces compilation. You can also specify `--force-#{compiler}` for each compiler
invoked to force specifically that compiler to run (it requires the compiler to respect the
`--force` option, which is advised)
* `--list` - lists all enabled compilers
* `--listeners` - starts Mix listeners (they are started by default,
unless `--no-listeners` or `--no-deps-check` are given)
+23 -13
View File
@@ -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
+13 -1
View File
@@ -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)
+28 -34
View File
@@ -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)
+2 -1
View File
@@ -271,7 +271,8 @@ defmodule Mix.Tasks.Escript.Build do
[]
end
defp extra_apps_in_app_tree(app) when app in [:eex, :ex_unit, :iex, :logger, :mix] do
# We include hex in here as it is always an archive and it cannot be depended on
defp extra_apps_in_app_tree(app) when app in [:eex, :ex_unit, :iex, :logger, :mix, :hex] do
[app]
end
+1 -1
View File
@@ -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`.
+15 -1
View File
@@ -112,12 +112,26 @@ defmodule Mix.Tasks.Help do
def run(["app:" <> app]) do
loadpaths!()
app = String.to_atom(app)
# If the application is not available, attempt to load it from Erlang/Elixir
if is_nil(Application.spec(app, :vsn)) do
try do
Mix.ensure_application!(app)
rescue
_ ->
# Otherwise, it may be a dep or the current project not yet compiled
if Mix.Project.get() do
Mix.Task.run("compile")
end
end
end
if modules = Application.spec(app, :modules) do
for module <- modules,
not (module |> Atom.to_string() |> String.starts_with?("Elixir.Mix.Tasks.")),
{:docs_v1, _, :elixir, "text/markdown", %{"en" => <<doc::binary>>}, _, _} <-
{:docs_v1, _, _, "text/markdown", %{"en" => <<doc::binary>>}, _, _} <-
[Code.fetch_docs(module)] do
leading = doc |> String.split(["\n\n", "\r\n\r\n"], parts: 2) |> hd()
"# #{inspect(module)}\n#{leading}\n"
+64 -15
View File
@@ -44,12 +44,22 @@ defmodule Mix.Tasks.Xref do
└── lib/a.ex
Because you have a compile-time dependency, any of the files `lib/a.ex`,
`lib/b.ex`, and `lib/c.ex` depend on will cause the whole cycle to
recompile. Therefore, your first priority to reduce compile times is
to remove such cycles. You can spot them by running:
`lib/b.ex`, and `lib/c.ex` depend on will cause `lib/a.ex` to recompile.
In other words, whenever you have a cycle, **a change to any file in the
cycle will cause all compile-time deps to recompile**. Therefore, your
first priority to reduce constant recompilations is to remove them.
You can spot them by running:
$ mix xref graph --format cycles --label compile-connected
> #### Use the --label option
>
> The job of `mix xref` is to explore relationships between files
> and it is expected that most of your files are either directly
> or indirectly connected. For this reason, it is strongly advised
> to pass the `--label` option to filter the amount of data,
> specifically with `compile-connected` (or `compile`) as values.
Whenever you find a compile-time dependency, such as `lib/a.ex` pointing
to `lib/b.ex`, there are two ways to remove them:
@@ -208,13 +218,11 @@ defmodule Mix.Tasks.Xref do
* `--exclude` - path to exclude. Can be repeated to exclude multiple paths.
* `--label` - only shows relationships with the given label.
The labels are "compile", "export" and "runtime". By default, the `--label`
option does not change how the graph is computed, it simply filters the
printed graph to show only relationships with the given label. However,
you can pass `--only-direct` to trim the graph to only the nodes that
have the direct relationship given by label. There is also a special
label called "compile-connected" that keeps only compile-time files with
at least one transitive dependency. See "Dependency types" section below.
The labels are "compile-connected", "compile", "export" and "runtime".
By default, the `--label` option does not change how the graph is computed,
it simply filters the printed graph to show only relationships with the given
label. However, you can pass `--only-direct` to trim the graph to only the
nodes that have the direct relationship given by label.
* `--group` - provide comma-separated paths to consider as a group. Dependencies
from and into multiple files of the group are considered a single dependency.
@@ -239,6 +247,9 @@ defmodule Mix.Tasks.Xref do
* `--min-cycle-size` - controls the minimum cycle size on formats
like `stats` and `cycles`
* `--min-cycle-label` - controls the minimum number of dependencies
with the given `--label` on a cycle
* `--format` - can be set to one of:
* `pretty` - prints the graph to the terminal using Unicode characters.
@@ -435,6 +446,7 @@ defmodule Mix.Tasks.Xref do
sink: :keep,
source: :keep,
min_cycle_size: :integer,
min_cycle_label: :integer,
output: :string
]
@@ -1218,9 +1230,22 @@ defmodule Mix.Tasks.Xref do
cycles
end
# :compile_connected is the same
min_cycle_label =
if integer = opts[:min_cycle_label] do
if filter == :all do
Mix.raise("--min-cycle-label requires the --label option to be given")
end
integer
else
1
end
# :compile_connected is the same as :compile
if cycle_fn = cycle_filter_fn(filter) do
Enum.filter(cycles, fn {_length, cycle} -> Enum.any?(cycle, cycle_fn) end)
Enum.filter(cycles, fn {_length, cycle} ->
Enum.count_until(cycle, cycle_fn, min_cycle_label) == min_cycle_label
end)
else
cycles
end
@@ -1269,11 +1294,35 @@ defmodule Mix.Tasks.Xref do
shell.info("#{length(cycles)} cycles found. Showing them in decreasing size:\n")
for {length, cycle} <- cycles do
shell.info("Cycle of length #{length}:\n")
meta =
cycle
|> Enum.reduce({0, 0}, fn
{_, :compile}, {compile, export} -> {compile + 1, export}
{_, :export}, {compile, export} -> {compile, export + 1}
{_, _}, {compile, export} -> {compile, export}
end)
|> case do
{0, 0} ->
""
{compile, export} ->
info =
if(compile > 0, do: ["#{compile} compile"], else: []) ++
if(export > 0, do: ["#{export} export"], else: [])
" (" <> Enum.join(info, ", ") <> ")"
end
shell.info("Cycle of length #{length}#{meta}:\n")
for {node, type} <- cycle do
type = if type, do: " (#{type})", else: ""
shell.info(" " <> node <> type)
shell.info(
case type do
:compile -> [:red, " #{node} (compile)"]
:export -> " #{node} (export)"
_ -> " #{node}"
end
)
end
shell.info("")
+73
View File
@@ -907,4 +907,77 @@ defmodule Mix.Utils do
uid
end
end
@doc """
Convert the given terms to a consultable Erlang term.
"""
def consultable(term) do
{:ok, to_erl_term(term)}
catch
{:error, term, reason} -> {:error, term, reason}
end
defp to_erl_term(tuple) when is_tuple(tuple) do
[?{, tuple |> Tuple.to_list() |> to_erl_head(), ?}]
end
defp to_erl_term(list) when is_list(list) do
if List.ascii_printable?(list) do
:io_lib.print(list)
else
[?[, to_erl_head(list), ?]]
end
end
defp to_erl_term(%Regex{re_pattern: {:re_pattern, _, _, _, ref}} = regex)
when is_reference(ref) do
throw({:error, regex, "you must use the /E modifier to store regexes"})
end
defp to_erl_term(map) when is_map(map) do
inner =
Enum.map_intersperse(
:maps.to_list(:maps.iterator(map, :reversed)),
?,,
fn {key, value} -> [to_erl_term(key), "=>", to_erl_term(value)] end
)
[?#, ?{, inner, ?}]
end
defp to_erl_term(map) when is_map(map) do
inner =
Enum.map_intersperse(
:maps.to_list(:maps.iterator(map, :reversed)),
?,,
fn {key, value} -> [to_erl_term(key), "=>", to_erl_term(value)] end
)
[?#, ?{, inner, ?}]
end
defp to_erl_term(function) when is_function(function) do
fun_info = Function.info(function)
if fun_info[:type] == :external and fun_info[:env] == [] do
:io_lib.print(function)
else
throw({:error, function, "only functions in the form &Mod.fun/arity are allowed"})
end
end
defp to_erl_term(term) when is_reference(term) or is_pid(term) do
throw({:error, term, "PIDs and References are not supported"})
end
defp to_erl_term(term) do
:io_lib.print(term)
end
defp to_erl_head([]), do: []
defp to_erl_head([h | t]), do: [to_erl_term(h) | to_erl_tail(t)]
defp to_erl_tail([h | t]), do: [?,, to_erl_term(h) | to_erl_tail(t)]
defp to_erl_tail([]), do: []
defp to_erl_tail(other), do: [?|, to_erl_term(other)]
end
+1 -1
View File
@@ -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
+6 -5
View File
@@ -592,19 +592,20 @@ defmodule Mix.ReleaseTest do
assert make_sys_config(release([]), [foo: [bar: :baz]], "unused/runtime/path") == :ok
contents = File.read!(@sys_config)
assert contents =~ "%% RUNTIME_CONFIG=false"
assert contents =~ "[{foo,[{bar,baz}]}]."
{:ok, contents} = :file.consult(@sys_config)
assert contents == [[foo: [bar: :baz]]]
end
test "writes sys_config with encoding" do
assert make_sys_config(
release([]),
[encoding: {:_μ, :"£", "£", ~c"£"}],
[encoding: [key: {:_μ, :"£", "£", ~c"£"}]],
"unused/runtime/path"
) ==
:ok
{:ok, contents} = :file.consult(@sys_config)
assert contents == [[encoding: {:_μ, :"£", "£", ~c"£"}]]
assert contents == [[encoding: [key: {:_μ, :"£", "£", ~c"£"}]]]
end
test "writes the given sys_config with config providers" do
@@ -649,12 +650,12 @@ defmodule Mix.ReleaseTest do
end
test "errors on bad config" do
assert {:error, "Could not read configuration file." <> _} =
assert {:error, "Could not write configuration file " <> _} =
make_sys_config(release([]), [foo: [bar: self()]], "unused/runtime/path")
env = %{__ENV__ | lexical_tracker: self()}
assert {:error, "Could not read configuration file." <> _} =
assert {:error, "Could not write configuration file " <> _} =
make_sys_config(release([]), [foo: [bar: env]], "unused/runtime/path")
end
end
+50 -23
View File
@@ -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
+69 -4
View File
@@ -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)
+51 -32
View File
@@ -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
+47 -3
View File
@@ -199,20 +199,64 @@ defmodule Mix.Tasks.HelpTest do
in_tmp(context.test, fn ->
output =
capture_io(fn ->
Mix.Tasks.Help.run(["app:mix"])
Mix.Tasks.Help.run(["app:iex"])
end)
assert output =~ "# Mix\n\nMix is a build tool"
assert output =~ "# IEx\n\nElixir's interactive shell."
if System.otp_release() >= "27" do
output =
capture_io(fn ->
Mix.Tasks.Help.run(["app:parsetools"])
end)
assert output =~ "# :leex"
end
end)
end
test "help unknown app:APP", context do
test "help app:UNKNOWN", context do
in_tmp(context.test, fn ->
Mix.Tasks.Help.run(["app:foobar"])
assert_received {:mix_shell, :error, ["Application foobar does not exist or is not loaded"]}
end)
end
defmodule ExampleProject do
def project do
[
app: :sample,
version: "0.1.0"
]
end
end
test "help app:APP for current project", context do
in_tmp(context.test, fn ->
Mix.Project.push(ExampleProject)
File.mkdir_p!("lib")
File.write!("lib/example.ex", ~s'''
defmodule Example do
@moduledoc """
This is an example module.
"""
end
''')
output =
capture_io(fn ->
Mix.Tasks.Help.run(["app:sample"])
end)
assert output =~ """
# Example
This is an example module.
"""
end)
end
test "help Elixir MODULE", context do
in_tmp(context.test, fn ->
output =
+25 -7
View File
@@ -500,7 +500,7 @@ defmodule Mix.Tasks.XrefTest do
assert_graph(["--format", "cycles"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
@@ -512,7 +512,7 @@ defmodule Mix.Tasks.XrefTest do
assert_graph(["--format", "cycles", "--label", "compile"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
@@ -524,7 +524,7 @@ defmodule Mix.Tasks.XrefTest do
assert_graph(["--format", "cycles", "--label", "compile-connected"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
@@ -539,7 +539,7 @@ defmodule Mix.Tasks.XrefTest do
assert_graph(["--format", "cycles", "--fail-above", "0"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
@@ -548,11 +548,11 @@ defmodule Mix.Tasks.XrefTest do
end
end
test "cycles with min_cycle_size matching actual length" do
assert_graph(["--format", "cycles", "--min-cycle-size", "2"], """
test "cycles with min_cycle_label matching actual length" do
assert_graph(["--format", "cycles", "--label", "compile", "--min-cycle-label", "1"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
@@ -560,6 +560,24 @@ defmodule Mix.Tasks.XrefTest do
""")
end
test "cycles with min_cycle_size matching actual length" do
assert_graph(["--format", "cycles", "--min-cycle-size", "2"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
""")
end
test "cycles with min_cycle_label greater than actual length" do
assert_graph(["--format", "cycles", "--label", "compile", "--min-cycle-label", "2"], """
No cycles found
""")
end
test "cycles with min_cycle_size greater than actual length" do
assert_graph(["--format", "cycles", "--min-cycle-size", "3"], """
No cycles found
+8 -18
View File
@@ -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
[
+11 -1
View File
@@ -43,12 +43,22 @@ cover_exclude =
[]
end
# OTP 28.1+
re_import_exclude =
if Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) do
[]
else
[:re_import]
end
Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__)
CoverageRecorder.maybe_record("mix")
ExUnit.start(
trace: !!System.get_env("TRACE"),
exclude: epmd_exclude ++ os_exclude ++ git_exclude ++ line_exclude ++ cover_exclude,
exclude:
epmd_exclude ++
os_exclude ++ git_exclude ++ line_exclude ++ cover_exclude ++ re_import_exclude,
include: line_include,
assert_receive_timeout: String.to_integer(System.get_env("ELIXIR_ASSERT_TIMEOUT", "300"))
)