Compare 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
José Valim 177dc8ab59 Release v1.19.0 2025-10-16 08:52:04 +02:00
José Valim bba8835ecc Fix IEx parser fetching on mix release 2025-10-16 08:46:48 +02:00
José Valim dea8456022 Escape meta within existing quote extensions (#14832)
Closes #14829
Closes #14830
2025-10-14 13:05:44 +02:00
José Valim 544a10ccfa Update CHANGELOG 2025-10-09 10:08:12 +02:00
José Valim ee14357bcd Add --shell to mix cmd (#14827) 2025-10-09 10:07:18 +02:00
José Valim 2fa136e60a Deal with relative paths in mix cmd, closes #14787 (#14788) 2025-10-09 09:27:16 +02:00
Eksperimental f382affce3 Fix typos in v1.19 (#14826) 2025-10-09 08:48:14 +02:00
José Valim 36190985d2 Include a hint for defimpl type checking 2025-10-08 18:27:39 +02:00
José Valim 957f84b7cd Release v1.19.0-rc.2 2025-10-07 18:10:32 +02:00
José Valim 74fab3caf4 Update parallel compiler docs, closes #14821 2025-10-07 17:54:41 +02:00
Eksperimental f1a6a4e019 Improve Kernel.ParallelCompiler warning (#14820) 2025-10-07 17:54:41 +02:00
Eksperimental c7884ca393 Elixir v1.19 introduces a warning related to structs, (#14818)
hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of:

         user = some_fun()
         %User{user | name: "John Doe"}

     it is enough to write:

         %User{} = user = some_fun()
         %{user | name: "John Doe"}

Since this could be seen by new-comers to the language, offering a better user experience by avoiding abbreviations. Favoring the usage of "some_function" instead of "some_fun"
2025-10-07 17:54:41 +02:00
José Valim 6f3fb272af Add mix help app:APP, closes #14782 2025-10-07 17:47:31 +02:00
José Valim 8c14d3a818 Add newline after inspection, closes #14819 2025-10-07 17:15:24 +02:00
José Valim 9e10ea876f Update CHANGELOG 2025-10-07 15:58:51 +02:00
José Valim 64f69cfee8 Improve error message when escaping default values with custom rules in structs, closes #14817 2025-10-07 14:26:11 +02:00
José Valim 7844c98fe7 Ensure escaping works within struct fields, closes #14817 2025-10-07 13:07:42 +02:00
José Valim 519eff5e29 Do not crash on empty test unit groups, closes #14754 2025-10-07 11:19:23 +02:00
Rafał Studnicki e29c40a7e9 Add key-based partitioning to duplicate registries (#14654) 2025-10-06 19:02:01 +02:00
José Valim 9fc3708980 Fix optimizations for closed map checking (#14813) 2025-10-06 17:25:10 +02:00
Jonatan Männchen c06b6a929e Update ORT Scanner (#14594) 2025-10-06 08:39:20 +02:00
Wojtek Mach cfa3bb7429 Fix preloading modules in mix test --slowest-modules=N (#14811) 2025-10-05 21:46:49 +02:00
Art Kay 195a4cf9f7 Optimize Access.filter to eliminate intermediate list creation (#14749) 2025-10-05 15:38:42 +02:00
José Valim 8daa2a6f67 Checkpoint before verification to avoid ignore modules warnings 2025-10-05 15:33:41 +02:00
José Valim d38f331db6 Use a map to track Mix compiler state 2025-10-05 15:29:54 +02:00
136 changed files with 3757 additions and 2061 deletions
+11 -1
View File
@@ -62,6 +62,16 @@ runs:
# Override Default Evaluator Rules
cp .ort/config/evaluator.rules.kts "$HOME/.ort/config/evaluator.rules.kts"
# Add Package Configurations
mkdir -p "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team"
for FILE in .ort/package-configurations/*.yml; do
COMPONENT="$(basename "$FILE")"
cp "$FILE" "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
sed -i -E \
"s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" \
"$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
done
# Set Version in SPDX & Config
sed -i "s/# elixir-version-insert/versionInfo: '${ELIXIR_VERSION}'/" project.spdx.yml
sed -i -E "s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" .ort.yml
@@ -80,7 +90,7 @@ runs:
id: ort
uses: oss-review-toolkit/ort-ci-github-action@1805edcf1f4f55f35ae6e4d2d9795ccfb29b6021 # v1.1.0
with:
image: ghcr.io/oss-review-toolkit/ort-minimal:54.0.0
image: ghcr.io/oss-review-toolkit/ort-minimal:65.0.0
run: >-
labels,
cache-dependencies,
+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
+56 -83
View File
@@ -3,18 +3,6 @@
excludes:
paths:
- pattern: "lib/elixir/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/elixir/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/ex_unit/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
- pattern: "lib/*/test/**/*"
reason: "TEST_OF"
comment: "Tests"
- pattern: "man/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
@@ -25,8 +13,64 @@ excludes:
reason: "BUILD_TOOL_OF"
comment: "Documentation"
# Unfortunately we'll have to repeat all package level excludes here
# Make sure to keep them in sync with the package configuration in
# .ort/package-configurations
- pattern: "lib/*/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/*/test/**/*"
reason: "TEST_OF"
comment: "Tests"
- pattern: "lib/*/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/*/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
curations:
license_findings:
# Version File
- path: "VERSION"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to VERSION file"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Wrongly Identified
- path: ".gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: ".gitattributes"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "CONTRIBUTING.md"
reason: "INCORRECT"
comment: "Wrongly identified TSL license"
detected_license: "Apache-2.0 OR NOASSERTION OR LicenseRef-scancode-tsl-2020"
concluded_license: "Apache-2.0"
- path: "OPEN_SOURCE_POLICY.md"
reason: "INCORRECT"
comment: "Wrongly identified NOASSERTION"
detected_license: "NOASSERTION"
concluded_license: "Apache-2.0"
# Unfortunately we'll have to repeat all package level license curations here
# Make sure to keep them in sync with the package configuration in
# .ort/package-configurations
# Test Fixtures
- path: "lib/*/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Logos
- path: "lib/elixir/pages/images/logo.png"
reason: "NOT_DETECTED"
@@ -39,13 +83,6 @@ curations:
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
# Version File
- path: "VERSION"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to VERSION file"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Documentation Images
- path: "lib/elixir/pages/images/**/*.png"
reason: "NOT_DETECTED"
@@ -54,26 +91,11 @@ curations:
concluded_license: "Apache-2.0"
# Test Fixtures
- path: "lib/eex/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/elixir/test/elixir/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/ex_unit/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/mix/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Unicode
- path: "lib/elixir/unicode/*.txt"
@@ -89,57 +111,8 @@ curations:
The guide mentions multiple licenses for users to choose from.
It however is not licensed itself by the mentioned licenses.
concluded_license: "Apache-2.0"
- path: ".gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: ".gitattributes"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "lib/elixir/scripts/windows_installer/.gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "CONTRIBUTING.md"
reason: "INCORRECT"
comment: "Wrongly identified TSL license"
detected_license: "Apache-2.0 OR NOASSERTION OR LicenseRef-scancode-tsl-2020"
concluded_license: "Apache-2.0"
- path: "OPEN_SOURCE_POLICY.md"
reason: "INCORRECT"
comment: "Wrongly identified NOASSERTION"
detected_license: "NOASSERTION"
concluded_license: "Apache-2.0"
packages:
- id: "SpdxDocumentFile:The Elixir Team:elixir-lang:"
curations:
concluded_license: "Apache-2.0 AND LicenseRef-scancode-unicode"
- id: "SpdxDocumentFile:The Elixir Team:eex:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:elixir:"
curations:
concluded_license: "Apache-2.0 AND LicenseRef-scancode-unicode"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:exunit:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:iex:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:logger:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
- id: "SpdxDocumentFile:The Elixir Team:mix:"
curations:
concluded_license: "Apache-2.0"
is_metadata_only: true
+8 -1
View File
@@ -3,6 +3,7 @@
ort:
enableRepositoryPackageCurations: true
enableRepositoryPackageConfigurations: true
scanner:
skipConcluded: false
@@ -11,4 +12,10 @@ ort:
analyzer:
allowDynamicVersions: true
enabledPackageManagers: [SpdxDocumentFile]
skipExcluded: true
reporter:
reporters:
SpdxDocument:
options:
creationInfoOrganization: The Elixir Team
documentName: "Elixir Source SPDX Document"
+15
View File
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:eex:"
path_excludes:
- pattern: "lib/eex/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/eex/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+60
View File
@@ -0,0 +1,60 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:elixir:"
path_excludes:
- pattern: "lib/elixir/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/elixir/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/elixir/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Logos
- path: "lib/elixir/pages/images/logo.png"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
- path: "lib/elixir/scripts/windows_installer/assets/Elixir.ico"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
# Documentation Images
- path: "lib/elixir/pages/images/**/*.png"
reason: "NOT_DETECTED"
comment: "Apply default license to all images"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Test Fixtures
- path: "lib/elixir/test/elixir/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Unicode
- path: "lib/elixir/unicode/*.txt"
reason: "NOT_DETECTED"
comment: "Apply default license to unicode files"
detected_license: "NONE"
concluded_license: "LicenseRef-scancode-unicode"
# Wrongly Identified
- path: "lib/elixir/pages/references/library-guidelines.md"
reason: "INCORRECT"
comment: |
The guide mentions multiple licenses for users to choose from.
It however is not licensed itself by the mentioned licenses.
concluded_license: "Apache-2.0"
- path: "lib/elixir/scripts/windows_installer/.gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+18
View File
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:exunit:"
path_excludes:
- pattern: "lib/ex_unit/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
- pattern: "lib/ex_unit/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/ex_unit/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+8
View File
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:logger:"
path_excludes:
- pattern: "lib/logger/test/**/*"
reason: "TEST_OF"
comment: "Tests"
+15
View File
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:mix:"
path_excludes:
- pattern: "lib/mix/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/mix/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+204 -51
View File
@@ -122,13 +122,27 @@ While Elixir has always compiled the given files in project or a dependency in p
### Code loading bottlenecks
Prior to this release, Elixir would load modules as soon as they were defined. However, because the Erlang part of code loading happens within a single process (the code server), this would make it a bottleneck, reducing the amount of parallelization, especially on large projects.
Prior to this release, Elixir would load modules as soon as they were defined. However, because the Erlang part of code loading happens within a single process (the code server), this would make it a bottleneck, reducing parallelization, especially on large projects.
This release makes it so modules are loaded lazily. This reduces the pressure on the code server, making compilation up to 2x faster for large projects, and also reduces the overall amount of work done during compilation.
This release makes it so modules are loaded lazily. This reduces the pressure on the code server and the amount of work during compilation, with reports of more than two times faster compilation for large projects. The benefits depend on the codebase size and the number of CPU cores available.
Implementation wise, [the parallel compiler already acts as a mechanism to resolve modules during compilation](https://elixir-lang.org/blog/2012/04/24/a-peek-inside-elixir-s-parallel-compiler/), so we built on that. By making sure the compiler controls both module compilation and module loading, it can also better guarantee deterministic builds.
The only potential regression in this approach happens if you have a module, which is used at compile time and defines an `@on_load` callback (typically used for [NIFs](https://www.erlang.org/doc/system/nif.html)) that invokes another modules within the same project. For example:
There are two potential regressions with this approach. The first one happens if you spawn processes during compilation which invoke other modules defined within the same project. For example:
```elixir
defmodule MyLib.SomeModule do
list = [...]
Task.async_stream(list, fn item ->
MyLib.SomeOtherModule.do_something(item)
end)
end
```
Because the spawned process is not visible to the compiler, it won't be able to load `MyLib.SomeOtherModule`. You have two options, either use `Kernel.ParallelCompiler.pmap/2` or explicitly call `Code.ensure_compiled!(MyLib.SomeOtherModule)` before spawning the process that uses said module.
The second one is related to `@on_load` callbacks (typically used for [NIFs](https://www.erlang.org/doc/system/nif.html)) that invoke other modules defined within the same project. For example:
```elixir
defmodule MyLib.SomeModule do
@@ -148,11 +162,13 @@ MyLib.SomeModule.something_else()
The reason this fails is because `@on_load` callbacks are invoked within the code server and therefore they have limited ability to load additional modules. It is generally advisable to limit invocation of external modules during `@on_load` callbacks but, in case it is strictly necessary, you can set `@compile {:autoload, true}` in the invoked module to address this issue in a forward and backwards compatible manner.
Both snippets above could actually lead to non-deterministic compilation failures in the past, and as a result of these changes, compiling these cases are now deterministic.
### Parallel compilation of dependencies
This release introduces a variable called `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`, which instructs `mix deps.compile` to compile dependencies in parallel.
While fetching dependencies and compiling individual Elixir dependencies already happened in parallel, there were pathological cases where performance would be left on the table, such as compiling dependencies with native code or dependencies where one or two large file would take over most of the compilation time.
While fetching dependencies and compiling individual Elixir dependencies already happened in parallel, as outlined in the previous section, there were pathological cases where performance gains would be left on the table, such as when compiling dependencies with native code or dependencies where one or two large files would take most of the compilation time.
By setting `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` to a number greater than 1, Mix will now compile multiple dependencies at the same time, using separate OS processes. Empirical testing shows that setting it to half of the number of cores on your machine is enough to maximize resource usage. The exact speed up will depend on the number of dependencies and the number of machine cores, although some reports mention up to 4x faster compilation times. If you plan to enable it on CI or build servers, keep in mind it will most likely have a direct impact on memory usage too.
@@ -183,6 +199,30 @@ The outer list is the first element, the first nested list is the second, follow
Given this may reduce the amount of data printed by default, the default limit has also been increased from 50 to 100. We may further increase it in upcoming releases based on community feedback.
## Erlang/OTP 28 support
Elixir v1.19 officially supports Erlang/OTP 28.1+ and later. In order to support the new Erlang/OTP 28 representation for regular expressions, structs can now control how they are escaped into abstract syntax trees by defining a `__escape__/1` callback.
On the other hand, the new representation for regular expressions implies they can no longer be used as default values for struct fields. Instead of this:
```elixir
defmodule Foo do
defstruct regex: ~r/foo/
end
```
You must do this:
```elixir
defmodule Foo do
defstruct [:regex]
def new do
%Foo{regex: ~r/foo/}
end
end
```
## OpenChain certification
Elixir v1.19 is also our first release following OpenChain compliance, [as previously announced](https://elixir-lang.org/blog/2025/02/26/elixir-openchain-certification/). In a nutshell:
@@ -192,68 +232,147 @@ Elixir v1.19 is also our first release following OpenChain compliance, [as previ
These additions offer greater transparency into the components and licenses of each release, supporting more rigorous supply chain requirements.
This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosystem Foundation.
This work was performed by [Jonatan Männchen](https://maennchen.dev) and sponsored by the [Erlang Ecosystem Foundation](https://erlef.org).
## v1.19.0-rc.1 (2025-10-05)
## 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
* [Kernel] Raise when U+2028 and U+2029 characters are present in comments and strings to avoid line spoofing attacks
* [Kernel] Include the line for the previous clause in errors/warnings related to conflicts between defaults on function definitions
* [Macro] Add `__escape__/1` callback so structs can escape references and other runtime data types in `Macro.escape/1`
* [OptionParser] Support the `:regex` type
* [OptionParser] Enhance parsing error to display available options
* [String] Update to Unicode 17.0.0
#### ExUnit
* [ExUnit] Set a process label for each test
#### Logger
* [Logger] Accept any enumerable in `Logger.metadata/1`
#### Mix
* [mix format] Add options to mix format to allow excluding of files
* [mix test] Add `--name-pattern` option to `mix test`
* [Mix.install/2] Support the `:compilers` option
* [Protocol] Optimize protocol consolidation to no longer load structs
### 2. Bug fixes
#### Elixir
* [Code] Return error on invalid unicode sequences in `Code.string_to_quoted/2` instead of raising
* [Code] Properly handle column annotation for `in` in `not in` expressions
* [Enum] Fix infinite loop on `Enum.take/2` with negative index on empty enumerable
* [Inspect] Inspect ill-formed structs as maps
* [Kernel] Properly increment metadata newline when `?` is followed by a literal newline character
#### ExUnit
* [ExUnit.Assertions] Fix order in ExUnit results when listing pinned variables
* [ExUnit.Assertions] Raise if attempting to raise an assertion error with invalid message (not a binary)
* [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] Abort pipelines when there is an error in any step along the way
* [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 compile] Fix bug where reverting changes to an external resource (such as HEEx template) after a compilation error would make it so the source module would not be compiled
* [mix compile] Avoid failures when locking compilation across different users
* [mix compile] Fix race condition when renaming files used by the compilation lock
* [mix test] Prevent `mix test` from overriding `:failures_manifest_path` option
* [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
### 3. Hard deprecations
### 2. Bug fixes
#### Elixir
* [Code] Warn if line-break characters outside of `\r` and `\r\n` are found in strings according to UX#55. This warning will be fast-tracked into an error for security reasons in Elixir v1.20, following a similar rule to bidirectional control characters. They will already raise if found in comments
* [File] Ensure `File.cp_r/3` reports non-existing destination properly (instead of source)
## v1.19.0-rc.0 (2025-06-09)
#### 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
@@ -271,18 +390,26 @@ This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosyst
* [Inspect] Allow `optional: :all` when deriving Inspect
* [Inspect.Algebra] Add optimistic/pessimistic groups as a simplified implementation of `next_break_fits`
* [IO.ANSI] Add ANSI codes to turn off conceal and crossed_out
* [Kernel] Allow controlling which applications are used during inference
* [Kernel] Raise when U+2028 and U+2029 characters are present in comments and strings to avoid line spoofing attacks
* [Kernel] Include the line for the previous clause in errors/warnings related to conflicts between defaults on function definitions
* [Kernel] Support `min/2` and `max/2` as guards
* [Kernel.ParallelCompiler] Add `each_long_verification_threshold` which invokes a callback when type checking a module takes too long
* [Kernel.ParallelCompiler] Include lines in `== Compilation error in file ... ==` slogans
* [Macro] Print debugging results from `Macro.dbg/3` as they happen, instead of once at the end
* [Macro] Add `__escape__/1` callback so structs can escape references and other runtime data types in `Macro.escape/1`
* [Module] Do not automatically load modules after their compilation, guaranteeing a more consistent compile time experience and drastically improving compilation times
* [OptionParser] Support the `:regex` type
* [OptionParser] Enhance parsing error to display available options
* [Protocol] Type checking of protocols dispatch and implementations
* [Regex] Add `Regex.to_embed/2` which returns an embeddable representation of regex in another regex
* [Regex] Raise error message when regexes are used as default values in struct fields for compatibility with Erlang/OTP 28
* [Registry] Add key-based partitioning of duplicate registries
* [String] Add `String.count/2` to count occurrences of a pattern
* [String] Update to Unicode 17.0.0
#### ExUnit
* [ExUnit] Set a process label for each test
* [ExUnit.CaptureLog] Parallelize log dispatch when multiple processes are capturing log
* [ExUnit.Case] Add `:test_group` to the test context
* [ExUnit.Doctest] Support ellipsis in doctest exceptions to match the remaining of the exception
@@ -293,37 +420,62 @@ This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosyst
* [IEx] Support multi-line prompts (due to this feature, `:continuation_prompt` and `:alive_continuation_prompt` are no longer supported as IEx configuration)
* [IEx.Autocomplete] Functions annotated with `@doc group: "Name"` metadata will appear within their own groups in autocompletion
#### Logger
* [Logger] Accept any enumerable in `Logger.metadata/1`
#### Mix
* [mix] Add support for `MIX_PROFILE_FLAGS` to configure `MIX_PROFILE`
* [mix compile] Debug the compiler and type checker PID when `MIX_DEBUG=1` and compilation/verification thresholds are met
* [mix compile] Add `Mix.Tasks.Compiler.reenable/1`
* [mix deps.compile] Support `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` for compiling deps concurrently across multiple operating system processes
* [mix help] Add `mix help Mod`, `mix help :mod`, `mix help Mod.fun` and `mix help Mod.fun/arity`
* [mix help] Add `mix help Mod`, `mix help :mod`, `mix help Mod.fun`, `mix help Mod.fun/arity`, and `mix help app:package`
* [mix format] Add options to mix format to allow excluding of files
* [mix test] Add `--name-pattern` option to `mix test`
* [mix test] Allow to distinguish the exit status between warnings as errors and test failures
* [mix xref graph] Add support for `--format json`
* [mix xref graph] Emit a warning if `--source` is part of a cycle
* [M ix.Task.Compiler] Add `Mix.Task.Compiler.run/2`
* [Mix] Support the `:compilers` option
* [Mix.Task.Compiler] Add `Mix.Task.Compiler.run/2`
### 2. Bug fixes
#### Elixir
* [Code] Return error on invalid unicode sequences in `Code.string_to_quoted/2` instead of raising
* [Code] Properly handle column annotation for `in` in `not in` expressions
* [DateTime] Do not truncate microseconds regardless of precision in `DateTime.diff/3`
* [Enum] Fix infinite loop on `Enum.take/2` with negative index on empty enumerable
* [File] Properly handle permissions errors cascading from parent in `File.mkdir_p/1`
* [Inspect] Inspect ill-formed structs as maps
* [Kernel] Properly increment metadata newline when `?` is followed by a literal newline character
* [Kernel] `not_a_map.key` now raises `BadMapError` for consistency with other map operations
* [Protocol] `defstruct/1` and `defexception/1` are now disabled inside `defprotocol` as to not allow defining structs/exceptions alongside a protocol
* [Regex] Fix `Regex.split/2` returning too many results when the chunk being split on was empty (which can happen when using features such as `/K`)
* [Stream] Ensure `Stream.transform/5` respects suspend command when its inner stream halts
* [URI] Several fixes to `URI.merge/2` related to trailing slashes, trailing dots, and hostless base URIs
#### ExUnit
* [ExUnit.Assertions] Fix order of pinned variables in failure reports
* [ExUnit.Assertions] Raise if attempting to raise an assertion error with invalid message (not a binary)
* [ExUnit.Case] Do not crash on empty test unit groups
#### IEx
* [IEx] Abort pipelines when there is an error in any step along the way
#### Mix
* [mix cmd] Preserve argument quoting in subcommands
* [mix cmd] Preserve argument quoting in subcommands by no longer performing shell expansion. To revert to the previous behaviour, pass `--shell` before the command name
* [mix compile] Fix bug where reverting changes to an external resource (such as HEEx template) after a compilation error would make it so the source module would not be compiled
* [mix compile] Avoid failures when locking compilation across different users
* [mix compile] Fix race condition when renaming files used by the compilation lock
* [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] Preserve files with no longer filter on `mix test`
* [mix xref graph] Provide more consistent output by considering strong connected components only when computing graphs
* [mix test] Ensure modules are preloaded in `mix test --slowest-modules=N`
* [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)
@@ -335,12 +487,13 @@ This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosyst
#### Mix
* [mix compile] `--no-protocol-consolidation` is deprecated in favor of `--no-consolidate-protocols` for consistency with `mix.exs` configuration
* [mix compile.protocols] Protocol consolidation is now part of `compile.elixir` and has no effect
* [mix compile.protocols] Protocol consolidation is now part of `compile.elixir` and the task itself has no effect
### 4. Hard deprecations
#### Elixir
* [Code] Warn if line-break characters outside of `\r` and `\r\n` are found in strings according to UX#55. This warning will be fast-tracked into an error for security reasons in Elixir v1.20, following a similar rule to bidirectional control characters. They will already raise if found in comments
* [Code] The `on_undefined_variable: :warn` is deprecated. Relying on undefined variables becoming function calls will not be supported in the future
* [File] Passing a callback as third argument to `File.cp/3` is deprecated, pass it as a `on_conflict: callback` option instead
* [File] Passing a callback as third argument to `File.cp_r/3` is deprecated, pass it as a `on_conflict: callback` option instead
+1 -1
View File
@@ -1 +1 @@
1.19.0-rc.1
1.19.6
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.19.0-rc.1
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-rc.1
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
@@ -891,7 +891,7 @@ defmodule Access do
end
defp filter(:get, data, func, next) when is_list(data) do
data |> Enum.filter(func) |> Enum.map(next)
for elem <- data, func.(elem), do: next.(elem)
end
defp filter(:get_and_update, data, func, next) when is_list(data) do
+1 -1
View File
@@ -1763,7 +1763,7 @@ defmodule DateTime do
Although the first example shows a difference of 2 hours when
comparing the wall clocks of the given datetime with the returned one,
due to the "spring forward" time jump, the actual ellapsed time is
due to the "spring forward" time jump, the actual elapsed time is
still exactly of 1 hour.
In case you don't want these changes to happen automatically or you
+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 =
[
+5 -6
View File
@@ -188,13 +188,12 @@ defmodule Exception do
term
|> inspect(pretty: true)
|> String.split("\n")
|> Enum.map(fn
|> Enum.map_intersperse("\n", fn
"" -> ""
line -> " " <> line
end)
|> Enum.join("\n")
message <> "\n\n" <> inspected
IO.iodata_to_binary([message, "\n\n", inspected, "\n"])
end
@doc """
@@ -1912,7 +1911,7 @@ defmodule UndefinedFunctionError do
end
defp format_fa({_dist, fun, arity}) do
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
end
defp exports_for(module) do
@@ -2244,7 +2243,7 @@ defmodule KeyError do
case suggestions do
[] -> []
suggestions -> ["\n\nDid you mean:\n\n" | format_suggestions(suggestions)]
suggestions -> ["\nDid you mean:\n\n" | format_suggestions(suggestions)]
end
end
@@ -2253,7 +2252,7 @@ defmodule KeyError do
|> Enum.sort(&(elem(&1, 0) >= elem(&2, 0)))
|> Enum.take(@max_suggestions)
|> Enum.sort(&(elem(&1, 1) <= elem(&2, 1)))
|> Enum.map(fn {_, key} -> [" * ", inspect(key), ?\n] end)
|> Enum.map(fn {_, key} -> [" * ", inspect(key), ?\n] end)
end
end
+10 -4
View File
@@ -317,7 +317,7 @@ defmodule File do
directories of `path`
* `:enospc` - there is no space left on the device
* `:enotdir` - a component of `path` is not a directory
* `:eperm` - missed required permisions
* `:eperm` - missed required permissions
## Examples
@@ -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)
+73 -24
View File
@@ -146,10 +146,9 @@ defmodule Kernel.ParallelCompiler do
the current file stops being compiled until the dependency is
resolved.
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
by default but we recommend using `return_diagnostics: true` so it returns
diagnostics as maps as well as a map of compilation information.
The map has the shape of:
It must be invoked with `return_diagnostics: true` as option, so it returns
`{:ok, modules, warnings_info}` or `{:error, errors, warnings_info}`,
where `warnings_info` has the shape:
%{
runtime_warnings: [warning],
@@ -209,7 +208,8 @@ defmodule Kernel.ParallelCompiler do
* `:beam_timestamp` - the modification timestamp to give all BEAM files
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
a list of warnings and returns diagnostics as maps instead of tuples
a list of warnings and returns diagnostics as maps instead of tuples.
This option must be set to true, except for backwards compatibibility reasons.
* `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially.
@@ -243,10 +243,9 @@ defmodule Kernel.ParallelCompiler do
Opposite to compile, dependencies are not attempted to be
automatically solved between files.
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
by default but we recommend using `return_diagnostics: true` so it returns
diagnostics as maps as well as a map of compilation information.
The map has the shape of:
It must be invoked with `return_diagnostics: true` as option, so it returns
`{:ok, modules, warnings_info}` or `{:error, errors, warnings_info}`,
where `warnings_info` has the shape:
%{
runtime_warnings: [warning],
@@ -265,8 +264,9 @@ defmodule Kernel.ParallelCompiler do
Setting this option to 1 will compile files sequentially.
Defaults to the number of schedulers online, or at least `2`.
* `:return_diagnostics` - when `true`, returns structured diagnostics
as maps instead of the legacy format. Defaults to `false`.
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
a list of warnings and returns diagnostics as maps instead of tuples.
This option must be set to true, except for backwards compatibibility reasons.
"""
@doc since: "1.6.0"
@@ -321,7 +321,10 @@ defmodule Kernel.ParallelCompiler do
if Keyword.get(options, :return_diagnostics, false) do
{status, modules_or_errors, info}
else
IO.warn("you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler")
IO.warn(
"you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler functions"
)
to_tuples = &Enum.map(&1, fn diag -> {diag.file, diag.position, diag.message} end)
modules_or_errors =
@@ -377,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
+4 -3
View File
@@ -126,10 +126,11 @@ defmodule Kernel.Utils do
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
try do
:elixir_quote.escape(val, :escape, false)
:elixir_quote.escape(val, {:struct, module}, false)
rescue
e in [ArgumentError] ->
raise ArgumentError, "invalid value for struct field #{key}, " <> Exception.message(e)
raise ArgumentError,
"invalid default value for struct field #{key}, " <> Exception.message(e)
else
_ -> {key, val}
end
@@ -171,7 +172,7 @@ defmodule Kernel.Utils do
:lists.foreach(foreach, enforce_keys)
struct = :maps.from_list([__struct__: module] ++ fields)
escaped_struct = :elixir_quote.escape(struct, :escape, false)
escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false)
body =
case bootstrapped? 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} =
+9 -18
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`
@@ -565,7 +556,7 @@ defmodule Module do
written to disk, and therefore raising does not effectively halt compilation
and may leave unused artifacts on disk. If you must raise, use `@after_compile`
or other callback. Given modules have already been compiled, functions in
ths module, such as `get_attribute/2`, which expect modules to not have been
this module, such as `get_attribute/2`, which expect modules to not have been
yet compiled, do not work on `@after_verify` callback.
Accepts a module or a `{module, function_name}` tuple. The function
+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
+20 -8
View File
@@ -49,8 +49,8 @@ defmodule Module.Types do
finder =
fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, kind, _, _} = clause ->
{infer_mode(kind, infer_signatures?), clause, default_domain(fun_arity, impl)}
{_, kind, _, _} = def ->
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
false ->
false
@@ -82,7 +82,7 @@ defmodule Module.Types do
{types, context} ->
# Optimized version of finder, since we already the definition
finder = fn _ ->
{infer_mode(kind, infer_signatures?), def, default_domain(fun_arity, impl)}
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
end
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
@@ -146,12 +146,24 @@ defmodule Module.Types do
end
end
defp default_domain({_, arity} = fun_arity, impl) do
defp default_domain(mode, def, {_, arity} = fun_arity, impl) do
with {for, callbacks} <- impl,
true <- fun_arity in callbacks do
[Descr.dynamic(Module.Types.Of.impl(for)) | List.duplicate(Descr.dynamic(), arity - 1)]
args = [
Descr.dynamic(Module.Types.Of.impl(for))
| List.duplicate(Descr.dynamic(), arity - 1)
]
{fun_arity, kind, meta, clauses} = def
clauses =
for {meta, args, guards, body} <- clauses do
{[type_check: {:impl, for}] ++ meta, args, guards, body}
end
{mode, {fun_arity, kind, meta, clauses}, args}
else
_ -> List.duplicate(Descr.dynamic(), arity)
_ -> {mode, def, List.duplicate(Descr.dynamic(), arity)}
end
end
@@ -208,7 +220,7 @@ defmodule Module.Types do
finder = fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, _, _, _} = clause -> {:dynamic, clause, default_domain(fun_arity, impl)}
{_, _, _, _} = def -> default_domain(:dynamic, def, fun_arity, impl)
false -> false
end
end
@@ -219,7 +231,7 @@ defmodule Module.Types do
context =
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
# Optimized version of finder, since we already the definition
finder = fn _ -> {:dynamic, def, default_domain(fun_arity, impl)} end
finder = fn _ -> default_domain(:dynamic, def, fun_arity, impl) end
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
context
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()
+114 -64
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.
@@ -3363,10 +3352,10 @@ defmodule Module.Types.Descr do
true
# The keys is only in the negative map, and the positive map is closed
# in that case, this field is not_set(), and its difference with the negative map type is empty iff
# the negative type is optional.
# in that case, this field is not_set(), and its difference with the
# negative map type is empty iff the negative type is optional.
tag == :closed ->
is_optional_static(neg_type) or map_line_empty?(tag, fields, negs)
is_optional_static(neg_type) or throw(:closed)
# There may be value in common
tag == :open ->
@@ -3391,6 +3380,9 @@ defmodule Module.Types.Descr do
neg_tag == :open ->
true
neg_tag == :closed and not is_optional_static(type) ->
throw(:closed)
true ->
# an absent key in a open negative map can be ignored
diff = difference(type, neg_atom_default)
@@ -3401,6 +3393,8 @@ defmodule Module.Types.Descr do
else
map_line_empty?(tag, fields, negs)
end
catch
:closed -> map_line_empty?(tag, fields, negs)
end
# Verify the domain condition from equation (22) in paper ICFP'23 https://www.irif.fr/~gc/papers/icfp23.pdf
@@ -4420,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
@@ -4470,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
@@ -4490,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)}
@@ -4520,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
@@ -4538,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
@@ -4563,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)),
@@ -4587,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
@@ -4598,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]
@@ -4625,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
+2 -2
View File
@@ -849,12 +849,12 @@ defmodule Module.Types.Expr do
you may optionally convert the struct update into a map update. For \
example, instead of:
user = some_fun()
user = some_function()
%User{user | name: "John Doe"}
it is enough to write:
%User{} = user = some_fun()
%User{} = user = some_function()
%{user | name: "John Doe"}
"""
])
+14 -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 ->
"""
@@ -132,6 +109,20 @@ defmodule Module.Types.Helpers do
the union (which may be none)
"""
{:impl, for} ->
# Get the type without dynamic for better pretty printing
type =
for
|> Module.Types.Of.impl()
|> Module.Types.Descr.dynamic()
|> Map.fetch!(:dynamic)
|> Module.Types.Descr.to_quoted_string(collapse_structs: true)
"""
#{hint()} defimpl for #{inspect(for)} requires its callbacks to match exclusively on #{type}
"""
:empty_domain ->
"""
+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]))
+10 -4
View File
@@ -259,12 +259,12 @@ defmodule Module.Types.Pattern do
defp badpattern_error(expr, index, tag, stack, context) do
meta =
if meta = get_meta(expr) do
meta ++ Keyword.take(stack.meta, [:generated, :line])
meta ++ Keyword.take(stack.meta, [:generated, :line, :type_check])
else
stack.meta
end
error(__MODULE__, {:badpattern, expr, index, tag, context}, meta, stack, context)
error(__MODULE__, {:badpattern, meta, expr, index, tag, context}, meta, stack, context)
end
defp of_pattern_intersect(tree, expected, expr, index, tag, stack, context) do
@@ -802,13 +802,19 @@ defmodule Module.Types.Pattern do
#
# The match pattern ones have the whole expression instead
# of a single pattern.
def format_diagnostic({:badpattern, pattern_or_expr, index, tag, context}) do
def format_diagnostic({:badpattern, meta, pattern_or_expr, index, tag, context}) do
{to_trace, message} = badpattern(tag, pattern_or_expr, index)
traces = collect_traces(to_trace, context)
hints =
case Keyword.get(meta, :type_check) do
{:impl, _} = impl -> [impl]
_ -> []
end
%{
details: %{typing_traces: traces},
message: IO.iodata_to_binary([message, format_traces(traces)])
message: IO.iodata_to_binary([message, format_traces(traces), format_hints(hints)])
}
end
+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 -8
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}
@@ -1017,14 +1041,14 @@ defmodule Regex do
pattern_ast =
cond do
is_nil(regex.re_pattern) ->
nil
# TODO: Remove this when we require Erlang/OTP 28+
# Before OTP 28.0, patterns did not contain any refs and could be safely be escaped
: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)
+119 -35
View File
@@ -187,7 +187,7 @@ defmodule Registry do
Note that the registry uses one ETS table plus two ETS tables per partition.
"""
@keys [:unique, :duplicate]
@keys [:unique, :duplicate, {:duplicate, :key}, {:duplicate, :pid}]
@all_info -1
@key_info -2
@@ -195,7 +195,7 @@ defmodule Registry do
@type registry :: atom
@typedoc "The type of the registry"
@type keys :: :unique | :duplicate
@type keys :: :unique | :duplicate | {:duplicate, :key} | {:duplicate, :pid}
@typedoc "The type of keys allowed on registration"
@type key :: term
@@ -266,8 +266,8 @@ defmodule Registry do
:undefined
end
{kind, _, _} ->
raise ArgumentError, ":via is not supported for #{kind} registries"
{{:duplicate, _}, _, _} ->
raise ArgumentError, ":via is not supported for duplicate registries"
end
end
@@ -329,11 +329,24 @@ defmodule Registry do
{Registry, keys: :unique, name: MyApp.Registry, partitions: System.schedulers_online()}
], strategy: :one_for_one)
For `:duplicate` registries with many different keys (e.g., many topics with
few subscribers each), you can optimize key-based lookups by partitioning by key:
Registry.start_link(
keys: {:duplicate, :key},
name: MyApp.TopicRegistry,
partitions: System.schedulers_online()
)
This allows key-based lookups to check only a single partition instead of
searching all partitions. Use the default `:pid` partitioning when you have
fewer keys with many entries each (e.g., one topic with many subscribers).
## Options
The registry requires the following keys:
* `:keys` - chooses if keys are `:unique` or `:duplicate`
* `:keys` - chooses if keys are `:unique`, `:duplicate`, `{:duplicate, :key}`, or `{:duplicate, :pid}`
* `:name` - the name of the registry and its tables
The following keys are optional:
@@ -345,16 +358,40 @@ defmodule Registry do
crashes. Messages sent to listeners are of type `t:listener_message/0`.
* `:meta` - a keyword list of metadata to be attached to the registry.
For `:duplicate` registries, you can specify the partitioning strategy
directly in the `:keys` option:
* `:duplicate` or `{:duplicate, :pid}` - Use `:pid` partitioning (default)
when you have keys with many entries (e.g., one topic with many subscribers).
This is the traditional behavior and groups all entries from the same process together.
* `{:duplicate, :key}` - Use `:key` partitioning when entries are spread across
many different keys (e.g., many topics with few subscribers each). This makes
key-based lookups more efficient as they only need to check a single partition
instead of all partitions.
"""
@doc since: "1.5.0"
@spec start_link([start_option]) :: {:ok, pid} | {:error, term}
def start_link(options) do
keys = Keyword.get(options, :keys)
if keys not in @keys do
raise ArgumentError,
"expected :keys to be given and be one of :unique or :duplicate, got: #{inspect(keys)}"
end
# Validate and normalize keys format
kind =
case keys do
{:duplicate, partition_strategy} when partition_strategy in [:key, :pid] ->
{:duplicate, partition_strategy}
:unique ->
:unique
:duplicate ->
{:duplicate, :pid}
_ ->
raise ArgumentError,
"expected :keys to be given and be one of :unique, :duplicate, {:duplicate, :key}, or {:duplicate, :pid}, got: #{inspect(keys)}"
end
name =
case Keyword.fetch(options, :name) do
@@ -397,11 +434,18 @@ defmodule Registry do
# The @info format must be kept in sync with Registry.Partition optimization.
entries = [
{@all_info, {keys, partitions, nil, nil, listeners}},
{@key_info, {keys, partitions, nil}} | meta
{@all_info, {kind, partitions, nil, nil, listeners}},
{@key_info, {kind, partitions, nil}} | meta
]
Registry.Supervisor.start_link(keys, name, partitions, listeners, entries, compressed)
Registry.Supervisor.start_link(
kind,
name,
partitions,
listeners,
entries,
compressed
)
end
@doc false
@@ -468,7 +512,8 @@ defmodule Registry do
end
{kind, _, _} ->
raise ArgumentError, "Registry.update_value/3 is not supported for #{kind} registries"
raise ArgumentError,
"Registry.update_value/3 is not supported for #{inspect(kind)} registries"
end
end
@@ -508,12 +553,12 @@ defmodule Registry do
|> List.wrap()
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
{:duplicate, 1, key_ets} ->
{{:duplicate, _}, 1, key_ets} ->
key_ets
|> safe_lookup_second(key)
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
{:duplicate, partitions, _} ->
{{:duplicate, _}, partitions, _} ->
if Keyword.get(opts, :parallel, false) do
registry
|> dispatch_parallel(key, mfa_or_fun, partitions)
@@ -625,10 +670,14 @@ defmodule Registry do
[]
end
{:duplicate, 1, key_ets} ->
{{:duplicate, _}, 1, key_ets} ->
safe_lookup_second(key_ets, key)
{:duplicate, partitions, _key_ets} ->
{{:duplicate, :key}, partitions, _key_ets} ->
partition = hash(key, partitions)
safe_lookup_second(key_ets!(registry, partition), key)
{{:duplicate, :pid}, partitions, _key_ets} ->
for partition <- 0..(partitions - 1),
pair <- safe_lookup_second(key_ets!(registry, partition), key),
do: pair
@@ -749,10 +798,10 @@ defmodule Registry do
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select(key_ets, spec)
{:duplicate, 1, key_ets} ->
{{:duplicate, _}, 1, key_ets} ->
:ets.select(key_ets, spec)
{:duplicate, partitions, _key_ets} ->
{{:duplicate, _}, partitions, _key_ets} ->
for partition <- 0..(partitions - 1),
pair <- :ets.select(key_ets!(registry, partition), spec),
do: pair
@@ -795,15 +844,34 @@ defmodule Registry do
@spec keys(registry, pid) :: [key]
def keys(registry, pid) when is_atom(registry) and is_pid(pid) do
{kind, partitions, _, pid_ets, _} = info!(registry)
{_, pid_ets} = pid_ets || pid_ets!(registry, pid, partitions)
pid_etses =
if pid_ets do
{_, pid_ets} = pid_ets
[pid_ets]
else
case kind do
{:duplicate, :key} ->
for partition <- 0..(partitions - 1) do
{_, pid_ets} = pid_ets!(registry, partition)
pid_ets
end
_ ->
{_, pid_ets} = pid_ets!(registry, pid, partitions)
[pid_ets]
end
end
keys =
try do
spec = [{{pid, :"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}]
:ets.select(pid_ets, spec)
catch
:error, :badarg -> []
end
Enum.flat_map(pid_etses, fn pid_ets ->
try do
spec = [{{pid, :"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}]
:ets.select(pid_ets, spec)
catch
:error, :badarg -> []
end
end)
# Handle the possibility of fake keys
keys = gather_keys(keys, [], false)
@@ -882,8 +950,17 @@ defmodule Registry do
[]
end
{:duplicate, partitions, key_ets} ->
key_ets = key_ets || key_ets!(registry, pid, partitions)
{{:duplicate, _}, 1, key_ets} ->
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
{{:duplicate, :key}, partitions, _key_ets} ->
partition = hash(key, partitions)
key_ets = key_ets!(registry, partition)
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
{{:duplicate, :pid}, partitions, _key_ets} ->
partition = hash(pid, partitions)
key_ets = key_ets!(registry, partition)
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
end
end
@@ -1121,7 +1198,7 @@ defmodule Registry do
end
end
defp register_key(:duplicate, key_ets, _key, entry) do
defp register_key({:duplicate, _}, key_ets, _key, entry) do
true = :ets.insert(key_ets, entry)
:ok
end
@@ -1339,10 +1416,10 @@ defmodule Registry do
key_ets = key_ets || key_ets!(registry, key, partitions)
:ets.select_count(key_ets, spec)
{:duplicate, 1, key_ets} ->
{{:duplicate, _}, 1, key_ets} ->
:ets.select_count(key_ets, spec)
{:duplicate, partitions, _key_ets} ->
{{:duplicate, _}, partitions, _key_ets} ->
Enum.sum_by(0..(partitions - 1), fn partition_index ->
:ets.select_count(key_ets!(registry, partition_index), spec)
end)
@@ -1512,7 +1589,12 @@ defmodule Registry do
{hash(key, partitions), hash(pid, partitions)}
end
defp partitions(:duplicate, _key, pid, partitions) do
defp partitions({:duplicate, :key}, key, _pid, partitions) do
partition = hash(key, partitions)
{partition, partition}
end
defp partitions({:duplicate, :pid}, _key, pid, partitions) do
partition = hash(pid, partitions)
{partition, partition}
end
@@ -1576,9 +1658,10 @@ defmodule Registry.Supervisor do
defp strategy_for_kind(:unique), do: :one_for_all
# Duplicate registries have both key and pid partitions hashed
# by pid. This means that, if a PID partition crashes, all of
# by key ({:duplicate, :key}) or pid ({:duplicate, :pid}).
# This means that, if a PID or key partition crashes, all of
# its associated entries are in its sibling table, so we crash one.
defp strategy_for_kind(:duplicate), do: :one_for_one
defp strategy_for_kind({:duplicate, _}), do: :one_for_one
end
defmodule Registry.Partition do
@@ -1633,6 +1716,7 @@ defmodule Registry.Partition do
def init({kind, registry, i, partitions, key_partition, pid_partition, listeners, compressed}) do
Process.flag(:trap_exit, true)
key_ets = init_key_ets(kind, key_partition, compressed)
pid_ets = init_pid_ets(kind, pid_partition)
@@ -1659,7 +1743,7 @@ defmodule Registry.Partition do
:ets.new(key_partition, compression_opt(opts, compressed))
end
defp init_key_ets(:duplicate, key_partition, compressed) do
defp init_key_ets({:duplicate, _}, key_partition, compressed) do
opts = [:duplicate_bag, :public, read_concurrency: true, write_concurrency: true]
:ets.new(key_partition, compression_opt(opts, compressed))
end
+22 -22
View File
@@ -964,17 +964,13 @@ defmodule Stream do
after_fun.(user_acc)
:erlang.raise(kind, reason, __STACKTRACE__)
else
{:suspended, [val], next} ->
do_transform_user(val, user_acc, :cont, next, inner_acc, funs)
{:suspended, vals, next} ->
do_transform_user(:lists.reverse(vals), user_acc, :cont, next, inner_acc, funs)
{_, result} ->
{_, vals} ->
# Do not attempt to call the resource again, it has either done or halted
next = fn _ -> {:done, []} end
case result do
[val] -> do_transform_user(val, user_acc, :last, next, inner_acc, funs)
[] -> do_transform(user_acc, :last, next, inner_acc, funs)
end
do_transform_user(:lists.reverse(vals), user_acc, :last, next, inner_acc, funs)
end
end
@@ -989,7 +985,7 @@ defmodule Stream do
after_fun.(user_acc)
:erlang.raise(kind, reason, __STACKTRACE__)
else
result -> do_transform_result(result, :halt, next, inner_acc, funs)
result -> do_transform_result(result, [], :halt, next, inner_acc, funs)
end
else
do_transform(user_acc, :halt, next, inner_acc, funs)
@@ -1002,7 +998,11 @@ defmodule Stream do
{:halted, elem(inner_acc, 1)}
end
defp do_transform_user(val, user_acc, next_op, next, inner_acc, funs) do
defp do_transform_user([], user_acc, next_op, next, inner_acc, funs) do
do_transform(user_acc, next_op, next, inner_acc, funs)
end
defp do_transform_user([val | vals], user_acc, next_op, next, inner_acc, funs) do
{user, _, _, _, after_fun} = funs
try do
@@ -1013,20 +1013,20 @@ defmodule Stream do
after_fun.(user_acc)
:erlang.raise(kind, reason, __STACKTRACE__)
else
result -> do_transform_result(result, next_op, next, inner_acc, funs)
result -> do_transform_result(result, vals, next_op, next, inner_acc, funs)
end
end
defp do_transform_result(result, next_op, next, inner_acc, funs) do
defp do_transform_result(result, vals, next_op, next, inner_acc, funs) do
{_, fun, inner, _, after_fun} = funs
case result do
{[], user_acc} ->
do_transform(user_acc, next_op, next, inner_acc, funs)
do_transform_user(vals, user_acc, next_op, next, inner_acc, funs)
{list, user_acc} when is_list(list) ->
reduce = &Enumerable.List.reduce(list, &1, fun)
do_transform_inner_list(user_acc, next_op, next, inner_acc, reduce, funs)
do_transform_inner_list(vals, user_acc, next_op, next, inner_acc, reduce, funs)
{:halt, user_acc} ->
next.({:halt, []})
@@ -1035,11 +1035,11 @@ defmodule Stream do
{other, user_acc} ->
reduce = &Enumerable.reduce(other, &1, inner)
do_transform_inner_enum(user_acc, next_op, next, inner_acc, reduce, funs)
do_transform_inner_enum(vals, user_acc, next_op, next, inner_acc, reduce, funs)
end
end
defp do_transform_inner_list(user_acc, next_op, next, inner_acc, reduce, funs) do
defp do_transform_inner_list(vals, user_acc, next_op, next, inner_acc, reduce, funs) do
{_, _, _, _, after_fun} = funs
try do
@@ -1051,7 +1051,7 @@ defmodule Stream do
:erlang.raise(kind, reason, __STACKTRACE__)
else
{:done, acc} ->
do_transform(user_acc, next_op, next, {:cont, acc}, funs)
do_transform_user(vals, user_acc, next_op, next, {:cont, acc}, funs)
{:halted, acc} ->
next.({:halt, []})
@@ -1059,12 +1059,12 @@ defmodule Stream do
{:halted, acc}
{:suspended, acc, continuation} ->
resume = &do_transform_inner_list(user_acc, next_op, next, &1, continuation, funs)
resume = &do_transform_inner_list(vals, user_acc, next_op, next, &1, continuation, funs)
{:suspended, acc, resume}
end
end
defp do_transform_inner_enum(user_acc, next_op, next, {op, inner_acc}, reduce, funs) do
defp do_transform_inner_enum(vals, user_acc, next_op, next, {op, inner_acc}, reduce, funs) do
{_, _, _, _, after_fun} = funs
try do
@@ -1078,7 +1078,7 @@ defmodule Stream do
# The user wanted to cont/suspend but the stream halted,
# so we continue with the user intention.
{:halted, [inner_op | acc]} when op != :halt and inner_op != :halt ->
do_transform(user_acc, next_op, next, {inner_op, acc}, funs)
do_transform_user(vals, user_acc, next_op, next, {inner_op, acc}, funs)
{:halted, [_ | acc]} ->
next.({:halt, []})
@@ -1086,10 +1086,10 @@ defmodule Stream do
{:halted, acc}
{:done, [_ | acc]} ->
do_transform(user_acc, next_op, next, {:cont, acc}, funs)
do_transform_user(vals, user_acc, next_op, next, {:cont, acc}, funs)
{:suspended, [_ | acc], continuation} ->
resume = &do_transform_inner_enum(user_acc, next_op, next, &1, continuation, funs)
resume = &do_transform_inner_enum(vals, user_acc, next_op, next, &1, continuation, funs)
{:suspended, acc, resume}
end
end
+6 -6
View File
@@ -448,17 +448,17 @@ defmodule Task do
the code before async/await has the same properties after you
add the async call. For example, imagine you have this:
x = heavy_fun()
y = some_fun()
x = heavy_function()
y = some_function()
x + y
Now you want to make the `heavy_fun()` async:
Now you want to make the `heavy_function()` async:
x = Task.async(&heavy_fun/0)
y = some_fun()
x = Task.async(&heavy_function/0)
y = some_function()
Task.await(x) + y
As before, if `heavy_fun/0` fails, the whole computation will
As before, if `heavy_function/0` fails, the whole computation will
fail, including the caller process. If you don't want the task
to fail then you must change the `heavy_fun/0` code in the
same way you would achieve it if you didn't have the async call.
@@ -320,7 +320,7 @@ end
In the previous example, even though Elixir does not know which modules the function `example/0` was invoked on, it knows the modules `OtherModule.Foo` and `OtherModule.Bar` are referred outside of a function and therefore they become compile-time dependencies. If any of them change, Elixir will recompile `MyModule` itself.
However, you should not programatically generate the module names themselves, as that would make it impossible for Elixir to track them. More precisely, do not do this:
However, you should not programmatically generate the module names themselves, as that would make it impossible for Elixir to track them. More precisely, do not do this:
```elixir
defmodule MyModule do
@@ -348,7 +348,7 @@ end
#### Refactoring
To address this anti-pattern, you should avoid defining module names programatically. For example, if you need to dispatch to multiple modules, do so by using full module names.
To address this anti-pattern, you should avoid defining module names programmatically. For example, if you need to dispatch to multiple modules, do so by using full module names.
Instead of:
@@ -50,13 +50,13 @@ after: [2, 4, 6]
It is also very common to use `IO.inspect/2` with `binding/0`, which returns all variable names and their values:
```elixir
def some_fun(a, b, c) do
def some_function(a, b, c) do
IO.inspect(binding())
...
end
```
When `some_fun/3` is invoked with `:foo`, `"bar"`, `:baz` it prints:
When `some_function/3` is invoked with `:foo`, `"bar"`, `:baz` it prints:
```elixir
[a: :foo, b: "bar", c: :baz]
+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:
+1 -1
View File
@@ -207,4 +207,4 @@ end
When a long action is performed on the server, all other requests to that particular server will wait until the action is done, which may cause some clients to timeout.
Some APIs, such as GenServers, make a clearer distiction between client and server, and we will explore them in future chapters. Next let's talk about naming things, applications, and supervisors.
Some APIs, such as GenServers, make a clearer distinction between client and server, and we will explore them in future chapters. Next let's talk about naming things, applications, and supervisors.
@@ -181,7 +181,7 @@ end
Now run `mix test` again and our app should boot but we should see one failure. When we changed the `KV` module, we broke the boilerplate test case which tested the `KV.hello/0` function. You can simply remove that test case and we are back to a green suite.
We wrote very little code but we did something incredibly powerful. We now have a function, `KV.start/2` that is invoked whenever your application starts. This gives us the perfect place to start our key-value registry. The `Application` module also allows us to define a `stop/1` callback and other funtionality. You can check the `Application` and `Supervisor` modules for extensive documentation on their uses.
We wrote very little code but we did something incredibly powerful. We now have a function, `KV.start/2` that is invoked whenever your application starts. This gives us the perfect place to start our key-value registry. The `Application` module also allows us to define a `stop/1` callback and other functionality. You can check the `Application` and `Supervisor` modules for extensive documentation on their uses.
Let's finally start our registry.
@@ -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)
@@ -142,11 +142,13 @@ Inferring type signatures comes with a series of trade-offs:
* Cascading errors - when a user accidentally makes type errors or the code has conflicting assumptions, type inference may lead to less clear error messages as the type system tries to reconcile diverging type assumptions across code paths.
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir aims to provide "module type inference": our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies (in the future). Calls to modules within the same project are assumed to be `dynamic()` as to reduce cyclic dependencies and the need for recompilations. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, we are exploring "module type inference": our goal is to infer type signatures considering invocations of functions in the same module and of functions from *other applications* (such as Elixir itself and your dependencies). Once module types are inferred, your whole project is type checked considering all declared and inferred types.
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing without requiring any effort from them.
We have successfully implemented these features as part of Elixir v1.19, by performing inference of all constructs (except guards), taking into account the signatures from calls to functions within the same module and in Elixir's standard library.
In the long term, Elixir developers who want typing guarantees must explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages, without performing type inference. In summary, type checking will rely on type signatures and only fallback to inferred types when no signature is available.
In future releases, we plan to perform type inference of guards and also consider the type signatures of your dependencies during inference. Overall, our goal with type inference is to find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing without requiring any effort from them.
Keep in mind this only applies to *type inference*. Once we introduce type signatures and you explicitly annotate your functions, type inference and the trade-offs above no longer play a role. Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages.
## Roadmap
@@ -83,7 +83,7 @@ iex> _
** (CompileError) iex:3: invalid use of _
```
A pinned value represents the value itself and not its – even if syntatically equal – pattern. The right hand side is compared to be equal to the pinned value:
A pinned value represents the value itself and not its – even if syntactically equal – pattern. The right hand side is compared to be equal to the pinned value:
```iex
iex> x = %{}
+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}]},
+2 -2
View File
@@ -23,7 +23,7 @@
'MACRO-defmacrop'(Caller, Call, Expr) -> define(Caller, defmacrop, Call, Expr).
'MACRO-defmodule'({Line, _S, _E} = _Caller, Alias, [{do, Block}]) ->
Escaped = elixir_quote:escape(Block, none, false),
Escaped = elixir_quote:escape(Block, escape, false),
Args = [[{line, Line}], Alias, Escaped, [], false, env()],
{{'.', [], [elixir_module, compile]}, [], Args}.
@@ -46,7 +46,7 @@ define({Line, _S, #{module := Module} = E}, Kind, Call, Expr) ->
Store =
case UC or UE of
true ->
elixir_quote:escape({Call, Expr}, none, true);
elixir_quote:escape({Call, Expr}, escape, true);
false ->
Key = erlang:unique_integer(),
+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]
+2 -4
View File
@@ -109,11 +109,9 @@ emit_diagnostic(Severity, Position, File, Message, Stacktrace, Options) ->
case get(elixir_code_diagnostics) of
undefined ->
StoreDiagnostic = proplists:get_value(store_diagnostic, Options, true),
case get(elixir_compiler_info) of
{CompilerPid, _} when StoreDiagnostic -> CompilerPid ! {diagnostic, Diagnostic, ReadSnippet};
_ -> print_diagnostic(Diagnostic, ReadSnippet)
undefined -> print_diagnostic(Diagnostic, ReadSnippet);
{CompilerPid, _} -> CompilerPid ! {diagnostic, Diagnostic, ReadSnippet}
end;
{Tail, true} ->
+2 -1
View File
@@ -257,11 +257,12 @@ expand({quote, Meta, [Opts, Do]}, S, E) when is_list(Do) ->
{EContext, SC, EC} = expand(QContext, SP, EP),
Quoted = elixir_quote:quote(Exprs, Q),
{EQuoted, ES, EQ} = expand(Quoted, SC, EC),
BindingMeta = lists:keydelete(column, 1, Meta),
EBinding =
[{'{}', [],
['=', [], [
{'{}', [], [K, Meta, EContext]},
{'{}', [], [K, BindingMeta, EContext]},
V
]
]} || {K, V} <- Binding],
+1 -1
View File
@@ -37,7 +37,7 @@ expand_struct(Meta, Left, {'%{}', MapMeta, MapArgs}, S, #{context := Context} =
Struct = load_struct(Meta, ELeft, Assocs, EE),
Keys = ['__struct__'] ++ AssocKeys,
WithoutKeys = lists:sort(maps:to_list(maps:without(Keys, Struct))),
StructAssocs = elixir_quote:escape(WithoutKeys, none, false),
StructAssocs = elixir_quote:escape(WithoutKeys, escape, false),
{{'%', Meta, [ELeft, {'%{}', MapMeta, StructAssocs ++ Assocs}]}, SE, EE};
{'%{}', MapMeta, Assocs} ->
+7 -10
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),
@@ -375,9 +374,7 @@ check_module_availability(Module, Line, E) ->
false ->
case code:ensure_loaded(Module) of
{module, _} ->
%% This diagnostic is by definition temporary
WarnMeta = [{store_diagnostic, false}, {line, Line}],
elixir_errors:file_warn(WarnMeta, E, ?MODULE, {module_defined, Module});
elixir_errors:file_warn([{line, Line}], E, ?MODULE, {module_defined, Module});
{error, _} ->
ok
end;
@@ -583,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,
+47 -52
View File
@@ -18,7 +18,7 @@
line=false,
file=nil,
context=nil,
op=escape, % escape | escape_and_prune | quote
op=escape, % escape | escape_and_prune | {struct, Module} | quote
aliases_hygiene=nil,
imports_hygiene=nil,
unquote=true,
@@ -140,27 +140,33 @@ do_tuple_linify(Fun, Meta, Left, Right, Var) ->
%% Escapes the given expression. It is similar to quote, but
%% lines are kept and hygiene mechanisms are disabled.
escape(Expr, Op, Unquote) ->
Q = #elixir_quote{
line=true,
file=nil,
op=Op,
unquote=Unquote
},
case Unquote of
true -> do_quote(Expr, Q);
false -> do_escape(Expr, Q)
try
do_quote(Expr, #elixir_quote{
line=true,
file=nil,
op=Op,
unquote=Unquote
})
catch
Kind:Reason:Stacktrace ->
Pruned = lists:dropwhile(fun
({?MODULE, _, _, _}) -> true;
(_) -> false
end, Stacktrace),
erlang:raise(Kind, Reason, Pruned)
end.
do_escape({Left, Meta, Right}, #elixir_quote{op=escape_and_prune} = Q) when is_list(Meta) ->
TM = [{K, V} || {K, V} <- Meta, (K == no_parens) orelse (K == line) orelse (K == delimiter)],
TL = do_escape(Left, Q),
TR = do_escape(Right, Q),
TL = do_quote(Left, Q),
TR = do_quote(Right, Q),
{'{}', [], [TL, TM, TR]};
do_escape({Left, Right}, Q) ->
{do_escape(Left, Q), do_escape(Right, Q)};
{do_quote(Left, Q), do_quote(Right, Q)};
do_escape(Tuple, Q) when is_tuple(Tuple) ->
TT = do_escape(tuple_to_list(Tuple), Q),
TT = do_quote(tuple_to_list(Tuple), Q),
{'{}', [], TT};
do_escape(BitString, _) when is_bitstring(BitString) ->
@@ -176,17 +182,31 @@ do_escape(Map, Q) when is_map(Map) ->
maybe
#{'__struct__' := Module} ?= Map,
true ?= is_atom(Module),
% We never escape ourselves (it can only happen during Elixir bootstrapping)
true ?= (Q#elixir_quote.op /= {struct, Module}),
{module, Module} ?= code:ensure_loaded(Module),
true ?= erlang:function_exported(Module, '__escape__', 1),
Expr = Module:'__escape__'(Map),
case shallow_valid_ast(Expr) of
true -> Expr;
false -> argument_error(
<<('Elixir.Kernel':inspect(Module))/binary, ".__escape__/1 returned invalid AST: ", ('Elixir.Kernel':inspect(Expr))/binary>>)
case Q#elixir_quote.op of
{struct, _Module} ->
argument_error(<<('Elixir.Kernel':inspect(Module))/binary,
" defines custom escaping rules which are not supported in struct defaults",
(bad_escape_hint())/binary>>);
_ ->
Expr = Module:'__escape__'(Map),
case shallow_valid_ast(Expr) of
true -> Expr;
false -> argument_error(
<<('Elixir.Kernel':inspect(Module))/binary, ".__escape__/1 returned invalid AST: ", ('Elixir.Kernel':inspect(Expr))/binary>>
)
end
end
else
_ ->
TT = [escape_map_key_value(K, V, Map, Q) || {K, V} <- lists:sort(maps:to_list(Map))],
TT = [
{do_quote(K, Q), do_quote(V, Q)}
|| {K, V} <- lists:sort(maps:to_list(Map))
],
{'%{}', [], TT}
end;
@@ -194,11 +214,7 @@ do_escape([], _) ->
[];
do_escape([H | T], #elixir_quote{unquote=false} = Q) ->
case is_list(T) of
true -> [do_escape(H, Q) | do_escape(T, Q)];
% improper list
false -> [{'|', [], [do_escape(H, Q), do_escape(T, Q)]}]
end;
do_quote_simple_list(T, do_escape(H, Q), Q);
do_escape([H | T], Q) ->
%% The improper case is inefficient, but improper lists are rare.
@@ -208,7 +224,7 @@ do_escape([H | T], Q) ->
_:_ ->
{L, R} = reverse_improper(T, [H]),
TL = do_quote_splice(L, Q, [], []),
TR = do_escape(R, Q),
TR = do_quote(R, Q),
update_last(TL, fun(X) -> {'|', [], [X, TR]} end)
end;
@@ -225,34 +241,12 @@ do_escape(Fun, _) when is_function(Fun) ->
do_escape(Other, _) ->
bad_escape(Other).
escape_map_key_value(K, V, Map, Q) ->
MaybeRef = if
is_reference(V) -> V;
is_tuple(V) -> find_tuple_ref(V, 1);
true -> nil
end,
if
is_reference(MaybeRef) ->
argument_error(<<('Elixir.Kernel':inspect(Map, []))/binary, " contains a reference (",
('Elixir.Kernel':inspect(MaybeRef, []))/binary, ") and therefore it cannot be escaped ",
"(it must be defined within a function instead). ", (bad_escape_hint())/binary>>);
true ->
{do_escape(K, Q), do_escape(V, Q)}
end.
find_tuple_ref(Tuple, Index) when Index > tuple_size(Tuple) -> nil;
find_tuple_ref(Tuple, Index) ->
case element(Index, Tuple) of
Ref when is_reference(Ref) -> Ref;
_ -> find_tuple_ref(Tuple, Index + 1)
end.
bad_escape(Arg) ->
argument_error(<<"cannot escape ", ('Elixir.Kernel':inspect(Arg, []))/binary, ". ",
argument_error(<<"cannot escape ", ('Elixir.Kernel':inspect(Arg, []))/binary,
(bad_escape_hint())/binary>>).
bad_escape_hint() ->
<<"The supported values are: lists, tuples, maps, atoms, numbers, bitstrings, ",
<<". The supported values are: lists, tuples, maps, atoms, numbers, bitstrings, ",
"PIDs and remote functions in the format &Mod.fun/arity">>.
%% Quote entry points
@@ -363,7 +357,6 @@ do_quote({quote, Meta, [Opts, Arg]}, Q) when is_list(Meta) ->
{'{}', [], [quote, meta(NewMeta, Q), [TOpts, TArg]]};
%
do_quote({unquote, Meta, [Expr]}, #elixir_quote{unquote=true, shallow_validate=Validate}) when is_list(Meta) ->
case Validate of
true -> {{'.', Meta, [?MODULE, shallow_validate_ast]}, Meta, [Expr]};
@@ -600,8 +593,10 @@ argument_error(Message) ->
%% Helpers
meta(Meta, #elixir_quote{op=quote} = Q) ->
generated(keep(keydelete(column, Meta), Q), Q);
meta(Meta, Q) ->
generated(keep(keydelete(column, Meta), Q), Q).
do_quote(Meta, Q).
generated(Meta, #elixir_quote{generated=true}) -> [{generated, true} | Meta];
generated(Meta, #elixir_quote{generated=false}) -> Meta.
+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
+17 -16
View File
@@ -608,17 +608,17 @@ defmodule ExceptionTest do
assert blame_message(Enum, & &1.map(:ok)) == """
function Enum.map/1 is undefined or private. Did you mean:
* map/2
* map/2
"""
assert blame_message(Enum, & &1.man(:ok)) == """
function Enum.man/1 is undefined or private. Did you mean:
* map/2
* max/1
* max/2
* max/3
* min/1
* map/2
* max/1
* max/2
* max/3
* min/1
"""
message = blame_message(:erlang, & &1.gt_cookie())
@@ -731,22 +731,23 @@ defmodule ExceptionTest do
test "annotates key error with suggestions if keys are atoms" do
message = blame_message(%{first: nil, second: nil}, fn map -> map.firts end)
assert message == """
key :firts not found in:
assert message ==
"""
key :firts not found in:
%{first: nil, second: nil}
%{first: nil, second: nil}
Did you mean:
Did you mean:
* :first
"""
"""
message = blame_message(%{"first" => nil, "second" => nil}, fn map -> map.firts end)
assert message == """
key :firts not found in:
%{"first" => nil, "second" => nil}\
%{"first" => nil, "second" => nil}
"""
message =
@@ -756,7 +757,7 @@ defmodule ExceptionTest do
"""
key "firts" not found in:
%{"first" => nil, "second" => nil}\
%{"first" => nil, "second" => nil}
"""
message =
@@ -786,9 +787,9 @@ defmodule ExceptionTest do
Did you mean:
* :created_at
* :finished_at
* :started_at
* :created_at
* :finished_at
* :started_at
"""
end
+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))
@@ -435,7 +435,7 @@ defmodule Kernel.ErrorsTest do
end
test "invalid struct field value" do
msg = ~r"invalid value for struct field baz, cannot escape "
msg = ~r"invalid default value for struct field baz, cannot escape "
assert_raise ArgumentError, msg, fn ->
defmodule InvalidStructFieldValue do
+6 -6
View File
@@ -459,7 +459,7 @@ defmodule Kernel.RaiseTest do
no match of right hand side value:
[1000000, 1000001, 1000002, 1000003, 1000004, 1000005, 1000006, 1000007,
1000008, 1000009]\
1000008, 1000009]
"""
end
@@ -480,7 +480,7 @@ defmodule Kernel.RaiseTest do
x in [KeyError] -> Exception.message(x)
end
assert result == "key :foo not found in:\n\n %{}"
assert result == "key :foo not found in:\n\n %{}\n"
end
test "bad map error" do
@@ -491,7 +491,7 @@ defmodule Kernel.RaiseTest do
x in [BadMapError] -> Exception.message(x)
end
assert result == "expected a map, got:\n\n 0"
assert result == "expected a map, got:\n\n 0\n"
end
test "bad boolean error" do
@@ -502,7 +502,7 @@ defmodule Kernel.RaiseTest do
x in [BadBooleanError] -> Exception.message(x)
end
assert result == "expected a boolean on left-side of \"and\", got:\n\n 1"
assert result == "expected a boolean on left-side of \"and\", got:\n\n 1\n"
end
test "case clause error" do
@@ -517,7 +517,7 @@ defmodule Kernel.RaiseTest do
x in [CaseClauseError] -> Exception.message(x)
end
assert result == "no case clause matching:\n\n 0"
assert result == "no case clause matching:\n\n 0\n"
end
test "cond clause error" do
@@ -549,7 +549,7 @@ defmodule Kernel.RaiseTest do
x in [TryClauseError] -> Exception.message(x)
end
assert result == "no try clause matching:\n\n :example"
assert result == "no try clause matching:\n\n :example\n"
end
test "undefined function error as Erlang error" do
@@ -1953,6 +1953,18 @@ defmodule Kernel.WarningTest do
)
end
test "do+end with not in operator without explicit parentheses" do
assert_warn_eval(
["nofile:3\n", "missing parentheses on expression following operator \"not in\""],
"""
quote do
case do
end not in no_parens 1, 2
end
"""
)
end
test "variable is being expanded to function call (on_undefined_variable: warn)" do
capture_io(:stderr, fn ->
Code.put_compiler_option(:on_undefined_variable, :warn)
+1 -1
View File
@@ -153,7 +153,7 @@ defmodule Kernel.WithTest do
end
test "else conditions with match error" do
assert_raise WithClauseError, "no with clause matching:\n\n :error", fn ->
assert_raise WithClauseError, "no with clause matching:\n\n :error\n", fn ->
with({:ok, res} <- error(), do: res, else: ({:error, error} -> error))
end
end
+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 ->
+2 -2
View File
@@ -70,11 +70,11 @@ defmodule KeywordTest do
assert Keyword.replace!([a: 1, b: 2, a: 3, b: 4], :a, 1) == [a: 1, b: 2, b: 4]
assert Keyword.replace!([a: 1, b: 2, c: 3, b: 4], :b, :new) == [a: 1, b: :new, c: 3]
assert_raise KeyError, "key :b not found in:\n\n []", fn ->
assert_raise KeyError, "key :b not found in:\n\n []\n", fn ->
Keyword.replace!([], :b, :new)
end
assert_raise KeyError, "key :c not found in:\n\n [a: 1, b: 2, a: 3]", fn ->
assert_raise KeyError, "key :c not found in:\n\n [a: 1, b: 2, a: 3]\n", fn ->
Keyword.replace!([a: 1, b: 2, a: 3], :c, :new)
end
end
+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
+18 -6
View File
@@ -82,6 +82,9 @@ defmodule MacroTest do
contents = quote(unquote: false, do: unquote(x))
assert Macro.escape(contents, unquote: true) == {:x, [], MacroTest}
contents = %{foo: quote(unquote: false, do: unquote(1))}
assert Macro.escape(contents, unquote: true) == {:%{}, [], [foo: 1]}
end
test "with generated" do
@@ -97,6 +100,7 @@ defmodule MacroTest do
test "with remote unquote" do
contents = quote(unquote: false, do: Kernel.unquote(:is_atom)(:ok))
assert eval_escaped(contents) == quote(do: Kernel.is_atom(:ok))
assert eval_escaped(%{foo: contents}) == %{foo: quote(do: Kernel.is_atom(:ok))}
end
test "with nested unquote" do
@@ -140,6 +144,9 @@ defmodule MacroTest do
quote(unquote: false, do: [1, unquote_splicing([2]), 3, unquote_splicing([4]) | [5]])
assert eval_escaped(contents) == [1, 2, 3, 4, 5]
contents = %{foo: quote(unquote: false, do: [1, 2, unquote_splicing([3, 4, 5])])}
assert eval_escaped(contents) == %{foo: [1, 2, 3, 4, 5]}
end
test "does not add context to quote" do
@@ -154,12 +161,6 @@ defmodule MacroTest do
[:foo, {:{}, [], [:quote, [{:%{}, [], []}], [{:{}, [], []}]]}]
end
test "escape container when a reference cannot be escaped" do
assert_raise ArgumentError, ~r"contains a reference", fn ->
Macro.escape(%{re_pattern: {:re_pattern, 0, 0, 0, make_ref()}})
end
end
@tag :re_import
test "escape regex will remove references and replace it by a call to :re.import/1" do
assert {
@@ -176,6 +177,17 @@ defmodule MacroTest do
} = Macro.escape(~r/foo/)
end
@tag :re_import
test "escape raises within structs fields" do
assert_raise ArgumentError,
~r"Regex defines custom escaping rules which are not supported in struct defaults",
fn ->
defmodule Test do
defstruct my_regex: ~r/^hi$/
end
end
end
defmodule EscapedStruct do
defstruct [:ast, :ref]
+1 -1
View File
@@ -229,7 +229,7 @@ defmodule MapTest do
Map.replace!(map, :x, 10)
end
assert_raise KeyError, "key :x not found in:\n\n %{}", fn ->
assert_raise KeyError, "key :x not found in:\n\n %{}\n", fn ->
Map.replace!(%{}, :x, 10)
end
end
@@ -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))
@@ -2505,5 +2503,110 @@ defmodule Module.Types.DescrTest do
refute subtype?(map1, map2)
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 =
[
closed_map(__struct__: atom([MapSet]), map: term()),
closed_map(__struct__: atom([Jason.OrderedObject]), values: term()),
closed_map(__struct__: atom([GenEvent.Stream]), timeout: term(), manager: term()),
closed_map(__struct__: atom([HashDict]), size: term(), root: term()),
closed_map(__struct__: atom([HashSet]), size: term(), root: term()),
closed_map(
__struct__: atom([IO.Stream]),
raw: term(),
device: term(),
line_or_bytes: term()
),
closed_map(__struct__: atom([Range]), first: term(), last: term(), step: term()),
closed_map(
__struct__: atom([Stream]),
enum: term(),
done: term(),
funs: term(),
accs: term()
),
closed_map(
__struct__: atom([Req.Response.Async]),
pid: term(),
ref: term(),
stream_fun: term(),
cancel_fun: term()
),
closed_map(
__struct__: atom([Postgrex.Stream]),
options: term(),
params: term(),
query: term(),
conn: term()
),
closed_map(
__struct__: atom([DBConnection.PrepareStream]),
opts: term(),
params: term(),
query: term(),
conn: term()
),
closed_map(
__struct__: atom([DBConnection.Stream]),
opts: term(),
params: term(),
query: term(),
conn: term()
),
closed_map(
__struct__: atom([Ecto.Adapters.SQL.Stream]),
meta: term(),
opts: term(),
params: term(),
statement: term()
),
closed_map(
__struct__: atom([Date.Range]),
first: term(),
last: term(),
step: term(),
first_in_iso_days: term(),
last_in_iso_days: term()
),
closed_map(
__struct__: atom([File.Stream]),
node: term(),
raw: term(),
path: term(),
modes: term(),
line_or_bytes: term()
),
closed_map(
__struct__: atom([Phoenix.LiveView.LiveStream]),
name: term(),
ref: term(),
inserts: term(),
deletes: term(),
reset?: term(),
dom_id: term(),
consumable?: term()
)
]
range =
closed_map(__struct__: atom([Range]), first: integer(), last: integer(), step: integer())
assert subtype?(range, Enum.reduce(entries, &union/2))
end
end
end
@@ -964,12 +964,12 @@ defmodule Module.Types.ExprTest do
hint: given pattern matching is enough to catch typing errors, you may optionally convert the struct update into a map update. For example, instead of:
user = some_fun()
user = some_function()
%User{user | name: "John Doe"}
it is enough to write:
%User{} = user = some_fun()
%User{} = user = some_function()
%{user | name: "John Doe"}
"""
@@ -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
@@ -408,6 +408,8 @@ defmodule Module.Types.IntegrationTest do
dynamic(%Range{})
hint: defimpl for Range requires its callbacks to match exclusively on %Range{}
typing violation found at:
│
6 │ def itself(nil), do: nil
@@ -447,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:
│
@@ -476,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:
@@ -509,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
"""
]
@@ -549,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:
@@ -562,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:
@@ -570,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:
@@ -580,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
@@ -0,0 +1,504 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
Code.require_file("../test_helper.exs", __DIR__)
defmodule Registry.DuplicateTest do
use ExUnit.Case,
async: true,
parameterize:
for(
keys <- [:duplicate, {:duplicate, :pid}, {:duplicate, :key}],
partitions <- [1, 8],
do: %{keys: keys, partitions: partitions}
)
setup config do
keys = config.keys
partitions = config.partitions
listeners =
List.wrap(config[:base_listener]) |> Enum.map(&:"#{&1}_#{partitions}_#{inspect(keys)}")
name = :"#{config.test}_#{partitions}_#{inspect(keys)}"
opts = [keys: config.keys, name: name, partitions: partitions, listeners: listeners]
{:ok, _} = start_supervised({Registry, opts})
%{registry: name, listeners: listeners}
end
test "starts configured number of partitions", %{registry: registry, partitions: partitions} do
assert length(Supervisor.which_children(registry)) == partitions
end
test "counts 0 keys in an empty registry", %{registry: registry} do
assert 0 == Registry.count(registry)
end
test "counts the number of keys in a registry", %{registry: registry} do
{:ok, _} = Registry.register(registry, "hello", :value)
{:ok, _} = Registry.register(registry, "hello", :value)
assert 2 == Registry.count(registry)
end
test "has duplicate registrations", %{registry: registry} do
{:ok, pid} = Registry.register(registry, "hello", :value)
assert is_pid(pid)
assert Registry.keys(registry, self()) == ["hello"]
assert Registry.values(registry, "hello", self()) == [:value]
assert {:ok, pid} = Registry.register(registry, "hello", :value)
assert is_pid(pid)
assert Registry.keys(registry, self()) == ["hello", "hello"]
assert Registry.values(registry, "hello", self()) == [:value, :value]
{:ok, pid} = Registry.register(registry, "world", :value)
assert is_pid(pid)
assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "hello", "world"]
end
test "has duplicate registrations across processes", %{registry: registry} do
{_, task} = register_task(registry, "hello", :world)
assert Registry.keys(registry, self()) == []
assert Registry.keys(registry, task) == ["hello"]
assert Registry.values(registry, "hello", self()) == []
assert Registry.values(registry, "hello", task) == [:world]
assert {:ok, _pid} = Registry.register(registry, "hello", :value)
assert Registry.keys(registry, self()) == ["hello"]
assert Registry.values(registry, "hello", self()) == [:value]
end
test "compares using matches", %{registry: registry} do
{:ok, _} = Registry.register(registry, 1.0, :value)
{:ok, _} = Registry.register(registry, 1, :value)
assert Registry.keys(registry, self()) |> Enum.sort() == [1, 1.0]
end
test "dispatches to multiple keys in serial", %{registry: registry} do
Process.flag(:trap_exit, true)
parent = self()
fun = fn _ -> raise "will never be invoked" end
assert Registry.dispatch(registry, "hello", fun, parallel: false) == :ok
{:ok, _} = Registry.register(registry, "hello", :value1)
{:ok, _} = Registry.register(registry, "hello", :value2)
{:ok, _} = Registry.register(registry, "world", :value3)
fun = fn entries ->
assert parent == self()
for {pid, value} <- entries, do: send(pid, {:dispatch, value})
end
assert Registry.dispatch(registry, "hello", fun, parallel: false)
assert_received {:dispatch, :value1}
assert_received {:dispatch, :value2}
refute_received {:dispatch, :value3}
fun = fn entries ->
assert parent == self()
for {pid, value} <- entries, do: send(pid, {:dispatch, value})
end
assert Registry.dispatch(registry, "world", fun, parallel: false)
refute_received {:dispatch, :value1}
refute_received {:dispatch, :value2}
assert_received {:dispatch, :value3}
refute_received {:EXIT, _, _}
end
test "dispatches to multiple keys in parallel", context do
%{registry: registry, partitions: partitions} = context
Process.flag(:trap_exit, true)
parent = self()
fun = fn _ -> raise "will never be invoked" end
assert Registry.dispatch(registry, "hello", fun, parallel: true) == :ok
{:ok, _} = Registry.register(registry, "hello", :value1)
{:ok, _} = Registry.register(registry, "hello", :value2)
{:ok, _} = Registry.register(registry, "world", :value3)
fun = fn entries ->
if partitions == 8 do
assert parent != self()
else
assert parent == self()
end
for {pid, value} <- entries, do: send(pid, {:dispatch, value})
end
assert Registry.dispatch(registry, "hello", fun, parallel: true)
assert_received {:dispatch, :value1}
assert_received {:dispatch, :value2}
refute_received {:dispatch, :value3}
fun = fn entries ->
if partitions == 8 do
assert parent != self()
else
assert parent == self()
end
for {pid, value} <- entries, do: send(pid, {:dispatch, value})
end
assert Registry.dispatch(registry, "world", fun, parallel: true)
refute_received {:dispatch, :value1}
refute_received {:dispatch, :value2}
assert_received {:dispatch, :value3}
refute_received {:EXIT, _, _}
end
test "unregisters by key", %{registry: registry} do
{:ok, _} = Registry.register(registry, "hello", :value)
{:ok, _} = Registry.register(registry, "hello", :value)
{:ok, _} = Registry.register(registry, "world", :value)
assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "hello", "world"]
:ok = Registry.unregister(registry, "hello")
assert Registry.keys(registry, self()) == ["world"]
:ok = Registry.unregister(registry, "world")
assert Registry.keys(registry, self()) == []
end
test "unregisters with no entries", %{registry: registry} do
assert Registry.unregister(registry, "hello") == :ok
end
test "unregisters with tricky keys", %{registry: registry} do
{:ok, _} = Registry.register(registry, :_, :foo)
{:ok, _} = Registry.register(registry, :_, :bar)
{:ok, _} = Registry.register(registry, "hello", "a")
{:ok, _} = Registry.register(registry, "hello", "b")
Registry.unregister(registry, :_)
assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "hello"]
end
test "supports match patterns", %{registry: registry} do
value1 = {1, :atom, 1}
value2 = {2, :atom, 2}
{:ok, _} = Registry.register(registry, "hello", value1)
{:ok, _} = Registry.register(registry, "hello", value2)
assert Registry.match(registry, "hello", {1, :_, :_}) == [{self(), value1}]
assert Registry.match(registry, "hello", {1.0, :_, :_}) == []
assert Registry.match(registry, "hello", {:_, :atom, :_}) |> Enum.sort() ==
[{self(), value1}, {self(), value2}]
assert Registry.match(registry, "hello", {:"$1", :_, :"$1"}) |> Enum.sort() ==
[{self(), value1}, {self(), value2}]
assert Registry.match(registry, "hello", {2, :_, :_}) == [{self(), value2}]
assert Registry.match(registry, "hello", {2.0, :_, :_}) == []
end
test "supports guards", %{registry: registry} do
value1 = {1, :atom, 1}
value2 = {2, :atom, 2}
{:ok, _} = Registry.register(registry, "hello", value1)
{:ok, _} = Registry.register(registry, "hello", value2)
assert Registry.match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 2}]) ==
[{self(), value1}]
assert Registry.match(registry, "hello", {:"$1", :_, :_}, [{:>, :"$1", 3}]) == []
assert Registry.match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 3}]) |> Enum.sort() ==
[{self(), value1}, {self(), value2}]
assert Registry.match(registry, "hello", {:_, :"$1", :_}, [{:is_atom, :"$1"}])
|> Enum.sort() == [{self(), value1}, {self(), value2}]
end
test "count_match supports match patterns", %{registry: registry} do
value = {1, :atom, 1}
{:ok, _} = Registry.register(registry, "hello", value)
assert 1 == Registry.count_match(registry, "hello", {1, :_, :_})
assert 0 == Registry.count_match(registry, "hello", {1.0, :_, :_})
assert 1 == Registry.count_match(registry, "hello", {:_, :atom, :_})
assert 1 == Registry.count_match(registry, "hello", {:"$1", :_, :"$1"})
assert 1 == Registry.count_match(registry, "hello", :_)
assert 0 == Registry.count_match(registry, :_, :_)
value2 = %{a: "a", b: "b"}
{:ok, _} = Registry.register(registry, "world", value2)
assert 1 == Registry.count_match(registry, "world", %{b: "b"})
end
test "count_match supports guard conditions", %{registry: registry} do
value = {1, :atom, 2}
{:ok, _} = Registry.register(registry, "hello", value)
assert 1 == Registry.count_match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 1}])
assert 0 == Registry.count_match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 2}])
assert 1 == Registry.count_match(registry, "hello", {:_, :"$1", :_}, [{:is_atom, :"$1"}])
end
test "unregister_match supports patterns", %{registry: registry} do
value1 = {1, :atom, 1}
value2 = {2, :atom, 2}
{:ok, _} = Registry.register(registry, "hello", value1)
{:ok, _} = Registry.register(registry, "hello", value2)
Registry.unregister_match(registry, "hello", {2, :_, :_})
assert Registry.lookup(registry, "hello") == [{self(), value1}]
{:ok, _} = Registry.register(registry, "hello", value2)
Registry.unregister_match(registry, "hello", {2.0, :_, :_})
assert Registry.lookup(registry, "hello") == [{self(), value1}, {self(), value2}]
Registry.unregister_match(registry, "hello", {:_, :atom, :_})
assert Registry.lookup(registry, "hello") == []
end
test "unregister_match supports guards", %{registry: registry} do
value1 = {1, :atom, 1}
value2 = {2, :atom, 2}
{:ok, _} = Registry.register(registry, "hello", value1)
{:ok, _} = Registry.register(registry, "hello", value2)
Registry.unregister_match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 2}])
assert Registry.lookup(registry, "hello") == [{self(), value2}]
end
test "unregister_match supports tricky keys", %{registry: registry} do
{:ok, _} = Registry.register(registry, :_, :foo)
{:ok, _} = Registry.register(registry, :_, :bar)
{:ok, _} = Registry.register(registry, "hello", "a")
{:ok, _} = Registry.register(registry, "hello", "b")
Registry.unregister_match(registry, :_, :foo)
assert Registry.lookup(registry, :_) == [{self(), :bar}]
assert Registry.keys(registry, self()) |> Enum.sort() == [:_, "hello", "hello"]
end
@tag base_listener: :unique_listener
test "allows listeners", %{registry: registry, listeners: [listener]} do
Process.register(self(), listener)
{_, task} = register_task(registry, "hello", :world)
assert_received {:register, ^registry, "hello", ^task, :world}
self = self()
{:ok, _} = Registry.register(registry, "hello", :value)
assert_received {:register, ^registry, "hello", ^self, :value}
:ok = Registry.unregister(registry, "hello")
assert_received {:unregister, ^registry, "hello", ^self}
after
Process.unregister(listener)
end
test "links and unlinks on register/unregister", %{registry: registry} do
{:ok, pid} = Registry.register(registry, "hello", :value)
{:links, links} = Process.info(self(), :links)
assert pid in links
{:ok, pid} = Registry.register(registry, "world", :value)
{:links, links} = Process.info(self(), :links)
assert pid in links
:ok = Registry.unregister(registry, "hello")
{:links, links} = Process.info(self(), :links)
assert pid in links
:ok = Registry.unregister(registry, "world")
{:links, links} = Process.info(self(), :links)
refute pid in links
end
test "raises on unknown registry name" do
assert_raise ArgumentError, ~r/unknown registry/, fn ->
Registry.register(:unknown, "hello", :value)
end
end
test "raises if attempt to be used on via", %{registry: registry} do
assert_raise ArgumentError, ":via is not supported for duplicate registries", fn ->
name = {:via, Registry, {registry, "hello"}}
Agent.start_link(fn -> 0 end, name: name)
end
end
test "empty list for empty registry", %{registry: registry} do
assert Registry.select(registry, [{{:_, :_, :_}, [], [:"$_"]}]) == []
end
test "select all", %{registry: registry} do
{:ok, _} = Registry.register(registry, "hello", :value)
{:ok, _} = Registry.register(registry, "hello", :value)
assert Registry.select(registry, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}])
|> Enum.sort() ==
[{"hello", self(), :value}, {"hello", self(), :value}]
end
test "select supports full match specs", %{registry: registry} do
value = {1, :atom, 1}
{:ok, _} = Registry.register(registry, "hello", value)
assert [{"hello", self(), value}] ==
Registry.select(registry, [
{{"hello", :"$2", :"$3"}, [], [{{"hello", :"$2", :"$3"}}]}
])
assert [{"hello", self(), value}] ==
Registry.select(registry, [
{{:"$1", self(), :"$3"}, [], [{{:"$1", self(), :"$3"}}]}
])
assert [{"hello", self(), value}] ==
Registry.select(registry, [
{{:"$1", :"$2", value}, [], [{{:"$1", :"$2", {value}}}]}
])
assert [] ==
Registry.select(registry, [
{{"world", :"$2", :"$3"}, [], [{{"world", :"$2", :"$3"}}]}
])
assert [] == Registry.select(registry, [{{:"$1", :"$2", {1.0, :_, :_}}, [], [:"$_"]}])
assert [{"hello", self(), value}] ==
Registry.select(registry, [
{{:"$1", :"$2", {:"$3", :atom, :"$4"}}, [],
[{{:"$1", :"$2", {{:"$3", :atom, :"$4"}}}}]}
])
assert [{"hello", self(), {1, :atom, 1}}] ==
Registry.select(registry, [
{{:"$1", :"$2", {:"$3", :"$4", :"$3"}}, [],
[{{:"$1", :"$2", {{:"$3", :"$4", :"$3"}}}}]}
])
value2 = %{a: "a", b: "b"}
{:ok, _} = Registry.register(registry, "world", value2)
assert [:match] ==
Registry.select(registry, [{{"world", self(), %{b: "b"}}, [], [:match]}])
assert ["hello", "world"] ==
Registry.select(registry, [{{:"$1", :_, :_}, [], [:"$1"]}]) |> Enum.sort()
end
test "select supports guard conditions", %{registry: registry} do
value = {1, :atom, 2}
{:ok, _} = Registry.register(registry, "hello", value)
assert [{"hello", self(), {1, :atom, 2}}] ==
Registry.select(registry, [
{{:"$1", :"$2", {:"$3", :"$4", :"$5"}}, [{:>, :"$5", 1}],
[{{:"$1", :"$2", {{:"$3", :"$4", :"$5"}}}}]}
])
assert [] ==
Registry.select(registry, [
{{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 2}], [:"$_"]}
])
assert ["hello"] ==
Registry.select(registry, [
{{:"$1", :_, {:_, :"$2", :_}}, [{:is_atom, :"$2"}], [:"$1"]}
])
end
test "select allows multiple specs", %{registry: registry} do
{:ok, _} = Registry.register(registry, "hello", :value)
{:ok, _} = Registry.register(registry, "world", :value)
assert ["hello", "world"] ==
Registry.select(registry, [
{{"hello", :_, :_}, [], [{:element, 1, :"$_"}]},
{{"world", :_, :_}, [], [{:element, 1, :"$_"}]}
])
|> Enum.sort()
end
test "count_select supports match specs", %{registry: registry} do
value = {1, :atom, 1}
{:ok, _} = Registry.register(registry, "hello", value)
assert 1 == Registry.count_select(registry, [{{:_, :_, value}, [], [true]}])
assert 1 == Registry.count_select(registry, [{{"hello", :_, :_}, [], [true]}])
assert 1 == Registry.count_select(registry, [{{:_, :_, {1, :atom, :_}}, [], [true]}])
assert 1 == Registry.count_select(registry, [{{:_, :_, {:"$1", :_, :"$1"}}, [], [true]}])
assert 0 == Registry.count_select(registry, [{{"hello", :_, nil}, [], [true]}])
value2 = %{a: "a", b: "b"}
{:ok, _} = Registry.register(registry, "world", value2)
assert 1 == Registry.count_select(registry, [{{"world", :_, :_}, [], [true]}])
end
test "count_select supports guard conditions", %{registry: registry} do
value = {1, :atom, 2}
{:ok, _} = Registry.register(registry, "hello", value)
assert 1 ==
Registry.count_select(registry, [
{{:_, :_, {:_, :"$1", :_}}, [{:is_atom, :"$1"}], [true]}
])
assert 1 ==
Registry.count_select(registry, [
{{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 1}], [true]}
])
assert 0 ==
Registry.count_select(registry, [
{{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 2}], [true]}
])
end
test "count_select allows multiple specs", %{registry: registry} do
{:ok, _} = Registry.register(registry, "hello", :value)
{:ok, _} = Registry.register(registry, "world", :value)
assert 2 ==
Registry.count_select(registry, [
{{"hello", :_, :_}, [], [true]},
{{"world", :_, :_}, [], [true]}
])
end
test "rejects invalid tuple syntax", %{partitions: partitions} do
name = :"test_invalid_tuple_#{partitions}"
assert_raise ArgumentError, ~r/expected :keys to be given and be one of/, fn ->
Registry.start_link(keys: {:duplicate, :invalid}, name: name, partitions: partitions)
end
end
test "update_value is not supported", %{registry: registry} do
assert_raise ArgumentError, ~r/Registry.update_value\/3 is not supported/, fn ->
Registry.update_value(registry, "hello", fn val -> val end)
end
end
defp register_task(registry, key, value) do
parent = self()
{:ok, task} =
Task.start(fn ->
send(parent, Registry.register(registry, key, value))
Process.sleep(:infinity)
end)
assert_receive {:ok, owner}
{owner, task}
end
end
@@ -0,0 +1,507 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
Code.require_file("../test_helper.exs", __DIR__)
defmodule Registry.UniqueTest do
use ExUnit.Case,
async: true,
parameterize: [
%{partitions: 1},
%{partitions: 8}
]
@keys :unique
setup config do
partitions = config.partitions
listeners = List.wrap(config[:base_listener]) |> Enum.map(&:"#{&1}_#{partitions}")
name = :"#{config.test}_#{partitions}"
opts = [keys: @keys, name: name, partitions: partitions, listeners: listeners]
{:ok, _} = start_supervised({Registry, opts})
%{registry: name, listeners: listeners}
end
test "starts configured number of partitions", %{registry: registry, partitions: partitions} do
assert length(Supervisor.which_children(registry)) == partitions
end
test "counts 0 keys in an empty registry", %{registry: registry} do
assert 0 == Registry.count(registry)
end
test "counts the number of keys in a registry", %{registry: registry} do
{:ok, _} = Registry.register(registry, "hello", :value)
{:ok, _} = Registry.register(registry, "world", :value)
assert 2 == Registry.count(registry)
end
test "has unique registrations", %{registry: registry} do
{:ok, pid} = Registry.register(registry, "hello", :value)
assert is_pid(pid)
assert Registry.keys(registry, self()) == ["hello"]
assert Registry.values(registry, "hello", self()) == [:value]
assert {:error, {:already_registered, pid}} = Registry.register(registry, "hello", :value)
assert pid == self()
assert Registry.keys(registry, self()) == ["hello"]
assert Registry.values(registry, "hello", self()) == [:value]
{:ok, pid} = Registry.register(registry, "world", :value)
assert is_pid(pid)
assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "world"]
end
test "has unique registrations across processes", %{registry: registry} do
{_, task} = register_task(registry, "hello", :value)
Process.link(Process.whereis(registry))
assert Registry.keys(registry, task) == ["hello"]
assert Registry.values(registry, "hello", task) == [:value]
assert {:error, {:already_registered, ^task}} =
Registry.register(registry, "hello", :recent)
assert Registry.keys(registry, self()) == []
assert Registry.values(registry, "hello", self()) == []
{:links, links} = Process.info(self(), :links)
assert Process.whereis(registry) in links
end
test "has unique registrations even if partition is delayed", %{registry: registry} do
{owner, task} = register_task(registry, "hello", :value)
assert Registry.register(registry, "hello", :other) ==
{:error, {:already_registered, task}}
:sys.suspend(owner)
kill_and_assert_down(task)
Registry.register(registry, "hello", :other)
assert Registry.lookup(registry, "hello") == [{self(), :other}]
end
test "supports match patterns", %{registry: registry} do
value = {1, :atom, 1}
{:ok, _} = Registry.register(registry, "hello", value)
assert Registry.match(registry, "hello", {1, :_, :_}) == [{self(), value}]
assert Registry.match(registry, "hello", {1.0, :_, :_}) == []
assert Registry.match(registry, "hello", {:_, :atom, :_}) == [{self(), value}]
assert Registry.match(registry, "hello", {:"$1", :_, :"$1"}) == [{self(), value}]
assert Registry.match(registry, "hello", :_) == [{self(), value}]
assert Registry.match(registry, :_, :_) == []
value2 = %{a: "a", b: "b"}
{:ok, _} = Registry.register(registry, "world", value2)
assert Registry.match(registry, "world", %{b: "b"}) == [{self(), value2}]
end
test "supports guard conditions", %{registry: registry} do
value = {1, :atom, 2}
{:ok, _} = Registry.register(registry, "hello", value)
assert Registry.match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 1}]) ==
[{self(), value}]
assert Registry.match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 2}]) == []
assert Registry.match(registry, "hello", {:_, :"$1", :_}, [{:is_atom, :"$1"}]) ==
[{self(), value}]
end
test "count_match supports match patterns", %{registry: registry} do
value = {1, :atom, 1}
{:ok, _} = Registry.register(registry, "hello", value)
assert 1 == Registry.count_match(registry, "hello", {1, :_, :_})
assert 0 == Registry.count_match(registry, "hello", {1.0, :_, :_})
assert 1 == Registry.count_match(registry, "hello", {:_, :atom, :_})
assert 1 == Registry.count_match(registry, "hello", {:"$1", :_, :"$1"})
assert 1 == Registry.count_match(registry, "hello", :_)
assert 0 == Registry.count_match(registry, :_, :_)
value2 = %{a: "a", b: "b"}
{:ok, _} = Registry.register(registry, "world", value2)
assert 1 == Registry.count_match(registry, "world", %{b: "b"})
end
test "count_match supports guard conditions", %{registry: registry} do
value = {1, :atom, 2}
{:ok, _} = Registry.register(registry, "hello", value)
assert 1 == Registry.count_match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 1}])
assert 0 == Registry.count_match(registry, "hello", {:_, :_, :"$1"}, [{:>, :"$1", 2}])
assert 1 == Registry.count_match(registry, "hello", {:_, :"$1", :_}, [{:is_atom, :"$1"}])
end
test "unregister_match supports patterns", %{registry: registry} do
value = {1, :atom, 1}
{:ok, _} = Registry.register(registry, "hello", value)
Registry.unregister_match(registry, "hello", {2, :_, :_})
assert Registry.lookup(registry, "hello") == [{self(), value}]
Registry.unregister_match(registry, "hello", {1.0, :_, :_})
assert Registry.lookup(registry, "hello") == [{self(), value}]
Registry.unregister_match(registry, "hello", {:_, :atom, :_})
assert Registry.lookup(registry, "hello") == []
end
test "unregister_match supports guards", %{registry: registry} do
value = {1, :atom, 1}
{:ok, _} = Registry.register(registry, "hello", value)
Registry.unregister_match(registry, "hello", {:"$1", :_, :_}, [{:<, :"$1", 2}])
assert Registry.lookup(registry, "hello") == []
end
test "unregister_match supports tricky keys", %{registry: registry} do
{:ok, _} = Registry.register(registry, :_, :foo)
{:ok, _} = Registry.register(registry, "hello", "b")
Registry.unregister_match(registry, :_, :foo)
assert Registry.lookup(registry, :_) == []
assert Registry.keys(registry, self()) |> Enum.sort() == ["hello"]
end
test "compares using ===", %{registry: registry} do
{:ok, _} = Registry.register(registry, 1.0, :value)
{:ok, _} = Registry.register(registry, 1, :value)
assert Registry.keys(registry, self()) |> Enum.sort() == [1, 1.0]
end
test "updates current process value", %{registry: registry} do
assert Registry.update_value(registry, "hello", &raise/1) == :error
register_task(registry, "hello", :value)
assert Registry.update_value(registry, "hello", &raise/1) == :error
Registry.register(registry, "world", 1)
assert Registry.lookup(registry, "world") == [{self(), 1}]
assert Registry.update_value(registry, "world", &(&1 + 1)) == {2, 1}
assert Registry.lookup(registry, "world") == [{self(), 2}]
end
test "dispatches to a single key", %{registry: registry} do
fun = fn _ -> raise "will never be invoked" end
assert Registry.dispatch(registry, "hello", fun) == :ok
{:ok, _} = Registry.register(registry, "hello", :value)
fun = fn [{pid, value}] -> send(pid, {:dispatch, value}) end
assert Registry.dispatch(registry, "hello", fun)
assert_received {:dispatch, :value}
end
test "unregisters process by key", %{registry: registry} do
:ok = Registry.unregister(registry, "hello")
{:ok, _} = Registry.register(registry, "hello", :value)
{:ok, _} = Registry.register(registry, "world", :value)
assert Registry.keys(registry, self()) |> Enum.sort() == ["hello", "world"]
:ok = Registry.unregister(registry, "hello")
assert Registry.keys(registry, self()) == ["world"]
:ok = Registry.unregister(registry, "world")
assert Registry.keys(registry, self()) == []
end
test "unregisters with no entries", %{registry: registry} do
assert Registry.unregister(registry, "hello") == :ok
end
test "unregisters with tricky keys", %{registry: registry} do
{:ok, _} = Registry.register(registry, :_, :foo)
{:ok, _} = Registry.register(registry, "hello", "b")
Registry.unregister(registry, :_)
assert Registry.lookup(registry, :_) == []
assert Registry.keys(registry, self()) |> Enum.sort() == ["hello"]
end
@tag base_listener: :unique_listener
test "allows listeners", %{registry: registry, listeners: [listener]} do
Process.register(self(), listener)
{_, task} = register_task(registry, "hello", :world)
assert_received {:register, ^registry, "hello", ^task, :world}
self = self()
{:ok, _} = Registry.register(registry, "world", :value)
assert_received {:register, ^registry, "world", ^self, :value}
:ok = Registry.unregister(registry, "world")
assert_received {:unregister, ^registry, "world", ^self}
after
Process.unregister(listener)
end
test "links and unlinks on register/unregister", %{registry: registry} do
{:ok, pid} = Registry.register(registry, "hello", :value)
{:links, links} = Process.info(self(), :links)
assert pid in links
{:ok, pid} = Registry.register(registry, "world", :value)
{:links, links} = Process.info(self(), :links)
assert pid in links
:ok = Registry.unregister(registry, "hello")
{:links, links} = Process.info(self(), :links)
assert pid in links
:ok = Registry.unregister(registry, "world")
{:links, links} = Process.info(self(), :links)
refute pid in links
end
test "raises on unknown registry name" do
assert_raise ArgumentError, ~r/unknown registry/, fn ->
Registry.register(:unknown, "hello", :value)
end
end
test "via callbacks", %{registry: registry} do
name = {:via, Registry, {registry, "hello"}}
# register_name
{:ok, pid} = Agent.start_link(fn -> 0 end, name: name)
# send
assert Agent.update(name, &(&1 + 1)) == :ok
# whereis_name
assert Agent.get(name, & &1) == 1
# unregister_name
assert {:error, _} = Agent.start(fn -> raise "oops" end)
# errors
assert {:error, {:already_started, ^pid}} = Agent.start(fn -> 0 end, name: name)
end
test "uses value provided in via", %{registry: registry} do
name = {:via, Registry, {registry, "hello", :value}}
{:ok, pid} = Agent.start_link(fn -> 0 end, name: name)
assert Registry.lookup(registry, "hello") == [{pid, :value}]
end
test "empty list for empty registry", %{registry: registry} do
assert Registry.select(registry, [{{:_, :_, :_}, [], [:"$_"]}]) == []
end
test "select all", %{registry: registry} do
name = {:via, Registry, {registry, "hello"}}
{:ok, pid} = Agent.start_link(fn -> 0 end, name: name)
{:ok, _} = Registry.register(registry, "world", :value)
assert Registry.select(registry, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}])
|> Enum.sort() ==
[{"hello", pid, nil}, {"world", self(), :value}]
end
test "select supports full match specs", %{registry: registry} do
value = {1, :atom, 1}
{:ok, _} = Registry.register(registry, "hello", value)
assert [{"hello", self(), value}] ==
Registry.select(registry, [
{{"hello", :"$2", :"$3"}, [], [{{"hello", :"$2", :"$3"}}]}
])
assert [{"hello", self(), value}] ==
Registry.select(registry, [
{{:"$1", self(), :"$3"}, [], [{{:"$1", self(), :"$3"}}]}
])
assert [{"hello", self(), value}] ==
Registry.select(registry, [
{{:"$1", :"$2", value}, [], [{{:"$1", :"$2", {value}}}]}
])
assert [] ==
Registry.select(registry, [
{{"world", :"$2", :"$3"}, [], [{{"world", :"$2", :"$3"}}]}
])
assert [] == Registry.select(registry, [{{:"$1", :"$2", {1.0, :_, :_}}, [], [:"$_"]}])
assert [{"hello", self(), value}] ==
Registry.select(registry, [
{{:"$1", :"$2", {:"$3", :atom, :"$4"}}, [],
[{{:"$1", :"$2", {{:"$3", :atom, :"$4"}}}}]}
])
assert [{"hello", self(), {1, :atom, 1}}] ==
Registry.select(registry, [
{{:"$1", :"$2", {:"$3", :"$4", :"$3"}}, [],
[{{:"$1", :"$2", {{:"$3", :"$4", :"$3"}}}}]}
])
value2 = %{a: "a", b: "b"}
{:ok, _} = Registry.register(registry, "world", value2)
assert [:match] ==
Registry.select(registry, [{{"world", self(), %{b: "b"}}, [], [:match]}])
assert ["hello", "world"] ==
Registry.select(registry, [{{:"$1", :_, :_}, [], [:"$1"]}]) |> Enum.sort()
end
test "select supports guard conditions", %{registry: registry} do
value = {1, :atom, 2}
{:ok, _} = Registry.register(registry, "hello", value)
assert [{"hello", self(), {1, :atom, 2}}] ==
Registry.select(registry, [
{{:"$1", :"$2", {:"$3", :"$4", :"$5"}}, [{:>, :"$5", 1}],
[{{:"$1", :"$2", {{:"$3", :"$4", :"$5"}}}}]}
])
assert [] ==
Registry.select(registry, [
{{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 2}], [:"$_"]}
])
assert ["hello"] ==
Registry.select(registry, [
{{:"$1", :_, {:_, :"$2", :_}}, [{:is_atom, :"$2"}], [:"$1"]}
])
end
test "select allows multiple specs", %{registry: registry} do
{:ok, _} = Registry.register(registry, "hello", :value)
{:ok, _} = Registry.register(registry, "world", :value)
assert ["hello", "world"] ==
Registry.select(registry, [
{{"hello", :_, :_}, [], [{:element, 1, :"$_"}]},
{{"world", :_, :_}, [], [{:element, 1, :"$_"}]}
])
|> Enum.sort()
end
test "select raises on incorrect shape of match spec", %{registry: registry} do
assert_raise ArgumentError, fn ->
Registry.select(registry, [{:_, [], []}])
end
end
test "count_select supports match specs", %{registry: registry} do
value = {1, :atom, 1}
{:ok, _} = Registry.register(registry, "hello", value)
assert 1 == Registry.count_select(registry, [{{:_, :_, value}, [], [true]}])
assert 1 == Registry.count_select(registry, [{{"hello", :_, :_}, [], [true]}])
assert 1 == Registry.count_select(registry, [{{:_, :_, {1, :atom, :_}}, [], [true]}])
assert 1 == Registry.count_select(registry, [{{:_, :_, {:"$1", :_, :"$1"}}, [], [true]}])
assert 0 == Registry.count_select(registry, [{{"hello", :_, nil}, [], [true]}])
value2 = %{a: "a", b: "b"}
{:ok, _} = Registry.register(registry, "world", value2)
assert 1 == Registry.count_select(registry, [{{"world", :_, :_}, [], [true]}])
end
test "count_select supports guard conditions", %{registry: registry} do
value = {1, :atom, 2}
{:ok, _} = Registry.register(registry, "hello", value)
assert 1 ==
Registry.count_select(registry, [
{{:_, :_, {:_, :"$1", :_}}, [{:is_atom, :"$1"}], [true]}
])
assert 1 ==
Registry.count_select(registry, [
{{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 1}], [true]}
])
assert 0 ==
Registry.count_select(registry, [
{{:_, :_, {:_, :_, :"$1"}}, [{:>, :"$1", 2}], [true]}
])
end
test "count_select allows multiple specs", %{registry: registry} do
{:ok, _} = Registry.register(registry, "hello", :value)
{:ok, _} = Registry.register(registry, "world", :value)
assert 2 ==
Registry.count_select(registry, [
{{"hello", :_, :_}, [], [true]},
{{"world", :_, :_}, [], [true]}
])
end
test "count_select raises on incorrect shape of match spec", %{registry: registry} do
assert_raise ArgumentError, fn ->
Registry.count_select(registry, [{:_, [], []}])
end
end
test "doesn't grow ets on already_registered",
%{registry: registry, partitions: partitions} do
assert sum_pid_entries(registry, partitions) == 0
{:ok, pid} = Registry.register(registry, "hello", :value)
assert is_pid(pid)
assert sum_pid_entries(registry, partitions) == 1
{:ok, pid} = Registry.register(registry, "world", :value)
assert is_pid(pid)
assert sum_pid_entries(registry, partitions) == 2
assert {:error, {:already_registered, _pid}} =
Registry.register(registry, "hello", :value)
assert sum_pid_entries(registry, partitions) == 2
end
test "doesn't grow ets on already_registered across processes",
%{registry: registry, partitions: partitions} do
assert sum_pid_entries(registry, partitions) == 0
{_, task} = register_task(registry, "hello", :value)
Process.link(Process.whereis(registry))
assert sum_pid_entries(registry, partitions) == 1
{:ok, pid} = Registry.register(registry, "world", :value)
assert is_pid(pid)
assert sum_pid_entries(registry, partitions) == 2
assert {:error, {:already_registered, ^task}} =
Registry.register(registry, "hello", :recent)
assert sum_pid_entries(registry, partitions) == 2
end
defp register_task(registry, key, value) do
parent = self()
{:ok, task} =
Task.start(fn ->
send(parent, Registry.register(registry, key, value))
Process.sleep(:infinity)
end)
assert_receive {:ok, owner}
{owner, task}
end
defp kill_and_assert_down(pid) do
ref = Process.monitor(pid)
Process.exit(pid, :kill)
assert_receive {:DOWN, ^ref, _, _, _}
end
defp sum_pid_entries(registry, partitions) do
Enum.sum_by(0..(partitions - 1), fn partition ->
registry
|> Module.concat("PIDPartition#{partition}")
|> ets_entries()
end)
end
defp ets_entries(table_name) do
:ets.all()
|> Enum.find_value(fn id -> :ets.info(id, :name) == table_name and :ets.info(id, :size) end)
end
end
File diff suppressed because it is too large Load Diff
+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
+16 -11
View File
@@ -165,18 +165,23 @@ defmodule ExUnit.Runner do
running
end
defp spawn_modules(config, [{group, [_ | _] = modules} | groups], async?, running) do
if max_failures_reached?(config) do
running
else
{pid, ref} =
spawn_monitor(fn ->
Enum.each(modules, fn {module, params} ->
run_module(config, module, async?, group, params)
end)
end)
defp spawn_modules(config, [{group, modules} | groups], async?, running) do
cond do
max_failures_reached?(config) ->
running
spawn_modules(config, groups, async?, Map.put(running, ref, pid))
modules == [] ->
spawn_modules(config, groups, async?, running)
true ->
{pid, ref} =
spawn_monitor(fn ->
Enum.each(modules, fn {module, params} ->
run_module(config, module, async?, group, params)
end)
end)
spawn_modules(config, groups, async?, Map.put(running, ref, pid))
end
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]), []}]
+27 -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
@@ -720,6 +720,30 @@ defmodule ExUnitTest do
assert_receive {:tmp_dir, tmp_dir2} when tmp_dir1 != tmp_dir2
end
test "empty parameterized tests" do
defmodule EmptyParameterizedTests do
use ExUnit.Case, async: true, parameterize: []
test "hello world" do
assert false
end
end
configure_and_reload_on_exit(trace: true)
assert capture_io(fn -> ExUnit.run() end) =~ "0 tests"
defmodule EmptyGroupedParameterizedTests do
use ExUnit.Case, async: true, parameterize: [], group: :example
test "hello world" do
assert false
end
end
configure_and_reload_on_exit(trace: true)
assert capture_io(fn -> ExUnit.run() end) =~ "0 tests"
end
describe "after_suite/1" do
test "executes all callbacks set in reverse order" do
Process.register(self(), :after_suite_test_process)
+3 -1
View File
@@ -80,7 +80,9 @@ defmodule IEx.Config do
end
def parser() do
Application.fetch_env!(:iex, :parser)
# Since the parser itself can be invoked from the remote node
# without IEx running, we cannot use fetch_env!
Application.get_env(:iex, :parser, {IEx.Evaluator, :parse, []})
end
def color(color) 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
-1
View File
@@ -19,7 +19,6 @@ defmodule IEx.MixProject do
mod: {IEx.App, []},
env: [
colors: [],
parser: {IEx.Evaluator, :parse, []},
inspect: [pretty: true],
history_size: 20,
default_prompt: "%prefix(%counter)>",

Some files were not shown because too many files have changed in this diff Show More