Compare commits
101
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
915d6dca16 | ||
|
|
a983c8c043 | ||
|
|
d33fd8e413 | ||
|
|
02603b147c | ||
|
|
e126dab368 | ||
|
|
38547285f1 | ||
|
|
4f07670d08 | ||
|
|
dcff0ffaea | ||
|
|
97407cde90 | ||
|
|
579fc97b97 | ||
|
|
003ef335e7 | ||
|
|
028cdbb91e | ||
|
|
794441a255 | ||
|
|
727106beb4 | ||
|
|
5e45871b5e | ||
|
|
0db308cbff | ||
|
|
35a7e3aa1f | ||
|
|
7340597020 | ||
|
|
3b6acc33d2 | ||
|
|
2ed048acc6 | ||
|
|
bf0c203d52 | ||
|
|
b074e6b159 | ||
|
|
bfe7315fbb | ||
|
|
28f4fdc21b | ||
|
|
ee1dc0729f | ||
|
|
32e724c87c | ||
|
|
7758084211 | ||
|
|
57d3098c2a | ||
|
|
427d21e410 | ||
|
|
24a188a016 | ||
|
|
457d52c89b | ||
|
|
6684dbd3f2 | ||
|
|
ccb2a742ad | ||
|
|
92094c9ef1 | ||
|
|
d4d5da2ccf | ||
|
|
666c082105 | ||
|
|
d2c9d39efe | ||
|
|
97d16038fe | ||
|
|
87cbf06ff1 | ||
|
|
caf39255a4 | ||
|
|
6440fb90a3 | ||
|
|
8793c68822 | ||
|
|
49ac038d67 | ||
|
|
3827b73d2d | ||
|
|
dc8a19028f | ||
|
|
357c346a9f | ||
|
|
6df9eb338c | ||
|
|
fa85b95250 | ||
|
|
a89655c434 | ||
|
|
730fd0f824 | ||
|
|
29d3b0cf3a | ||
|
|
ae94e6eb25 | ||
|
|
661e537cda | ||
|
|
4a5c076b7f | ||
|
|
297bc8302b | ||
|
|
f6413152ff | ||
|
|
c2edbaf173 | ||
|
|
ac9a659bc6 | ||
|
|
cc5837caa7 | ||
|
|
1738ed453f | ||
|
|
3e18aafa0a | ||
|
|
bcb73a468b | ||
|
|
613f631487 | ||
|
|
f08a84af10 | ||
|
|
b1a7afd061 | ||
|
|
e9bf5df25a | ||
|
|
2e7bb9b47f | ||
|
|
83c68cecc1 | ||
|
|
184b724483 | ||
|
|
47fea9580a | ||
|
|
6a6678f337 | ||
|
|
ec87c6b111 | ||
|
|
0c595489b6 | ||
|
|
cdbfa09edb | ||
|
|
baa25991fa | ||
|
|
a5c9121a7c | ||
|
|
177dc8ab59 | ||
|
|
bba8835ecc | ||
|
|
dea8456022 | ||
|
|
544a10ccfa | ||
|
|
ee14357bcd | ||
|
|
2fa136e60a | ||
|
|
f382affce3 | ||
|
|
36190985d2 | ||
|
|
957f84b7cd | ||
|
|
74fab3caf4 | ||
|
|
f1a6a4e019 | ||
|
|
c7884ca393 | ||
|
|
6f3fb272af | ||
|
|
8c14d3a818 | ||
|
|
9e10ea876f | ||
|
|
64f69cfee8 | ||
|
|
7844c98fe7 | ||
|
|
519eff5e29 | ||
|
|
e29c40a7e9 | ||
|
|
9fc3708980 | ||
|
|
c06b6a929e | ||
|
|
cfa3bb7429 | ||
|
|
195a4cf9f7 | ||
|
|
8daa2a6f67 | ||
|
|
d38f331db6 |
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
[
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -389,13 +389,20 @@ defimpl Inspect, for: List do
|
||||
close = color_doc("]", :list, opts)
|
||||
|
||||
cond do
|
||||
lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
|
||||
inspected =
|
||||
case Identifier.escape(IO.chardata_to_string(term), ?", printable_limit) do
|
||||
{escaped, ""} -> [?~, ?c, ?", escaped, ?"]
|
||||
{escaped, _} -> [?~, ?c, ?", escaped, ?", " ++ ..."]
|
||||
(lists == :as_charlists and unicode_list?(term, printable_limit)) or
|
||||
(lists == :infer and List.ascii_printable?(term, printable_limit)) ->
|
||||
{split, tail} =
|
||||
if is_integer(printable_limit) do
|
||||
case Enum.split(term, printable_limit) do
|
||||
{split, []} -> {split, []}
|
||||
{split, _} -> {split, " ++ ..."}
|
||||
end
|
||||
else
|
||||
{term, []}
|
||||
end
|
||||
|
||||
{escaped, _} = Identifier.escape(IO.chardata_to_string(split), ?")
|
||||
inspected = [?~, ?c, ?", escaped, ?" | tail]
|
||||
color_doc(IO.iodata_to_binary(inspected), :charlist, opts)
|
||||
|
||||
keyword?(term) ->
|
||||
@@ -409,6 +416,19 @@ defimpl Inspect, for: List do
|
||||
end
|
||||
end
|
||||
|
||||
defp unicode_list?(_, 0), do: true
|
||||
|
||||
defp unicode_list?([char | rest], counter)
|
||||
when char in 0..0xD7FF or char in 0xE000..0x10FFFF,
|
||||
do: unicode_list?(rest, decrement(counter))
|
||||
|
||||
defp unicode_list?([], _counter), do: true
|
||||
defp unicode_list?(_, _counter), do: false
|
||||
|
||||
@compile {:inline, decrement: 1}
|
||||
defp decrement(:infinity), do: :infinity
|
||||
defp decrement(counter), do: counter - 1
|
||||
|
||||
@doc false
|
||||
def keyword({key, value}, opts) do
|
||||
key = color_doc(Macro.inspect_atom(:key, key), :atom, opts)
|
||||
@@ -569,6 +589,7 @@ defimpl Inspect, for: Regex do
|
||||
defp translate_options([:firstline | t], acc), do: translate_options(t, [?f | acc])
|
||||
defp translate_options([:ungreedy | t], acc), do: translate_options(t, [?U | acc])
|
||||
defp translate_options([:multiline | t], acc), do: translate_options(t, [?m | acc])
|
||||
defp translate_options([:export | t], acc), do: translate_options(t, [?E | acc])
|
||||
defp translate_options([], acc), do: acc
|
||||
defp translate_options(_t, _acc), do: :error
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ defmodule Inspect.Opts do
|
||||
is `:decimal` and if it is printable, otherwise in bit syntax. See
|
||||
`String.printable?/1` to learn when a string is printable.
|
||||
|
||||
* `:charlists` - when `:as_charlists` all lists will be printed as charlists,
|
||||
non-printable elements will be escaped.
|
||||
* `:charlists` - when `:as_charlists` all charlists will be printed as charlists,
|
||||
non-printable code points will be escaped. Other lists will be printed as lists.
|
||||
|
||||
When `:as_lists` all lists will be printed as lists.
|
||||
|
||||
|
||||
@@ -511,7 +511,8 @@ defmodule IO do
|
||||
"""
|
||||
@spec inspect(device, item, inspect_opts) :: item when item: var
|
||||
def inspect(device, item, opts) when is_device(device) and is_list(opts) do
|
||||
label = if label = opts[:label], do: [to_chardata(label), ": "], else: []
|
||||
{label, opts} = Keyword.pop(opts, :label)
|
||||
label = if label, do: [to_chardata(label), ": "], else: []
|
||||
opts = Inspect.Opts.new(opts)
|
||||
doc = Inspect.Algebra.group(Inspect.Algebra.to_doc(item, opts))
|
||||
chardata = Inspect.Algebra.format(doc, opts.width)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1137,7 +1137,7 @@ defmodule List do
|
||||
|
||||
Please check the given list or call inspect/1 to get the list representation, got:
|
||||
|
||||
#{inspect(list)}
|
||||
#{inspect(list, charlists: :as_lists)}
|
||||
"""
|
||||
else
|
||||
result when is_binary(result) ->
|
||||
@@ -1189,7 +1189,7 @@ defmodule List do
|
||||
|
||||
Please check the given list or call inspect/1 to get the list representation, got:
|
||||
|
||||
#{inspect(list)}
|
||||
#{inspect(list, charlists: :as_lists)}
|
||||
"""
|
||||
else
|
||||
result when is_list(result) ->
|
||||
|
||||
@@ -959,8 +959,9 @@ defmodule Macro do
|
||||
backwards compatibility purposes.
|
||||
|
||||
In future releases, Elixir may introduce truly required struct
|
||||
fields, and therefore only one of required or default will be
|
||||
present. Your code should prepare for such scenario accordingly.
|
||||
fields, the required field will be removed and default will be
|
||||
present only if the field is optional. Your code should prepare
|
||||
for such scenario accordingly.
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec struct_info!(module(), Macro.Env.t()) ::
|
||||
@@ -2700,7 +2701,7 @@ defmodule Macro do
|
||||
:ok
|
||||
end
|
||||
|
||||
prelude = quote do: options = unquote(Macro.escape(options))
|
||||
prelude = quote do: options = unquote(options)
|
||||
acc = {prelude, dbg_format_header(env)}
|
||||
|
||||
{acc, nil} =
|
||||
|
||||
@@ -402,30 +402,21 @@ defmodule Module do
|
||||
|
||||
Accepts the function name (as an atom) of a function in the current module.
|
||||
The function must have an arity of 0 (no arguments). If the function does
|
||||
not return `:ok`, the loading of the module will be aborted.
|
||||
For example:
|
||||
not return `:ok`, the loading of the module will be aborted. Its primary
|
||||
use case is to load [NIFs](https://www.erlang.org/doc/man/erl_nif):
|
||||
|
||||
defmodule MyModule do
|
||||
@on_load :load_check
|
||||
@on_load :load_external_code
|
||||
|
||||
def load_check do
|
||||
if some_condition() do
|
||||
:ok
|
||||
else
|
||||
:abort
|
||||
end
|
||||
end
|
||||
|
||||
def some_condition do
|
||||
false
|
||||
def load_external_code do
|
||||
:erlang.load_nif(~c"path/to/extension.so_or_dll")
|
||||
end
|
||||
end
|
||||
|
||||
The function given to `on_load` should avoid calling functions from
|
||||
other modules. If you must call functions in other modules and those
|
||||
modules are defined within the same project, the called modules must
|
||||
have the `@compile {:autoload, true}` annotation, so they are loaded
|
||||
upfront (and not from within the `@on_load` callback).
|
||||
other modules. This is because, when running a `mix release`,
|
||||
`on_load` runs extremely early, before any application starts running,
|
||||
and therefore even systems like the `Logger` and `IO` are not yet available.
|
||||
|
||||
### `@vsn`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -979,7 +979,7 @@ defmodule Module.Types.Apply do
|
||||
{mod, fun, arity, converter} = mfac
|
||||
meta = elem(expr, 1)
|
||||
|
||||
{banner, hints, traces} =
|
||||
{banner, traces} =
|
||||
case Keyword.get(meta, :type_check) do
|
||||
:interpolation ->
|
||||
{_, _, [arg]} = expr
|
||||
@@ -990,7 +990,7 @@ defmodule Module.Types.Apply do
|
||||
#{expr_to_string(arg) |> indent(4)}
|
||||
|
||||
it has type:
|
||||
""", [:interpolation], collect_traces(expr, context)}
|
||||
""", collect_traces(expr, context)}
|
||||
|
||||
:generator ->
|
||||
{:<-, _, [_, arg]} = expr
|
||||
@@ -1001,7 +1001,7 @@ defmodule Module.Types.Apply do
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
it has type:
|
||||
""", [:generator], collect_traces(arg, context)}
|
||||
""", collect_traces(arg, context)}
|
||||
|
||||
:into ->
|
||||
{"""
|
||||
@@ -1010,7 +1010,7 @@ defmodule Module.Types.Apply do
|
||||
into: #{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
it has type:
|
||||
""", [:into], collect_traces(expr, context)}
|
||||
""", collect_traces(expr, context)}
|
||||
|
||||
_ ->
|
||||
mfa_or_fa = if mod, do: Exception.format_mfa(mod, fun, arity), else: "#{fun}/#{arity}"
|
||||
@@ -1021,37 +1021,79 @@ defmodule Module.Types.Apply do
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
given types:
|
||||
""", [], collect_traces(expr, context)}
|
||||
""", collect_traces(expr, context)}
|
||||
end
|
||||
|
||||
explanation =
|
||||
{explanation, impls} =
|
||||
cond do
|
||||
reason = empty_arg_reason(converter.(args_types)) ->
|
||||
reason
|
||||
{reason, ""}
|
||||
|
||||
Code.ensure_loaded?(mod) and
|
||||
Keyword.has_key?(mod.module_info(:attributes), :__protocol__) ->
|
||||
if function_exported?(mod, :__protocol__, 1) and
|
||||
mod.__protocol__(:impls) == {:consolidated, []} do
|
||||
"""
|
||||
but the protocol was not yet implemented for any type and therefore will always fail. \
|
||||
This error typically happens within libraries that define protocols and will disappear as \
|
||||
soon as there is one implementation. If you expect the protocol to be implemented later on, \
|
||||
you can define an implementation specific for development/test.
|
||||
"""
|
||||
{"""
|
||||
but the #{inspect(mod)} protocol was not yet implemented \
|
||||
for any type and therefore will always fail.
|
||||
|
||||
This warning will disappear once you define a implementation. \
|
||||
If the protocol is part of a library, you may define a dummy \
|
||||
implementation for development/test.
|
||||
""", ""}
|
||||
else
|
||||
# Protocol errors can be very verbose, so we collapse structs
|
||||
"""
|
||||
but expected a type that implements the #{inspect(mod)} protocol, it must be one of:
|
||||
#{clauses_args_to_quoted_string(clauses, converter, collapse_structs: true)}
|
||||
"""
|
||||
fix =
|
||||
case mod do
|
||||
String.Chars ->
|
||||
"""
|
||||
You either passed the wrong value or you must:
|
||||
|
||||
1. convert the given value to a string explicitly
|
||||
(use inspect/1 if you want to convert any data structure to a string)
|
||||
2. implement the String.Chars protocol
|
||||
"""
|
||||
|
||||
Enumerable ->
|
||||
"""
|
||||
You either passed the wrong value or you must:
|
||||
|
||||
1. convert the given value to an Enumerable explicitly
|
||||
2. implement the Enumerable protocol
|
||||
"""
|
||||
|
||||
_ ->
|
||||
"""
|
||||
You either passed the wrong value or you forgot to implement the protocol.
|
||||
"""
|
||||
end
|
||||
|
||||
# The protocol has, at the moment, simplified clauses, so we build the complete one
|
||||
clauses =
|
||||
case mod.__protocol__(:impls) do
|
||||
{:consolidated, mods} ->
|
||||
domain = mods |> Enum.map(&Module.Types.Of.impl/1) |> Enum.reduce(&union/2)
|
||||
[{[domain], dynamic()}]
|
||||
|
||||
_ ->
|
||||
clauses
|
||||
end
|
||||
|
||||
{"""
|
||||
but expected a type that implements the #{inspect(mod)} protocol.
|
||||
#{fix}\
|
||||
""",
|
||||
"""
|
||||
|
||||
#{hint()} the #{inspect(mod)} protocol is implemented for the following types:
|
||||
#{clauses_args_to_quoted_string(clauses, converter, collapse_structs: true)}
|
||||
"""}
|
||||
end
|
||||
|
||||
true ->
|
||||
"""
|
||||
but expected one of:
|
||||
#{clauses_args_to_quoted_string(clauses, converter, [])}
|
||||
"""
|
||||
{"""
|
||||
but expected one of:
|
||||
#{clauses_args_to_quoted_string(clauses, converter, [])}
|
||||
""", ""}
|
||||
end
|
||||
|
||||
%{
|
||||
@@ -1066,7 +1108,7 @@ defmodule Module.Types.Apply do
|
||||
""",
|
||||
explanation,
|
||||
format_traces(traces),
|
||||
format_hints(hints)
|
||||
impls
|
||||
])
|
||||
}
|
||||
end
|
||||
@@ -1252,7 +1294,7 @@ defmodule Module.Types.Apply do
|
||||
common = intersection(actual, expected)
|
||||
|
||||
uncommon_doc =
|
||||
difference(actual, common)
|
||||
difference(actual, expected)
|
||||
|> to_quoted()
|
||||
|> Code.Formatter.to_algebra()
|
||||
|> ansi_red()
|
||||
|
||||
@@ -27,11 +27,8 @@ defmodule Module.Types.Descr do
|
||||
@bit_top (1 <<< 7) - 1
|
||||
@bit_number @bit_integer ||| @bit_float
|
||||
|
||||
# Remark: those use AST for BDDs
|
||||
defmacrop bdd_leaf(arg1, arg2), do: {arg1, arg2}
|
||||
|
||||
defmacrop domain_key(key),
|
||||
do: {:domain_key, key}
|
||||
defmacro bdd_leaf(arg1, arg2), do: {arg1, arg2}
|
||||
defmacro domain_key(key), do: {:domain_key, key}
|
||||
|
||||
@domain_key_types [
|
||||
{:domain_key, :binary},
|
||||
@@ -454,11 +451,7 @@ defmodule Module.Types.Descr do
|
||||
defp intersection(:map, v1, v2), do: map_intersection(v1, v2)
|
||||
defp intersection(:optional, 1, 1), do: 1
|
||||
defp intersection(:tuple, v1, v2), do: tuple_intersection(v1, v2)
|
||||
|
||||
defp intersection(:fun, v1, v2) do
|
||||
bdd = fun_intersection(v1, v2)
|
||||
if bdd == :bdd_bot, do: 0, else: bdd
|
||||
end
|
||||
defp intersection(:fun, v1, v2), do: fun_intersection(v1, v2)
|
||||
|
||||
defp intersection(:dynamic, v1, v2) do
|
||||
descr = dynamic_intersection(v1, v2)
|
||||
@@ -548,11 +541,7 @@ defmodule Module.Types.Descr do
|
||||
defp difference(:map, v1, v2), do: map_difference(v1, v2)
|
||||
defp difference(:optional, 1, 1), do: 0
|
||||
defp difference(:tuple, v1, v2), do: tuple_difference(v1, v2)
|
||||
|
||||
defp difference(:fun, v1, v2) do
|
||||
bdd = fun_difference(v1, v2)
|
||||
if bdd == :bdd_bot, do: 0, else: bdd
|
||||
end
|
||||
defp difference(:fun, v1, v2), do: fun_difference(v1, v2)
|
||||
|
||||
@doc """
|
||||
Compute the negation of a type.
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
"""
|
||||
])
|
||||
|
||||
@@ -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 ->
|
||||
"""
|
||||
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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
@@ -267,6 +267,8 @@ defmodule Protocol do
|
||||
|
||||
@optional_callbacks __deriving__: 2
|
||||
|
||||
@elixir_checker_version :elixir_erl.checker_version()
|
||||
|
||||
@doc false
|
||||
defmacro def(signature)
|
||||
|
||||
@@ -451,7 +453,7 @@ defmodule Protocol do
|
||||
true
|
||||
|
||||
"""
|
||||
@spec extract_protocols([charlist | String.t()]) :: [atom]
|
||||
@spec extract_protocols([charlist | String.t() | {charlist, [charlist]}]) :: [atom]
|
||||
def extract_protocols(paths) do
|
||||
extract_matching_by_attribute(paths, [?E, ?l, ?i, ?x, ?i, ?r, ?.], fn module, attributes ->
|
||||
case attributes[:__protocol__] do
|
||||
@@ -480,7 +482,7 @@ defmodule Protocol do
|
||||
true
|
||||
|
||||
"""
|
||||
@spec extract_impls(module, [charlist | String.t()]) :: [atom]
|
||||
@spec extract_impls(module, [charlist | String.t() | {charlist, [charlist]}]) :: [atom]
|
||||
def extract_impls(protocol, paths) when is_atom(protocol) do
|
||||
prefix = Atom.to_charlist(protocol) ++ [?.]
|
||||
|
||||
@@ -494,17 +496,25 @@ defmodule Protocol do
|
||||
|
||||
defp extract_matching_by_attribute(paths, prefix, callback) do
|
||||
for path <- paths,
|
||||
# Do not use protocols as they may be consolidating
|
||||
path = if(is_list(path), do: path, else: String.to_charlist(path)),
|
||||
file <- list_dir(path),
|
||||
{path, files} = list_dir(path),
|
||||
file <- files,
|
||||
mod = extract_from_file(path, file, prefix, callback),
|
||||
do: mod
|
||||
end
|
||||
|
||||
# Do not use protocols as they may be consolidating
|
||||
defp list_dir({path, files}) when is_list(path) and is_list(files) do
|
||||
{path, files}
|
||||
end
|
||||
|
||||
defp list_dir(path) when is_binary(path) do
|
||||
list_dir(String.to_charlist(path))
|
||||
end
|
||||
|
||||
defp list_dir(path) when is_list(path) do
|
||||
case :file.list_dir(path) do
|
||||
{:ok, files} -> files
|
||||
_ -> []
|
||||
{:ok, files} -> {path, files}
|
||||
_ -> {path, []}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -564,31 +574,34 @@ defmodule Protocol do
|
||||
# Ensure the types are sorted so the compiled beam is deterministic
|
||||
types = Enum.sort(types)
|
||||
|
||||
with {:ok, any, definitions, signatures, compile_info} <- beam_protocol(protocol),
|
||||
{:ok, definitions, signatures} <-
|
||||
consolidate(protocol, any, definitions, signatures, types),
|
||||
do: compile(definitions, signatures, compile_info)
|
||||
with {:ok, any, definitions, checker, compile_info} <- beam_protocol(protocol),
|
||||
{:ok, definitions, checker} <-
|
||||
consolidate(protocol, any, definitions, checker, types),
|
||||
do: compile(definitions, checker, compile_info)
|
||||
end
|
||||
|
||||
defp beam_protocol(protocol) do
|
||||
chunk_ids = [:debug_info, [?D, ?o, ?c, ?s]]
|
||||
chunk_ids = [:debug_info, [?E, ?x, ?C, ?k], [?D, ?o, ?c, ?s]]
|
||||
opts = [:allow_missing_chunks]
|
||||
|
||||
case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do
|
||||
{:ok, {^protocol, [{:debug_info, debug_info} | chunks]}} ->
|
||||
{:ok, {^protocol, [{:debug_info, debug_info}, {_, checker} | chunks]}} ->
|
||||
{:debug_info_v1, _backend, {:elixir_v1, module_map, specs}} = debug_info
|
||||
%{attributes: attributes, definitions: definitions} = module_map
|
||||
|
||||
# Protocols in precompiled archives may not have signatures, so we default to an empty map.
|
||||
# TODO: Remove this on Elixir v1.23.
|
||||
signatures = Map.get(module_map, :signatures, %{})
|
||||
|
||||
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
|
||||
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
|
||||
|
||||
case attributes[:__protocol__] do
|
||||
[fallback_to_any: any] ->
|
||||
{:ok, any, definitions, signatures, {module_map, specs, chunks}}
|
||||
checker =
|
||||
with true <- is_binary(checker),
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
|
||||
contents
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
|
||||
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
|
||||
{:ok, any, definitions, checker, {module_map, specs, chunks}}
|
||||
|
||||
_ ->
|
||||
{:error, :not_a_protocol}
|
||||
@@ -607,7 +620,7 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
# Consolidate the protocol for faster implementations and fine-grained type information.
|
||||
defp consolidate(protocol, fallback_to_any?, definitions, signatures, types) do
|
||||
defp consolidate(protocol, fallback_to_any?, definitions, checker, types) do
|
||||
case List.keytake(definitions, {:__protocol__, 1}, 0) do
|
||||
{protocol_def, definitions} ->
|
||||
types = if fallback_to_any?, do: types, else: List.delete(types, Any)
|
||||
@@ -623,25 +636,37 @@ defmodule Protocol do
|
||||
protocol_def = change_protocol(protocol_def, types)
|
||||
impl_for = change_impl_for(impl_for, protocol, types)
|
||||
struct_impl_for = change_struct_impl_for(struct_impl_for, protocol, types, structs)
|
||||
new_signatures = new_signatures(definitions, protocol_funs, protocol, types)
|
||||
|
||||
definitions = [protocol_def, impl_for, impl_for!, struct_impl_for] ++ definitions
|
||||
signatures = Enum.into(new_signatures, signatures)
|
||||
{:ok, definitions, signatures}
|
||||
|
||||
checker =
|
||||
if checker do
|
||||
update_in(checker.exports, fn exports ->
|
||||
signatures = new_signatures(definitions, protocol_funs, protocol, types, structs)
|
||||
|
||||
for {fun, info} <- exports do
|
||||
if sig = Map.get(signatures, fun) do
|
||||
{fun, %{info | sig: sig}}
|
||||
else
|
||||
{fun, info}
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
{:ok, definitions, checker}
|
||||
|
||||
nil ->
|
||||
{:error, :not_a_protocol}
|
||||
end
|
||||
end
|
||||
|
||||
defp new_signatures(definitions, protocol_funs, protocol, types) do
|
||||
defp new_signatures(definitions, protocol_funs, protocol, types, structs) do
|
||||
alias Module.Types.Descr
|
||||
types_minus_any = List.delete(types, Any)
|
||||
|
||||
clauses =
|
||||
types
|
||||
|> List.delete(Any)
|
||||
|> Enum.map(fn impl ->
|
||||
{[Module.Types.Of.impl(impl)], Descr.atom([__concat__(protocol, impl)])}
|
||||
Enum.map(types_minus_any, fn impl ->
|
||||
{[Module.Types.Of.impl(impl, :open)], Descr.atom([__concat__(protocol, impl)])}
|
||||
end)
|
||||
|
||||
{domain, impl_for, impl_for!} =
|
||||
@@ -656,10 +681,16 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
_ ->
|
||||
structs_domain =
|
||||
case structs do
|
||||
[] -> Descr.none()
|
||||
_ -> Descr.open_map(__struct__: Descr.atom(structs))
|
||||
end
|
||||
|
||||
domain =
|
||||
clauses
|
||||
|> Enum.map(fn {[domain], _} -> domain end)
|
||||
|> Enum.reduce(&Descr.union/2)
|
||||
Enum.reduce(types_minus_any -- structs, structs_domain, fn impl, acc ->
|
||||
Descr.union(Module.Types.Of.impl(impl, :open), acc)
|
||||
end)
|
||||
|
||||
not_domain = Descr.negation(domain)
|
||||
|
||||
@@ -680,10 +711,12 @@ defmodule Protocol do
|
||||
{fun_arity, {:strong, nil, [{[domain | rest], Descr.dynamic()}]}}
|
||||
end
|
||||
|
||||
[
|
||||
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
|
||||
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
|
||||
] ++ new_signatures
|
||||
Map.new(
|
||||
[
|
||||
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
|
||||
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
|
||||
] ++ new_signatures
|
||||
)
|
||||
end
|
||||
|
||||
defp get_protocol_functions({_name, _kind, _meta, clauses}) do
|
||||
@@ -752,11 +785,9 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
# Finally compile the module and emit its bytecode.
|
||||
defp compile(definitions, signatures, {module_map, specs, docs_chunk}) do
|
||||
# Protocols in precompiled archives may not have signatures, so we default to an empty map.
|
||||
# TODO: Remove this on Elixir v1.23.
|
||||
module_map = %{module_map | definitions: definitions} |> Map.put(:signatures, signatures)
|
||||
{:ok, :elixir_erl.consolidate(module_map, specs, docs_chunk)}
|
||||
defp compile(definitions, checker, {module_map, specs, docs_chunk}) do
|
||||
module_map = %{module_map | definitions: definitions}
|
||||
{:ok, :elixir_erl.consolidate(module_map, checker, specs, docs_chunk)}
|
||||
end
|
||||
|
||||
## Definition callbacks
|
||||
|
||||
@@ -27,8 +27,9 @@ defmodule Record do
|
||||
|
||||
## Types
|
||||
|
||||
Types can be defined for tuples with the `record/2` macro (only available in
|
||||
typespecs). This macro will expand to a tuple as seen in the example below:
|
||||
Types can be defined for tuples with the `record/2` construct (which is only
|
||||
available in typespecs), with the record name as an atom and a keyword list
|
||||
of fields and their types as argument:
|
||||
|
||||
defmodule MyModule do
|
||||
require Record
|
||||
|
||||
+32
-8
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = %{}
|
||||
|
||||
@@ -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}]},
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
%% Compiler backend to Erlang.
|
||||
|
||||
-module(elixir_erl).
|
||||
-export([elixir_to_erl/1, elixir_to_erl/2, definition_to_anonymous/5, compile/1, consolidate/3,
|
||||
get_ann/1, debug_info/4, scope/2, format_error/1]).
|
||||
-export([elixir_to_erl/1, elixir_to_erl/2, definition_to_anonymous/5, compile/2, consolidate/4,
|
||||
get_ann/1, debug_info/4, scope/2, checker_version/0, format_error/1]).
|
||||
-include("elixir.hrl").
|
||||
-define(typespecs, 'Elixir.Kernel.Typespec').
|
||||
|
||||
checker_version() ->
|
||||
elixir_checker_v3.
|
||||
|
||||
%% debug_info callback
|
||||
|
||||
debug_info(elixir_v1, _Module, none, _Opts) ->
|
||||
@@ -132,14 +135,14 @@ scope(_Meta, ExpandCaptures) ->
|
||||
|
||||
%% Static compilation hook, used in protocol consolidation
|
||||
|
||||
consolidate(Map, TypeSpecs, DocsChunk) ->
|
||||
{Prefix, Forms, Def, _Defmacro, _Macros} = dynamic_form(Map, nil),
|
||||
CheckerChunk = checker_chunk(Map, Def, chunk_opts(Map)),
|
||||
consolidate(Map, Checker, TypeSpecs, DocsChunk) ->
|
||||
{Prefix, Forms, _Def, _Defmacro, _Macros} = dynamic_form(Map, nil),
|
||||
CheckerChunk = checker_chunk(Checker, chunk_opts(Map)),
|
||||
load_form(Map, Prefix, Forms, TypeSpecs, DocsChunk ++ CheckerChunk).
|
||||
|
||||
%% Dynamic compilation hook, used in regular compiler
|
||||
|
||||
compile(#{module := Module, anno := Anno} = BaseMap) ->
|
||||
compile(#{module := Module, anno := Anno} = BaseMap, Signatures) ->
|
||||
Map =
|
||||
case elixir_erl_compiler:env_compiler_options() of
|
||||
[] -> BaseMap;
|
||||
@@ -161,7 +164,7 @@ compile(#{module := Module, anno := Anno} = BaseMap) ->
|
||||
|
||||
ChunkOpts = chunk_opts(Map),
|
||||
DocsChunk = docs_chunk(Map, Set, Module, Anno, Def, Defmacro, Types, Callbacks, ChunkOpts),
|
||||
CheckerChunk = checker_chunk(Map, Def, ChunkOpts),
|
||||
CheckerChunk = checker_chunk(Map, Def, Signatures, ChunkOpts),
|
||||
load_form(Map, Prefix, Forms, TypeSpecs, DocsChunk ++ CheckerChunk).
|
||||
|
||||
chunk_opts(Map) ->
|
||||
@@ -639,9 +642,13 @@ signature_to_binary(_, Name, Signature) ->
|
||||
Doc = 'Elixir.Inspect.Algebra':format('Elixir.Code':quoted_to_algebra(Quoted), infinity),
|
||||
'Elixir.IO':iodata_to_binary(Doc).
|
||||
|
||||
checker_chunk(Map, Def, ChunkOpts) ->
|
||||
#{deprecated := Deprecated, defines_behaviour := DefinesBehaviour,
|
||||
signatures := Signatures, attributes := Attributes} = Map,
|
||||
checker_chunk(nil, _ChunkOpts) ->
|
||||
[];
|
||||
checker_chunk(Contents, ChunkOpts) ->
|
||||
[{<<"ExCk">>, term_to_binary({checker_version(), Contents}, ChunkOpts)}].
|
||||
|
||||
checker_chunk(Map, Def, Signatures, ChunkOpts) ->
|
||||
#{deprecated := Deprecated, defines_behaviour := DefinesBehaviour, attributes := Attributes} = Map,
|
||||
DeprecatedMap = maps:from_list(Deprecated),
|
||||
|
||||
Exports =
|
||||
@@ -662,7 +669,7 @@ checker_chunk(Map, Def, ChunkOpts) ->
|
||||
end
|
||||
},
|
||||
|
||||
[{<<"ExCk">>, term_to_binary({elixir_checker_v3, Contents}, ChunkOpts)}].
|
||||
checker_chunk(Contents, ChunkOpts).
|
||||
|
||||
prepend_behaviour_info(true, Def) -> [{{behaviour_info, 1}, []} | Def];
|
||||
prepend_behaviour_info(false, Def) -> Def.
|
||||
|
||||
@@ -566,6 +566,11 @@ extract_bit_type({'-', _, [L, R]}, Acc) ->
|
||||
extract_bit_type({unit, _, [Arg]}, Acc) ->
|
||||
[{unit, Arg} | Acc];
|
||||
extract_bit_type({Other, _, nil}, Acc) ->
|
||||
[Other | Acc];
|
||||
%% TODO: Remove me on Elixir v2.0.
|
||||
%% Elixir v1.14 and earlier emitted an empty list
|
||||
%% and may still be processed via debug_info.
|
||||
extract_bit_type({Other, _, []}, Acc) ->
|
||||
[Other | Acc].
|
||||
|
||||
%% Optimizations that are specific to Erlang and change
|
||||
|
||||
@@ -27,8 +27,9 @@ each_clause({'catch', Meta, Raw, Expr}, S) ->
|
||||
{Args, Guards} = elixir_utils:extract_splat_guards(Raw),
|
||||
|
||||
Match =
|
||||
%% TODO: Remove me on Elixir v2.0.
|
||||
%% Elixir v1.17 and earlier emitted single argument
|
||||
%% and may still be processed via debug_info
|
||||
%% and may still be processed via debug_info.
|
||||
case Args of
|
||||
[X] -> [throw, X];
|
||||
[X, Y] -> [X, Y]
|
||||
|
||||
@@ -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} ->
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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} ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -876,8 +876,18 @@ handle_unary_op(Rest, Line, Column, Kind, Length, Op, Scope, Tokens) ->
|
||||
Token = {identifier, {Line, Column, nil}, Op},
|
||||
tokenize(Remaining, Line, Column + Length + Extra, Scope, [Token | Tokens]);
|
||||
{Remaining, Extra} ->
|
||||
NewScope =
|
||||
%% TODO: Remove these deprecations on Elixir v2.0
|
||||
case Op of
|
||||
'~~~' ->
|
||||
Msg = "~~~ is deprecated. Use Bitwise.bnot/1 instead for clarity",
|
||||
prepend_warning(Line, Column, Msg, Scope);
|
||||
_ ->
|
||||
Scope
|
||||
end,
|
||||
|
||||
Token = {Kind, {Line, Column, nil}, Op},
|
||||
tokenize(Remaining, Line, Column + Length + Extra, Scope, [Token | Tokens])
|
||||
tokenize(Remaining, Line, Column + Length + Extra, NewScope, [Token | Tokens])
|
||||
end.
|
||||
|
||||
handle_op([$: | Rest], Line, Column, _Kind, Length, Op, Scope, Tokens) when ?is_space(hd(Rest)) ->
|
||||
@@ -897,10 +907,6 @@ handle_op(Rest, Line, Column, Kind, Length, Op, Scope, Tokens) ->
|
||||
Msg = "^^^ is deprecated. It is typically used as xor but it has the wrong precedence, use Bitwise.bxor/2 instead",
|
||||
prepend_warning(Line, Column, Msg, Scope);
|
||||
|
||||
'~~~' ->
|
||||
Msg = "~~~ is deprecated. Use Bitwise.bnot/1 instead for clarity",
|
||||
prepend_warning(Line, Column, Msg, Scope);
|
||||
|
||||
'<|>' ->
|
||||
Msg = "<|> is deprecated. Use another pipe-like operator",
|
||||
prepend_warning(Line, Column, Msg, Scope);
|
||||
|
||||
@@ -127,7 +127,7 @@ change_posix_time(Name, Time) when is_integer(Time) ->
|
||||
change_universal_time(Name, {{Y, M, D}, {H, Min, Sec}}=Time)
|
||||
when is_integer(Y), is_integer(M), is_integer(D),
|
||||
is_integer(H), is_integer(Min), is_integer(Sec) ->
|
||||
file:write_file_info(Name, #file_info{mtime=Time}, [{time, universal}]).
|
||||
file:write_file_info(Name, #file_info{mtime=Time}, [raw, {time, universal}]).
|
||||
|
||||
relative_to_cwd(Path) ->
|
||||
try elixir_config:get(relative_paths) of
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -787,6 +787,18 @@ defmodule FileTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "cp_r! with file src and dest unknown" do
|
||||
src = fixture_path("cp_r/a/1.txt")
|
||||
dest = tmp_path("tmp/unknown/")
|
||||
|
||||
message =
|
||||
"could not copy recursively from #{inspect(src)} to #{inspect(dest)}. #{dest}: no such file or directory"
|
||||
|
||||
assert_raise File.CopyError, message, fn ->
|
||||
File.cp_r!(src, dest)
|
||||
end
|
||||
end
|
||||
|
||||
test "cp preserves mode" do
|
||||
File.mkdir_p!(tmp_path("tmp"))
|
||||
src = fixture_path("cp_mode")
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
defmodule Dialyzer.Regressions do
|
||||
def io_inspect_opts do
|
||||
IO.inspect(123, label: "foo", limit: :infinity)
|
||||
end
|
||||
|
||||
def format_opts do
|
||||
Code.format_string!("",
|
||||
line_length: 120,
|
||||
force_do_end_blocks: true,
|
||||
locals_without_parens: true,
|
||||
migrate: true
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -282,10 +282,15 @@ defmodule Inspect.ListTest do
|
||||
|
||||
test "printable limit" do
|
||||
assert inspect(~c"hello world", printable_limit: 4) == ~s(~c"hell" ++ ...)
|
||||
assert inspect(~c"hello", printable_limit: :infinity) == ~s(~c"hello")
|
||||
# Non printable characters after the limit don't matter
|
||||
assert inspect(~c"hello world" ++ [0], printable_limit: 4) == ~s(~c"hell" ++ ...)
|
||||
# Non printable strings aren't affected by printable limit
|
||||
assert inspect([0, 1, 2, 3, 4], printable_limit: 3) == ~s([0, 1, 2, 3, 4])
|
||||
# Ensure a non-printable value after limit does not crash
|
||||
assert inspect(~c"hello world" ++ [nil], printable_limit: 4) == ~s(~c"hell" ++ ...)
|
||||
# Ensure a non-printable value after default limit does not hang
|
||||
assert String.ends_with?(inspect(List.duplicate(?a, 5000) ++ [nil]), "\" ++ ...")
|
||||
end
|
||||
|
||||
test "keyword" do
|
||||
@@ -310,10 +315,20 @@ defmodule Inspect.ListTest do
|
||||
assert inspect([0], charlists: :infer) == "[0]"
|
||||
end
|
||||
|
||||
test "opt as strings" do
|
||||
test "opt as charlists" do
|
||||
assert inspect(~c"john" ++ [0] ++ ~c"doe", charlists: :as_charlists) == ~s(~c"john\\0doe")
|
||||
assert inspect(~c"john", charlists: :as_charlists) == ~s(~c"john")
|
||||
assert inspect([0], charlists: :as_charlists) == ~s(~c"\\0")
|
||||
assert inspect([nil], charlists: :as_charlists) == "[nil]"
|
||||
assert inspect([0xD800], charlists: :as_charlists) == "[55296]"
|
||||
assert inspect([1_114_112], charlists: :as_charlists) == "[1114112]"
|
||||
assert inspect([[?a]], charlists: :as_charlists) == ~s([~c"a"])
|
||||
|
||||
assert inspect([?a, ?b, ?c | nil], charlists: :as_charlists, printable_limit: 4) ==
|
||||
"[97, 98, 99 | nil]"
|
||||
|
||||
assert inspect([?a, ?b, ?c | nil], charlists: :as_charlists, printable_limit: 3) ==
|
||||
~s(~c"abc" ++ ...)
|
||||
end
|
||||
|
||||
test "opt as lists" do
|
||||
@@ -906,6 +921,11 @@ defmodule Inspect.OthersTest do
|
||||
assert inspect(Regex.compile!("foo", [:ucp])) == ~S'Regex.compile!("foo", [:ucp])'
|
||||
end
|
||||
|
||||
@tag :re_import
|
||||
test "exported regex" do
|
||||
assert inspect(~r/foo/E) == "~r/foo/E"
|
||||
end
|
||||
|
||||
test "inspect_fun" do
|
||||
fun = fn
|
||||
integer, _opts when is_integer(integer) ->
|
||||
|
||||
@@ -185,6 +185,11 @@ defmodule Kernel.DialyzerTest do
|
||||
assert_dialyze_no_warnings!(context)
|
||||
end
|
||||
|
||||
test "no warning in various non-regression cases", context do
|
||||
copy_beam!(context, Dialyzer.Regressions)
|
||||
assert_dialyze_no_warnings!(context)
|
||||
end
|
||||
|
||||
defp copy_beam!(context, module) do
|
||||
name = "#{module}.beam"
|
||||
File.cp!(Path.join(context.base_dir, name), Path.join(context.outdir, name))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -183,6 +183,24 @@ defmodule StreamTest do
|
||||
assert Stream.chunk_while([1, 2, 3, 4, 5], [], chunk_fun, after_fun) |> Enum.at(0) == [1]
|
||||
end
|
||||
|
||||
test "chunk_while/4 regression case with concat" do
|
||||
result =
|
||||
["WrongHeader\nJohn Doe", "skipped"]
|
||||
|> Stream.take(1)
|
||||
|> Stream.chunk_while(
|
||||
"",
|
||||
fn element, acc ->
|
||||
{acc, elements} = String.split(acc <> element, "\n") |> List.pop_at(-1)
|
||||
{:cont, elements, acc}
|
||||
end,
|
||||
&{:cont, [&1], []}
|
||||
)
|
||||
|> Stream.concat()
|
||||
|> Enum.to_list()
|
||||
|
||||
assert result == ["WrongHeader", "John Doe"]
|
||||
end
|
||||
|
||||
test "concat/1" do
|
||||
stream = Stream.concat([1..3, [], [4, 5, 6], [], 7..9])
|
||||
assert is_function(stream)
|
||||
|
||||
@@ -16,6 +16,10 @@ tokenize_error(String) ->
|
||||
{error, Error, _, _, _} = elixir_tokenizer:tokenize(String, 1, []),
|
||||
Error.
|
||||
|
||||
tokenize_warnings(String) ->
|
||||
{ok, _Line, _Column, Warnings, Result, []} = elixir_tokenizer:tokenize(String, 1, []),
|
||||
{lists:reverse(Result), Warnings}.
|
||||
|
||||
type_test() ->
|
||||
[{int, {1, 1, 1}, "1"},
|
||||
{type_op, {1, 3, nil}, '::'},
|
||||
@@ -279,3 +283,13 @@ sigil_heredoc_test() ->
|
||||
invalid_sigil_delimiter_test() ->
|
||||
{[{line, 1}, {column, 1}], "invalid sigil delimiter: ", Message} = tokenize_error("~s\\"),
|
||||
true = lists:prefix("\"\\\" (column 3, code point U+005C)", lists:flatten(Message)).
|
||||
|
||||
deprecated_operators_test() ->
|
||||
{
|
||||
[{xor_op, {1, 1, nil}, '^^^'}, {int, {1, 4, 1}, "1"}],
|
||||
[{{1, 1}, "^^^ is deprecated. It is typically used as xor but it has the wrong precedence, use Bitwise.bxor/2 instead"}]
|
||||
} = tokenize_warnings("^^^1"),
|
||||
{
|
||||
[{unary_op, {1, 1, nil}, '~~~'}, {int, {1, 4, 1}, "1"}],
|
||||
[{{1, 1}, "~~~ is deprecated. Use Bitwise.bnot/1 instead for clarity"}]
|
||||
} = tokenize_warnings("~~~1").
|
||||
|
||||
@@ -160,6 +160,7 @@ defmodule ExUnit.Assertions do
|
||||
end
|
||||
end
|
||||
|
||||
{left, right} = move_match(left, right)
|
||||
__match__(left, right, code, check, __CALLER__)
|
||||
end
|
||||
|
||||
@@ -380,6 +381,12 @@ defmodule ExUnit.Assertions do
|
||||
{ExUnit.AssertionError.no_value(), expr}
|
||||
end
|
||||
|
||||
defp move_match(left, {:=, meta, [middle, right]}),
|
||||
do: move_match({:=, meta, [left, middle]}, right)
|
||||
|
||||
defp move_match(left, right),
|
||||
do: {left, right}
|
||||
|
||||
@doc false
|
||||
def __match__({:when, _, _} = left, right, _, _, _) do
|
||||
suggestion =
|
||||
@@ -409,7 +416,7 @@ defmodule ExUnit.Assertions do
|
||||
case right do
|
||||
unquote(left) ->
|
||||
unquote(check)
|
||||
unquote(mark_as_generated(vars))
|
||||
{unquote_splicing(mark_as_generated(vars))}
|
||||
|
||||
_ ->
|
||||
left = unquote(Macro.escape(left))
|
||||
@@ -427,7 +434,7 @@ defmodule ExUnit.Assertions do
|
||||
quote do
|
||||
right = unquote(right)
|
||||
expr = unquote(code)
|
||||
unquote(vars) = unquote(match_expr)
|
||||
{unquote_splicing(vars)} = unquote(match_expr)
|
||||
right
|
||||
end
|
||||
end
|
||||
|
||||
@@ -169,11 +169,7 @@ defmodule ExUnit.Callbacks do
|
||||
end
|
||||
"""
|
||||
|
||||
@type child_spec_overrides :: [
|
||||
restart: :permanent | :transient | :temporary,
|
||||
shutdown: :brutal_kill | timeout(),
|
||||
type: :worker | :supervisor
|
||||
]
|
||||
@type child_spec_overrides :: Supervisor.child_spec_overrides()
|
||||
|
||||
@doc false
|
||||
def __register__(module) do
|
||||
|
||||
@@ -964,7 +964,14 @@ defmodule ExUnit.DocTest do
|
||||
|
||||
@doc false
|
||||
defmacro __assert__({:=, _, [left, right]} = assertion) do
|
||||
{left, right} = move_match(left, right)
|
||||
code = Macro.escape(assertion, prune_metadata: true)
|
||||
ExUnit.Assertions.__match__(left, right, code, :ok, __CALLER__)
|
||||
end
|
||||
|
||||
defp move_match(left, {:=, meta, [middle, right]}),
|
||||
do: move_match({:=, meta, [left, middle]}, right)
|
||||
|
||||
defp move_match(left, right),
|
||||
do: {left, right}
|
||||
end
|
||||
|
||||
@@ -338,7 +338,8 @@ defmodule ExUnit.Formatter do
|
||||
{formatted_reason, wrapped_stack} =
|
||||
format_exception(test, struct, wrapped_stack, width, formatter, @counter_padding)
|
||||
|
||||
formatted_stack = format_stacktrace(wrapped_stack, test.module, test.name, formatter)
|
||||
formatted_stack = format_stacktrace(wrapped_stack, test, formatter)
|
||||
|
||||
{error_info(header, formatter) <> pad(formatted_reason <> formatted_stack), stack}
|
||||
|
||||
:error ->
|
||||
@@ -512,10 +513,15 @@ defmodule ExUnit.Formatter do
|
||||
|
||||
defp format_diff(left, right, context, formatter) do
|
||||
if has_value?(left) and has_value?(right) do
|
||||
{result, env} = find_diff(left, right, context)
|
||||
result = if formatter.(:diff_enabled?, false), do: result
|
||||
hints = Enum.map(env.hints, &{:hint, format_hint(&1)})
|
||||
{result, hints}
|
||||
case find_diff(left, right, context) do
|
||||
{result, env} ->
|
||||
result = if formatter.(:diff_enabled?, false), do: result
|
||||
hints = Enum.map(env.hints, &{:hint, format_hint(&1)})
|
||||
{result, hints}
|
||||
|
||||
nil ->
|
||||
{nil, []}
|
||||
end
|
||||
else
|
||||
{nil, []}
|
||||
end
|
||||
@@ -672,22 +678,28 @@ defmodule ExUnit.Formatter do
|
||||
|> Algebra.format(width)
|
||||
end
|
||||
|
||||
defp format_stacktrace([], _case, _test, _color) do
|
||||
defp format_stacktrace(stack, %{module: module, name: name}, color),
|
||||
do: format_stacktrace(stack, module, name, color)
|
||||
|
||||
defp format_stacktrace(stack, %{name: name}, color),
|
||||
do: format_stacktrace(stack, name, nil, color)
|
||||
|
||||
defp format_stacktrace([], _module, _test, _color) do
|
||||
""
|
||||
end
|
||||
|
||||
defp format_stacktrace(stacktrace, test_case, test, color) do
|
||||
defp format_stacktrace(stacktrace, module, test, color) do
|
||||
extra_info("stacktrace:", color) <>
|
||||
Enum.map_join(stacktrace, fn entry ->
|
||||
stacktrace_info(format_stacktrace_entry(entry, test_case, test), color)
|
||||
stacktrace_info(format_stacktrace_entry(entry, module, test), color)
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_stacktrace_entry({test_case, test, _, location}, test_case, test) do
|
||||
defp format_stacktrace_entry({module, test, _, location}, module, test) do
|
||||
format_file_line(location[:file], location[:line], " (test)")
|
||||
end
|
||||
|
||||
defp format_stacktrace_entry(entry, _test_case, _test) do
|
||||
defp format_stacktrace_entry(entry, _module, _test) do
|
||||
format_stacktrace_entry(entry)
|
||||
end
|
||||
|
||||
|
||||
@@ -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]), []}]
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user