Compare commits

...
136 Commits
Author SHA1 Message Date
José Valim 7b20c281d5 Release v1.18.4 2025-05-21 13:47:42 +02:00
José Valim c69f4407fd Fix ExUnit suite on Erlang/OTP 28 2025-05-21 12:50:21 +02:00
Wojtek Mach 88f65ae1a6 Install Hex and Rebar per OTP release (Elixir v1.18) (#14509) 2025-05-21 12:45:52 +02:00
José Valim 14d406fcd9 Raise nice error message when escaping maps/structs with references, closes #14497 2025-05-20 10:35:26 +02:00
Ahmed Elawad 08a9d4b9fc docs: Debugging page typo (#14502) 2025-05-18 09:00:36 +02:00
José Valim b3ab3696ec Update CI 2025-05-09 13:23:30 +02:00
José Valim afe4704666 Trace on definition callbacks, closes #14427 2025-05-09 09:37:20 +02:00
José Valim c51acfcbc5 Trace on load functions, see #14427 2025-05-09 09:37:16 +02:00
José Valim 332ff949a0 Ensure git checkout works with untracked files (#14428)
Closes #14425.
2025-05-09 09:37:12 +02:00
José Valim ab17a3c050 Emit trace events for super calls, closes #14395 2025-05-09 09:37:09 +02:00
Steffen Deusch 2522bba1bf IEx.Helpers.process_info/1 (#14418) 2025-05-09 09:37:05 +02:00
ruslandoga d00f172c59 Retry mix httpc with disabled middlebox_comp_mode (#14357)
Closes #14356
2025-05-09 09:36:58 +02:00
José Valim 0eb5289cd0 Add --no-listeners to mix compile and mix run 2025-05-08 10:18:50 +02:00
José Valim 7e01628b5d Compute beam location early 2025-04-25 20:14:08 +02:00
José Valim 6432e8655c Write modules before verification so modules can be found during verification 2025-04-25 20:14:01 +02:00
Jean Klingler e34495e304 Optimize map unions to avoid building long lists (#14215) 2025-04-08 23:39:53 +02:00
José Valim 64c4ecf9c7 Do not add duplicates on maps and tuples intersections 2025-04-08 23:35:17 +02:00
José Valim ef002e2b15 Properly track imported function calls in tracer
Closes #13878.
2025-04-08 09:28:50 +02:00
José Valim f89c5076f9 Do not crash on nested bitstrings
Closes #14391.
2025-04-03 11:51:53 +02:00
Fabian Becker 18d367c01b Remove deprecation warning check for 1.18 (#14382) 2025-03-31 17:02:48 +02:00
José Valim 65baed8681 Update Windows CI 2025-03-30 16:25:38 +02:00
José Valim 76c64a0f32 Recompile regexes when escaped from module attributes (#14381) 2025-03-30 16:13:40 +02:00
José Valim 6ac1d10f77 Do not run listeners when not checking the deps 2025-03-26 17:05:04 +01:00
Jean Klingler ca57cfe6f3 Handle non-binary bitstring in struct default values (#14363) 2025-03-25 18:08:02 +09:00
Jean Klingler c32f72581a Fix handling of ErlangError when :general key is chardata (#14329) 2025-03-15 19:19:08 +09:00
José Valim 8115bd38a1 Preserve backwards compatibility in elixir_erl, closes #14323 2025-03-14 20:13:13 +01:00
José Valim e35ffc5a90 Release v1.18.3 2025-03-06 10:55:08 +01:00
José Valim 178643f9eb Do not discard nil on protocol concat, closes #14311 (#14314) 2025-03-06 10:41:12 +01:00
José Valim 75677b9dab Encode any JSON key to string, closes #14305 (#14309) 2025-03-05 09:48:05 +01:00
Łukasz Samson 25ab648502 Fix autocomplete crash when expanding struct with __MODULE__ (#14308) 2025-03-05 09:48:04 +01:00
José Valim 8493f1934a Do not raise when Stream.cycle is explicitly halted, closes #14307 2025-03-03 12:55:37 +01:00
José Valim c63aeb9f77 Clarify order on uniq/uniq_by, closes #14304 2025-03-03 12:55:36 +01:00
José Valim d89fabe5e4 Fix date in CHANGELOG, closes #14303 2025-02-27 08:00:03 +01:00
Graham Preston d51153b56e Fix typo (#14302) 2025-02-26 17:57:17 +01:00
Jean Klingler 69990a5d1d Fix regression when diffing nested improper lists (#14292)
Close https://github.com/elixir-lang/elixir/issues/14291
2025-02-21 23:09:32 +09:00
José Valim 930ec69739 Support --no-listeners in loadpaths 2025-02-21 11:01:51 +01:00
José Valim d7478095e0 Support Erlang/OTP 28 2025-02-20 18:12:08 +01:00
José Valim b823f9efdf Allow <<_::3*8>> in typespecs 2025-02-20 06:12:13 +01:00
José Valim 6ecb430614 Do not purge on recompile if IEx is not running, closes #14260 2025-02-12 10:03:17 +01:00
Ben Murden b615c8435a Improved help on deprecation warning (#14239) 2025-01-29 08:59:50 +01:00
cevado 120467f83c Add section to Process.exit/2 with differences to Kernel.exit/1 (#14238) 2025-01-29 08:59:48 +01:00
José Valim 4b09a836e0 Validate signature for forward compatibility (#14235) 2025-01-28 10:32:10 +01:00
tea☆ 55c05d943e Fix --no-color not setting :ansi_enabled to false (#14229) 2025-01-26 14:10:20 +01:00
Jean Klingler 8e61baacab Fix String.split/3 example in guide and add note in docs (#14223) 2025-01-25 07:12:09 +09:00
José Valim 7426acb1b9 Use exdoc:loaded to reload mermaid graphs 2025-01-24 19:58:58 +01:00
José Valim 175c8243b2 Release v1.18.2 2025-01-22 17:38:30 +01:00
José Valim 2c1a836db3 Make --color/--no-color consistent with mix test 2025-01-22 17:33:00 +01:00
José Valim f29e18bca1 Do not precompile regexes on Erlang/OTP 28 2025-01-21 18:51:54 +01:00
José Valim 329442c481 Provide more AST around cursor fragments, closes #14118 2025-01-21 10:36:19 +01:00
José Valim 01a88e7137 Simplify token pruning, closes #14139 2025-01-20 18:57:09 +01:00
Jean Klingler 4def31f8ab Mention the new type system in typespecs doc (#14188) 2025-01-15 20:42:33 +09:00
José Valim 88c75bcbf5 Remove powershell scripts
It current leaves the shell broken after quitting Erlang.

Issues have been reported upstream, so we may be able
to bring it back in future Erlang/OTP versions.
2025-01-13 13:51:05 +01:00
José Valim 786f3ce797 Add --color for easy enabling and disabling of coloring (#14183) 2025-01-13 08:55:12 +01:00
José Valim 98aeee34b6 Simplify bat and ps1 files (#14180) 2025-01-13 08:55:08 +01:00
José Valim 6f7eaf1122 Keep line information in defstruct 2025-01-12 20:09:44 +01:00
Jonatan Kłosko 9593cef2f5 Remove duplicate call to mkdir in concurrency lock (#14175) 2025-01-11 10:53:36 +01:00
Claudio Ortolina c78f426bf5 Improve clarity of Path.safe_relative/2 argument names (#14167)
Switches from `cwd` to `relative_to`, which is more generic and more consistent with possible use cases of the function.
2025-01-10 20:03:32 +01:00
Steffen Deusch ff29628602 Track mix_pubsub and mix_lock folders per user (#14171) 2025-01-10 20:03:31 +01:00
Zach Allaun f5e42ba11a Update task-and-gen-tcp.md (#14166) 2025-01-09 18:10:35 +01:00
José Valim b0dae83353 Clarify the need for better tools in the TCP guide 2025-01-09 16:37:06 +01:00
Jean Klingler 40da7e8857 Fix ExUnit crash when diffing bitstring specifiers (#14161) 2025-01-09 18:27:58 +09:00
Jean Klingler ac910c1cc2 Prevent infinite loop in compiler for some invalid type specs (#14155) 2025-01-09 18:27:55 +09:00
José Valim 5c340e488e Migrate the git repo against the origin branch, closes #14163 2025-01-09 10:03:18 +01:00
Daniel Drexler defef37856 Improve warning for charlists (#14160)
The current warning for single-quoted character lists is confusing
because it appears to be a stylistic note instead of a deprecation
warning. This commit updates the language to make the reasoning clearer
and to more clearly describe what `mix format --migrate` will change
about the users' code.
2025-01-09 09:22:20 +01:00
Guillaume Milan 432d6fa1c2 Fix crashing on autocompleting structs with runtime values (#14150) 2025-01-07 11:40:01 +01:00
José Valim 24714c68a7 Improve error message for invalid default arguments 2025-01-02 18:36:27 +01:00
Jonatan Kłosko f088fc9cdb Print compilation lock waiting message to stderr (#14138) 2025-01-02 09:41:01 +01:00
José Valim dddb8f7b19 Bring kv-observer image back as it is used in guides 2024-12-31 08:53:56 +01:00
José Valim 2da3300bfc Do not wrap literals in variable when expanding in/2 2024-12-31 08:45:54 +01:00
Jonatan Kłosko c7a841baae Add an environment variable to optionally disable compilation locking (#14129) 2024-12-30 17:00:35 +01:00
José Valim f3dfb72c69 Properly handle optional keys in map intersection 2024-12-30 11:39:57 +01:00
José Valim 7e149619f5 Fix expression-tag pairing inside with type checking 2024-12-30 11:07:35 +01:00
José Valim 6dbb932bf3 Properly raise for invalid async value 2024-12-28 09:33:04 +01:00
José Valim 0cb79b0ba1 Add OTP 25 to sign entry 2024-12-28 09:18:59 +01:00
José Valim 265b9aaeb3 Update AST metadata documentation (#14120) 2024-12-27 11:37:54 +01:00
José Valim b9bd0e3dd9 Bring conditional violation reports back 2024-12-26 19:37:25 +01:00
José Valim 6a3301f237 Release v1.18.1 2024-12-24 10:17:42 +01:00
José Valim 4911916f62 Do not warn when comparing literals 2024-12-24 10:02:29 +01:00
José Valim 546a0db392 Use division by zero to show exception
Closes #14111.
2024-12-24 09:42:21 +01:00
José Valim 463f1aa593 Do not recompile if compilation fails due to --warnings-as-errors 2024-12-23 18:27:11 +01:00
José Valim a3a632efd1 Improve warning as errors deprecation notice 2024-12-23 18:26:37 +01:00
José Valim b7a5fd7b56 Improve container_cursor_to_quoted with trailing fragment, closes #14087 2024-12-23 12:24:03 +01:00
Jonatan Kłosko 04378bd9a6 Add build lock to deps.loadpaths (#14108) 2024-12-23 10:56:17 +01:00
José Valim fae36c5e49 Queue async groups until state is loaded 2024-12-23 10:56:15 +01:00
José Valim 712f24af0a Do not run async groups on load and support --repeat-until-failure (#14107) 2024-12-23 10:32:12 +01:00
José Valim 8c9f303e37 Avoid crash when typing violation is detected on dynamic dispatch
Closes #14105.
2024-12-22 17:56:59 +01:00
José Valim 1a2be16109 Track source for warnings, closes #14093 2024-12-22 11:08:13 +01:00
José Valim 4adaac702e Do not validate clauses of overridable functions, closes #14094 2024-12-22 09:55:23 +01:00
José Valim a1be4fbc86 Verify no warnings are emitted on generated clauses, see #14094 2024-12-22 09:55:21 +01:00
Jonatan Männchen d4e6a558cb Fix Release Signing Again (#14099) (#14102)
This time, fix the Docs publishing.
2024-12-21 18:31:26 +01:00
Jonatan Männchen 4e3203f19a Fix Release Signing (#14099) (#14100) 2024-12-21 18:31:21 +01:00
José Valim 4a5fb1eb66 Use binwrite only on Windows, closes #14101 2024-12-21 18:25:45 +01:00
Jonatan Kłosko 0fe6f68c34 Fix mix escript.build when protocol consolidation is disabled (#14098) 2024-12-21 12:22:54 +01:00
Jean Klingler 5aa049ccb3 Remove no_parens when using capture with arity (#14090) 2024-12-20 22:28:19 +09:00
Jonatan Männchen e4c86d4b5f Hash release files after signing (#14085) 2024-12-20 11:29:29 +01:00
José Valim e5033c94ce Keep traces backwards compatible 2024-12-20 10:43:48 +01:00
José Valim 5d65d60721 Comment out canonical and update release notes 2024-12-19 20:34:17 +01:00
José Valim 3564c68cdc Release v1.18.0 2024-12-19 18:30:20 +01:00
José Valim 9df42f5a0a Update CHANGELOG 2024-12-19 17:26:59 +01:00
José Valim c6597bcb20 Collapse structs for better pretty printing in different scenarios 2024-12-19 15:13:18 +01:00
José Valim ca6edfd389 Include types in comparison warning 2024-12-19 15:13:14 +01:00
José Valim ed83c407a5 Simplify and optimize NaiveDateTime.utc_now 2024-12-19 15:13:12 +01:00
José Valim a426520b66 Improvements to descr pretty printing 2024-12-18 09:32:08 +01:00
Guillaume Duboc 1c0f585f40 Fuse maps and tuples for printing (#14079) 2024-12-18 09:32:06 +01:00
José Valim 7c86c4bdb3 Update CHANGELOG 2024-12-17 10:11:15 +01:00
José Valim 4931ab40a5 Do not warn for CaseTemplate options, closes #14077 2024-12-17 10:03:53 +01:00
José Valim c7f78e8015 Emit warnings for duplicate patterns instead of errors 2024-12-16 19:30:54 +01:00
José Valim 6a24bafbda Tag exceptions as dynamic, closes #14074 2024-12-16 16:13:04 +01:00
José Valim 8ddef06e81 Also encode Duration to JSON 2024-12-16 12:23:47 +01:00
José Valim 472c4e1a24 Avoid crashes on diagnostics with tabs, closes #14073 2024-12-16 12:09:18 +01:00
José Valim 305d8e603c Do not convert fun() into (... -> any()) and vice-versa in typespecs 2024-12-15 15:18:52 +01:00
Jonatan Kłosko 1c7fc86e9c Make :source_path in docs chunk a charlist (#14071) 2024-12-13 16:57:54 +01:00
José Valim 617dc72759 Fix Range.new deprecation assertion 2024-12-13 10:14:47 +01:00
José Valim 4329a3c2f2 Address bootstrap 2024-12-13 10:01:18 +01:00
José Valim 8e8c9a7334 Wrap Range.new in Function.identity to avoid tail call optimization 2024-12-13 09:55:39 +01:00
Michał Łępicki 36c06c3a56 Fix JSON.decode/3 spec (#14068)
The error clause was missing the :error tuple wrapping
2024-12-13 09:52:42 +01:00
Mathias Polligkeit 048cc2ddb1 Further improve warning message for range with negative step (#14070) 2024-12-13 09:52:42 +01:00
Wojtek Mach 048ae58d6d Do not set docs source_ref to -latest tags (#14065) 2024-12-12 11:51:41 +01:00
José Valim 85e00cddd5 Clarify warnings happen on compiled code 2024-12-12 09:52:30 +01:00
Jean Klingler 6c866e8ab2 Fix type warnings in pop_in/1 (#14064) 2024-12-12 17:47:23 +09:00
José Valim 55eca5dff5 Implement JSON.Encoder for Calendar types (#14061) 2024-12-12 00:38:44 +01:00
Panagiotis Nezis 070a6d51bf Improve warning in Range.new/2 with negative step (#14062) 2024-12-12 00:38:40 +01:00
José Valim 9c28fb7a39 Improve JSON and Float docs 2024-12-11 21:08:20 +01:00
Andrea Leopardi cda1a09c58 Improve error message on empty "mix do" (#14055) 2024-12-11 12:44:47 +01:00
José Valim 3ca0bddd9d Fix encoding specs 2024-12-11 12:16:33 +01:00
José Valim e07a91594b Update deprecations 2024-12-11 12:02:01 +01:00
José Valim dfc659104c Touch the compile path when writing .app file for the first time (#14053)
Closes #14049.
2024-12-11 10:01:00 +01:00
Shenghang Tsai a4fa71ab23 Update CHANGELOG.md (#14052) 2024-12-11 09:43:42 +01:00
EksperimentalandWojtek Mach 2273bafb47 Improve grammar in CHANGELOG.md (#14050)
Co-authored-by: Wojtek Mach <wojtekmach@users.noreply.github.com>
2024-12-11 08:41:11 +01:00
José Valim a78f1715dc Fix error message around single quote keywords 2024-12-10 19:30:39 +01:00
José Valim b709865976 Improvements to markdown rendering in CHANGELOG 2024-12-10 19:18:32 +01:00
José Valim da7e04a540 Improve docs around auto_reloading 2024-12-10 19:16:09 +01:00
José Valim 66c5908619 Release v1.18.0-rc.0 2024-12-10 18:32:26 +01:00
José Valim 31d8da6c77 Avoid mtime races on compile.app_test.exs 2024-12-10 15:41:16 +01:00
Daniel Gomez de Souza 3bdf5a912c Fix typo (#14046) 2024-12-10 15:34:32 +01:00
José Valim 0e9b5ec070 Branch out v1.18 2024-12-10 15:01:43 +01:00
143 changed files with 2699 additions and 1441 deletions
+4 -4
View File
@@ -3,12 +3,12 @@ name: CI for Markdown content
on:
push:
branches:
- 'main'
- "main"
paths:
- 'lib/**/*.md'
- "lib/**/*.md"
pull_request:
paths:
- 'lib/**/*.md'
- "lib/**/*.md"
workflow_dispatch:
jobs:
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Check out the repository
+1 -1
View File
@@ -81,7 +81,7 @@ jobs:
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
strategy:
matrix:
otp_version: ["25.3", "26.2", "27.1"]
otp_version: ["25.3", "26.2", "27.3"]
runs-on: windows-2022
steps:
- name: Configure Git
+3 -3
View File
@@ -10,7 +10,7 @@ permissions:
jobs:
notify:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
name: Notify
steps:
- uses: actions/checkout@v4
@@ -18,8 +18,8 @@ jobs:
fetch-depth: 50
- uses: erlef/setup-beam@v1
with:
otp-version: '25.0'
elixir-version: '1.14.0'
otp-version: "27.3"
elixir-version: "1.18.3"
- name: Run Elixir script
env:
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
+78 -36
View File
@@ -55,7 +55,6 @@ jobs:
git push origin $ref_name --force
build:
needs: create_draft_release
strategy:
fail-fast: true
matrix:
@@ -80,26 +79,6 @@ jobs:
otp: ${{ matrix.otp }}
build_docs: ${{ matrix.build_docs }}
- name: "Attest release .exe provenance"
uses: actions/attest-build-provenance@v2
id: attest-exe-provenance
with:
subject-path: "elixir-otp-${{ matrix.otp }}.exe"
- name: "Copy release .exe provenance"
run: cp "$ATTESTATION" elixir-otp-${{ matrix.otp }}.exe.sigstore
env:
ATTESTATION: "${{ steps.attest-exe-provenance.outputs.bundle-path }}"
- name: "Attest release .zip provenance"
uses: actions/attest-build-provenance@v2
id: attest-zip-provenance
with:
subject-path: "elixir-otp-${{ matrix.otp }}.zip"
- name: "Copy release .zip provenance"
run: cp "$ATTESTATION" elixir-otp-${{ matrix.otp }}.zip.sigstore
env:
ATTESTATION: "${{ steps.attest-zip-provenance.outputs.bundle-path }}"
- name: "Attest docs provenance"
uses: actions/attest-build-provenance@v2
id: attest-docs-provenance
@@ -112,11 +91,23 @@ jobs:
env:
ATTESTATION: "${{ steps.attest-docs-provenance.outputs.bundle-path }}"
- name: "Upload release artifacts"
- name: Create Docs Hashes
if: ${{ matrix.build_docs }}
run: |
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
- name: "Upload linux release artifacts"
uses: actions/upload-artifact@v4
with:
name: elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}*
name: build-linux-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.zip
- name: "Upload windows release artifacts"
uses: actions/upload-artifact@v4
with:
name: build-windows-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.exe
- name: "Upload doc artifacts"
uses: actions/upload-artifact@v4
@@ -125,20 +116,26 @@ jobs:
name: Docs
path: Docs.zip*
upload-release:
needs: build
runs-on: windows-2022
sign:
needs: [build]
strategy:
fail-fast: true
matrix:
otp: [25, 26, 27]
flavor: [windows, linux]
env:
RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }}
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-22.04' || 'windows-2022' }}
steps:
- uses: actions/download-artifact@v4
- run: |
mv elixir-otp-*/* .
mv Docs/* .
shell: bash
with:
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
- name: "Sign files with Trusted Signing"
if: github.repository == 'elixir-lang/elixir'
if: github.repository == 'elixir-lang/elixir' && matrix.flavor == 'windows'
uses: azure/trusted-signing-action@v0.5.0
with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -153,6 +150,50 @@ jobs:
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: "Attest release provenance"
uses: actions/attest-build-provenance@v2
id: attest-provenance
with:
subject-path: ${{ env.RELEASE_FILE }}
- name: "Copy release .zip provenance"
shell: bash
run: cp "$ATTESTATION" "${RELEASE_FILE}.sigstore"
env:
ATTESTATION: "${{ steps.attest-provenance.outputs.bundle-path }}"
- name: Create Release Hashes
if: matrix.flavor == 'windows'
shell: pwsh
run: |
$sha1 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA1
$sha1.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha1sum"
$sha256 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA256
$sha256.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha256sum"
- name: Create Release Hashes
if: matrix.flavor == 'linux'
shell: bash
run: |
shasum -a 1 "$RELEASE_FILE" > "${RELEASE_FILE}.sha1sum"
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
- name: "Upload linux release artifacts"
uses: actions/upload-artifact@v4
with:
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
path: ${{ env.RELEASE_FILE }}*
upload-release:
needs: [create_draft_release, build, sign]
runs-on: ubuntu-22.04
steps:
- uses: actions/download-artifact@v4
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
- name: Upload Pre-built
shell: bash
env:
@@ -179,7 +220,7 @@ jobs:
Docs.zip.sigstore
upload-builds-hex-pm:
needs: build
needs: [build, sign]
runs-on: ubuntu-22.04
concurrency: builds-hex-pm
env:
@@ -193,6 +234,9 @@ jobs:
OTP_GENERIC_VERSION: "25"
steps:
- uses: actions/download-artifact@v4
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
- name: Init purge keys file
run: |
@@ -202,7 +246,6 @@ jobs:
run: |
ref_name=${{ github.ref_name }}
mv elixir-otp-*/* .
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
dest=${zip/elixir/${ref_name}}
surrogate_key=${dest/.zip$/}
@@ -221,7 +264,6 @@ jobs:
done
- name: Upload Docs to S3
working-directory: Docs
run: |
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
@@ -19,8 +19,6 @@ runs:
run: |
make Precompiled.zip
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
shasum -a 1 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha1sum
shasum -a 256 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha256sum
echo "$PWD/bin" >> $GITHUB_PATH
- name: Install NSIS
shell: bash
@@ -34,8 +32,6 @@ runs:
export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
(cd lib/elixir/scripts/windows_installer && ./build.sh)
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
shasum -a 1 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha1sum
shasum -a 256 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha256sum
- name: Get ExDoc ref
if: ${{ inputs.build_docs }}
shell: bash
@@ -66,5 +62,3 @@ runs:
run: |
git fetch --tags
make Docs.zip
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
+168 -27
View File
@@ -4,13 +4,13 @@ Elixir v1.18 is an impressive release with improvements across the two main effo
## Type system improvements
The most exciting change in Elixir v1.18 is type checking of function calls, alongside gradual inference of patterns and return types. To understand how this will impact your programs, consider the following code:
The most exciting change in Elixir v1.18 is type checking of function calls, alongside gradual inference of patterns and return types. To understand how this will impact your programs, consider the following code in "lib/user.ex":
```elixir
defmodule User do
defstruct [:age, :car_choice]
def drive(%User{age: age, car_choice: car}, cars_choices) when age >= 18 do
def drive(%User{age: age, car_choice: car}, car_choices) when age >= 18 do
if car in car_choices do
{:ok, car}
else
@@ -24,9 +24,9 @@ defmodule User do
end
```
Elixir's type system will infer the `drive` function expects a `%User{}` struct as input and returns either `{:ok, dynamic()}`, `{:error, :no_choice}`, or `{:error, :not_allowed}`.
Elixir's type system will infer that the `drive/2` function expects a `%User{}` struct and returns either `{:ok, dynamic()}`, `{:error, :no_choice}`, or `{:error, :not_allowed}`.
Therefore, the following code should emit a violation, due to an invalid argument:
Therefore, the following code in a separate module (either in a separate or the same file), should emit a violation, due to an invalid argument:
```elixir
User.drive({:ok, %User{}}, car_choices)
@@ -156,40 +156,34 @@ More migrations may be added in future releases.
This release includes official support for JSON encoding and decoding.
Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and
[ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/)
standards.
Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and [ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/) standards.
### Encoding
Encoding can be done via `JSON.encode!/1` and `JSON.encode_to_iodata!/1` functions.
The default encoding rules are applied as follows:
Encoding can be done via `JSON.encode!/1` and `JSON.encode_to_iodata!/1` functions. The default encoding rules are applied as follows:
| **Elixir** | **JSON** |
|------------------------|----------|
| `integer() \| float()` | Number |
| `true \| false ` | Boolean |
| `nil` | Null |
| `binary()` | String |
| `atom()` | String |
| `list()` | Array |
| `%{binary() => _}` | Object |
| `%{atom() => _}` | Object |
| `%{integer() => _}` | Object |
| **Elixir** | **JSON** |
|-----------------------------|----------|
| `integer() \| float()` | Number |
| `true \| false ` | Boolean |
| `nil` | Null |
| `binary()` | String |
| `atom()` | String |
| `list()` | Array |
| `%{String.Chars.t() => _}` | Object |
You may also implement the `JSON.Encoder` protocol for custom data structures.
If you have a struct, you can derive the implementation of the `JSON.Encoder`
by specifying which fields should be encoded to JSON:
You may also implement the `JSON.Encoder` protocol for custom data structures. Elixir already implements the protocol for all Calendar types.
If you have a struct, you can derive the implementation of the `JSON.Encoder` by specifying which fields should be encoded to JSON:
```elixir
@derive {JSON.Encoder, only: [....]}
@derive {JSON.Encoder, only: [...]}
defstruct ...
```
### Decoding
Decoding can be done via `JSON.decode/2` and `JSON.decode!/2` functions.
The default decoding rules are applied as follows:
Decoding can be done via `JSON.decode/2` and `JSON.decode!/2` functions. The default decoding rules are applied as follows:
| **JSON** | **Elixir** |
|----------|------------------------|
@@ -199,6 +193,16 @@ The default decoding rules are applied as follows:
| String | `binary()` |
| Object | `%{binary() => _}` |
## Language server listeners
4 months ago, we welcomed [the Official Language Server team](https://elixir-lang.org/blog/2024/08/15/welcome-elixir-language-server-team/), with the goal of unifying the efforts behind code intelligence, tools, and editors in Elixir. Elixir v1.18 brings new features on this front by introducing locks and listeners to its compilation. Let's understand what it means.
At the moment, all language server implementations have their own compilation environment. This means that your project and dependencies during development are compiled once, for your own use, and then again for the language server. This duplicate effort could cause the language server experience to lag, when it could be relying on the already compiled artifacts of your project.
This release address by introducing a compiler lock, ensuring that only a single operating system process running Elixir compiles your project at a given moment, and by providing the ability for one operating system process to listen to the compilation results of others. In other words, different Elixir instances can now communicate over the same compilation build, instead of racing each other.
These enhancements do not only improve editor tooling, but they also directly benefit projects like IEx and Phoenix. For example, you can invoke `IEx.configure(auto_reload: true)` and IEx will automatically reload modules changed elsewhere, either by a separate terminal or your IDE.
## Potential incompatibilities
This release no longer supports WERL (a graphical user interface on Windows used by Erlang 25 and earlier). For a better user experience on Windows terminals, use Erlang/OTP 26+ (this is also the last Elixir release to support Erlang/OTP 25).
@@ -219,7 +223,138 @@ You may also prefer to write using guards:
def foo(x, y, z) when x == y and y == z
## v1.18.0-dev
## v1.18.4 (2025-05-21)
This release includes initial support for Erlang/OTP 28, for those who want to try it out. In such cases, you may use Elixir v1.18.4 precompiled for Erlang/OTP 27, as it is binary compatible with Erlang/OTP 28.
### 1. Enhancements
#### IEx
* [IEx.Helpers] Add `IEx.Helpers.process_info/1` which prints process information
#### Mix
* [mix compile] Support the `--no-listeners` option
* [mix local] Retry HTTP requests with disabled middlebox comp mode depending on the failure reason
* [mix local.hex] Install Hex per OTP release
* [mix local.rebar] Install Hex per OTP release
* [mix run] Support the `--no-listeners` option
### 2. Bug fixes
#### Elixir
* [Kernel] Emit trace events for `@on_definition` callbacks
* [Kernel] Emit trace events for `@on_load` callbacks
* [Kernel] Emit trace events for `super` calls
* [Kernel] Emit trace events for imported function calls
* [Kernel] Optimize map unions to avoid building long lists
* [Kernel] Do not crash when type checking nested bitstrings in patterns
* [Kernel] Do not crash when non-binary bitstring is given as struct default value
* [Kernel] Recompile regexes when escaped from module attributes for Erlang/OTP 28 compatibility
* [Kernel] Preserve backwards compatibility in `elixir_erl`
#### Mix
* [mix deps.get] Ensure git checkout works when there are untracked files in the dependency
* [mix loadpaths] Do not run listeners when not checking the deps
## v1.18.3 (2025-03-06)
### 1. Enhancements
#### Elixir
* [JSON] Encode any JSON key to string
* [Kernel] Allow `<<_::3*8>>` in typespecs
#### Mix
* [mix loadpaths] Support `--no-listeners` option
### 2. Bug fixes
#### Elixir
* [CLI] Fix `--no-color` not setting `:ansi_enabled` to false
* [Protocol] Return correct implementation for an invalid struct pointing to `nil`
* [Stream] Do not raise when `Stream.cycle/1` is explicitly halted
#### ExUnit
* [ExUnit.Diff] Fix regression when diffing nested improper lists
#### IEx
* [IEx.Autocomplete] Fix autocomplete crash when expanding struct with `__MODULE__`
* [IEx.Helpers] Do not purge on `recompile` if IEx is not running
## v1.18.2 (2025-01-22)
### 1. Enhancements
#### Elixir
* [CLI] Add `--color`/`--no-color` for enabling and disabling of ANSI colors
* [Code.Fragment] Provide more AST context when invoking `container_cursor_to_quoted` with trailing fragments
* [Regex] Ensure compatibility with Erlang/OTP 28+ new Regex engine
#### Mix
* [mix] Print compilation lock waiting message to stderr
* [mix] Add an environment variable to optionally disable compilation locking
### 2. Bug fixes
#### Elixir
* [CLI] Temporarily remove PowerShell scripts for `elixir`, `elixirc`, and `mix` on Windows, as they leave the shell broken after quitting Erlang
#### ExUnit
* [ExUnit] Fix crash when diffing bitstring specifiers
#### IEx
* [IEx.Autocomplete] Fix crashing when autocompleting structs with runtime values
#### Mix
* [mix] Track compilation locks per user to avoid permission errors
* [mix deps.update] Ensure Git dependencies can be upgraded by doing so against the origin
## v1.18.1 (2024-12-24)
### 1. Enhancements
* [Kernel] Do not emit type violation warnings when comparing or matching against literals
* [Kernel] Do not validate clauses of private overridable functions
### 2. Bug fixes
#### Elixir
* [Code.Fragment] Ensure `Code.Fragment.container_cursor_to_quoted/2` with `:trailing_fragment` parses expressions that were supported in previous versions
* [Kernel] Do not crash when typing violation is detected on dynamic dispatch
* [Kernel] Properly annotate the source for warnings emitted by the compiler with the `@file` annotation
* [Kernel] Properly annotate the source for warnings emitted by the type system with the `@file` annotation
* [Kernel] Remove `:no_parens` metadata when using capture with arity on all cases
* [Kernel] Ensure diagnostic traces are kept backwards compatible
#### ExUnit
* [ExUnit.Case] Ensure async groups do not run concurrenly while the test suite is still loading
* [ExUnit.Case] Ensure `--repeat-until-failure` can be combined with groups
#### Mix
* [mix compile.elixir] Store compilation results if compilation fails due to `--warnings-as-errors`
* [mix deps.loadpaths] Add build lock
* [mix escript.build] Ensure build succeeds when protocol consolidation is disabled
* [Mix.Shell] Ensure encoding is properly respected on Windows and Unix systems
## v1.18.0 (2024-12-19)
### 1. Enhancements
@@ -234,6 +369,8 @@ You may also prefer to write using guards:
* [Config] Add `Config.read_config/1`
* [Enumerable] Add `Enum.product_by/2` and `Enum.sum_by/2`
* [Exception] Add `MissingApplicationsError` exception to denote missing applications
* [JSON] Add a new `JSON` module with encoding and decoding functionality
* [JSON] Implement `JSON.Encoder` for all Calendar types
* [Kernel] Update source code parsing to match [UTS #55](https://www.unicode.org/reports/tr55/) latest recommendations. In particular, mixed script is allowed in identifiers as long as they are separate by underscores (`_`), such as `http_сервер`. Previously allowed highly restrictive identifiers, which mixed Latin and other scripts, such as the japanese word for t-shirt, `Tシャツ`, now require the underscore as well
* [Kernel] Warn on bidirectional confusability in identifiers
* [Kernel] Verify the type of the binary generators
@@ -261,6 +398,7 @@ You may also prefer to write using guards:
#### IEx
* [IEx] Add `IEx.configure(auto_reload: true)` to automatically pick up modules recompiled from other operating system processes
* [IEx] Add `:dot_iex` support to `IEx.configure/1`
* [IEx] Add report for normal/shutdown exits in IEx
@@ -284,6 +422,7 @@ You may also prefer to write using guards:
* [Code.Fragment] Properly handle keyword keys as their own entry
* [Inspect.Algebra] Ensure `next_break_fits` respects `line_length`
* [Kernel] Validate AST on `unquote` and `unquote_splicing` to provide better error reports instead of failing too late inside the compiler
* [Kernel] Avoid crashes when emitting diagnostics on code using \t for indentation
* [Module] Include module attribute line and name when tracing its aliases
* [Stream] Do not halt streams twice in `Stream.transform/5`
* [URI] Fix a bug when a schemaless URI is given to `URI.merge/2`
@@ -334,6 +473,8 @@ You may also prefer to write using guards:
#### Mix
* [mix cmd] Deprecate `mix cmd --app APP` in favor of `mix do --app APP`
* [mix compile] `:warnings_as_errors` configuration in `:elixirc_options` is deprecated. Instead pass the `--warnings-as-errors` flag to `mix compile`. Alternatively, you might alias the task: `aliases: [compile: "compile --warnings-as-errors"]`
* [mix test] `:warnings_as_errors` configuration in `:test_elixirc_options` is deprecated. Instead pass the `--warnings-as-errors` flag to `mix test`. Alternatively, you might alias the task: `aliases: [test: "test --warnings-as-errors"]`
* [Mix.Tasks.Compile] Deprecate `compilers/0` in favor of `Mix.Task.Compiler.compilers/0`
## v1.17
+2 -2
View File
@@ -2,7 +2,7 @@ PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man
CANONICAL := main/
# CANONICAL := main/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ERLC := erlc -I lib/elixir/include
ERL_MAKE := erl -make
@@ -17,7 +17,7 @@ INSTALL_DIR = $(INSTALL) -m755 -d
INSTALL_DATA = $(INSTALL) -m644
INSTALL_PROGRAM = $(INSTALL) -m755
GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null ))
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | tail -1) )
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | grep -v latest | tail -1))
SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible
SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
+2 -2
View File
@@ -2,7 +2,7 @@
## Shipping a new version
1. Update version in /VERSION, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
1. Update version in /VERSION, bin/elixir, and bin/elixir.bat
2. Ensure /CHANGELOG.md is updated, versioned and add the current date
@@ -30,7 +30,7 @@
### Back in main
1. Bump /VERSION file, bin/elixir and bin/elixir.bat
1. Bump /VERSION file, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
2. Start new /CHANGELOG.md
+2 -3
View File
@@ -6,12 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.18 | Development
1.17 | Bug fixes and security patches
1.18 | Bug fixes and security patches
1.17 | Security patches only
1.16 | Security patches only
1.15 | Security patches only
1.14 | Security patches only
1.13 | Security patches only
## Announcements
+1 -1
View File
@@ -1 +1 @@
1.18.0-dev
1.18.4
+3 -2
View File
@@ -1,7 +1,7 @@
#!/bin/sh
set -e
ELIXIR_VERSION=1.18.0-dev
ELIXIR_VERSION=1.18.4
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
@@ -18,6 +18,7 @@ Usage: $(basename "$0") [options] [.exs file] [data]
-pz "PATH" Appends the given path to Erlang code path (*)
-v, --version Prints Erlang/OTP and Elixir versions (standalone)
--color, --no-color Enables or disables ANSI coloring
--erl "SWITCHES" Switches to be passed down to Erlang (*)
--eval "COMMAND" Evaluates the given command, same as -e (*)
--logger-otp-reports BOOL Enables or disables OTP reporting
@@ -111,7 +112,7 @@ while [ $I -le $LENGTH ]; do
C=1
MODE="iex"
;;
-v|--no-halt)
-v|--no-halt|--color|--no-color)
C=1
;;
-e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg)
+6 -3
View File
@@ -1,6 +1,6 @@
@echo off
set ELIXIR_VERSION=1.18.0-dev
set ELIXIR_VERSION=1.18.4
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
@@ -24,6 +24,7 @@ echo -pa "PATH" Prepends the given path to Erlang code path
echo -pz "PATH" Appends the given path to Erlang code path (*)
echo -v, --version Prints Erlang/OTP and Elixir versions (standalone)
echo.
echo --color, --no-color Enables or disables ANSI coloring
echo --erl "SWITCHES" Switches to be passed down to Erlang (*)
echo --eval "COMMAND" Evaluates the given command, same as -e (*)
echo --logger-otp-reports BOOL Enables or disables OTP reporting
@@ -107,6 +108,8 @@ if ""==!par:-pz=! (shift && goto startloop)
if ""==!par:-v=! (goto startloop)
if ""==!par:--version=! (goto startloop)
if ""==!par:--no-halt=! (goto startloop)
if ""==!par:--color=! (goto startloop)
if ""==!par:--no-color=! (goto startloop)
if ""==!par:--remsh=! (shift && goto startloop)
if ""==!par:--dot-iex=! (shift && goto startloop)
if ""==!par:--dbg=! (shift && goto startloop)
@@ -133,9 +136,9 @@ if not defined useIEx (
set beforeExtra=-noshell -elixir_root "%SCRIPT_PATH%..\lib" -pa "%SCRIPT_PATH%..\lib\elixir\ebin" %beforeExtra%
if defined ELIXIR_CLI_DRY_RUN (
echo "%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
echo "%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
) else (
"%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
"%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
)
exit /B %ERRORLEVEL%
:end
-304
View File
@@ -1,304 +0,0 @@
#!/usr/bin/env pwsh
$ELIXIR_VERSION = "1.18.0-dev"
$scriptPath = Split-Path -Parent $PSCommandPath
$erlExec = "erl"
# The iex.ps1, elixirc.ps1 and mix.ps1 scripts may populate this var.
if ($null -eq $allArgs) {
$allArgs = $args
}
function PrintElixirHelp {
$scriptName = Split-Path -Leaf $PSCommandPath
$help = @"
Usage: $scriptName [options] [.exs file] [data]
## General options
-e "COMMAND" Evaluates the given command (*)
-h, --help Prints this message (standalone)
-r "FILE" Requires the given files/patterns (*)
-S SCRIPT Finds and executes the given script in `$PATH
-pr "FILE" Requires the given files/patterns in parallel (*)
-pa "PATH" Prepends the given path to Erlang code path (*)
-pz "PATH" Appends the given path to Erlang code path (*)
-v, --version Prints Erlang/OTP and Elixir versions (standalone)
--erl "SWITCHES" Switches to be passed down to Erlang (*)
--eval "COMMAND" Evaluates the given command, same as -e (*)
--logger-otp-reports BOOL Enables or disables OTP reporting
--logger-sasl-reports BOOL Enables or disables SASL reporting
--no-halt Does not halt the Erlang VM after execution
--short-version Prints Elixir version (standalone)
Options given after the .exs file or -- are passed down to the executed code.
Options can be passed to the Erlang runtime using `$ELIXIR_ERL_OPTIONS or --erl.
## Distribution options
The following options are related to node distribution.
--cookie COOKIE Sets a cookie for this distributed node
--hidden Makes a hidden node
--name NAME Makes and assigns a name to the distributed node
--rpc-eval NODE "COMMAND" Evaluates the given command on the given remote node (*)
--sname NAME Makes and assigns a short name to the distributed node
--name and --sname may be set to undefined so one is automatically generated.
## Release options
The following options are generally used under releases.
--boot "FILE" Uses the given FILE.boot to start the system
--boot-var VAR "VALUE" Makes `$VAR available as VALUE to FILE.boot (*)
--erl-config "FILE" Loads configuration in FILE.config written in Erlang (*)
--vm-args "FILE" Passes the contents in file as arguments to the VM
--pipe-to is not supported via PowerShell.
** Options marked with (*) can be given more than once.
** Standalone options can't be combined with other options.
"@
Write-Host $help
}
if (($allArgs.Count -eq 1) -and ($allArgs[0] -eq "--short-version")) {
Write-Host "$ELIXIR_VERSION"
exit
}
if (($allArgs.Count -eq 0) -or (($allArgs.Count -eq 1) -and ($allArgs[0] -in @("-h", "--help")))) {
PrintElixirHelp
exit 1
}
function NormalizeArg {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string[]] $Items
)
$Items -join ","
}
function QuoteString {
param(
[Parameter(ValueFromPipeline = $true)]
[string] $Item
)
# We surround the string with double quotes, in order to preserve its contents as
# only one command arg.
# This is needed because PowerShell consider spaces as separator of arguments.
# The double quotes around will be removed when PowerShell process the argument.
# See: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#passing-quoted-strings-to-external-commands
if ($Item.Contains(" ")) {
'"' + $Item + '"'
}
else {
$Item
}
}
$elixirParams = @()
$erlangParams = @()
$beforeExtras = @()
$allOtherParams = @()
$runErlPipe = $null
$runErlLog = $null
for ($i = 0; $i -lt $allArgs.Count; $i++) {
$private:arg = $allArgs[$i]
switch -exact ($arg) {
{ $_ -in @("-e", "-r", "-pr", "-pa", "-pz", "--eval", "--remsh", "--dot-iex", "--dbg") } {
$private:nextArg = NormalizeArg($allArgs[++$i])
$elixirParams += $arg
$elixirParams += $nextArg
break
}
{ $_ -in @("-v", "--version") } {
# Standalone options goes only once in the Elixir params, when they are empty.
if (($elixirParams.Count -eq 0) -and ($allOtherParams.Count -eq 0)) {
$elixirParams += $arg
}
else {
$allOtherParams += $arg
}
break
}
"--no-halt" {
$elixirParams += $arg
break
}
"--cookie" {
$erlangParams += "-setcookie"
$erlangParams += $allArgs[++$i]
break
}
"--hidden" {
$erlangParams += "-hidden"
break
}
"--name" {
$erlangParams += "-name"
$erlangParams += $allArgs[++$i]
break
}
"--sname" {
$erlangParams += "-sname"
$erlangParams += $allArgs[++$i]
break
}
"--boot" {
$erlangParams += "-boot"
$erlangParams += $allArgs[++$i]
break
}
"--erl-config" {
$erlangParams += "-config"
$erlangParams += $allArgs[++$i]
break
}
"--vm-args" {
$erlangParams += "-args_file"
$erlangParams += $allArgs[++$i]
break
}
"--logger-otp-reports" {
$private:tempVal = $allArgs[$i + 1]
if ($tempVal -in @("true", "false")) {
$erlangParams += @("-logger", "handle_otp_reports", $allArgs[++$i])
}
break
}
"--logger-sasl-reports" {
$private:tempVal = $allArgs[$i + 1]
if ($tempVal -in @("true", "false")) {
$erlangParams += @("-logger", "handle_sasl_reports", $allArgs[++$i])
}
break
}
"--erl" {
$private:erlFlags = $allArgs[++$i] -split " "
$beforeExtras += $erlFlags
break
}
"+iex" {
$elixirParams += "+iex"
$useIex = $true
break
}
"+elixirc" {
$elixirParams += "+elixirc"
break
}
"--rpc-eval" {
$private:key = $allArgs[++$i]
$private:value = $allArgs[++$i]
if ($null -eq $key) {
Write-Error "--rpc-eval: NODE must be present"
exit 1
}
if ($null -eq $value) {
Write-Error "--rpc-eval: COMMAND for the '$key' node must be present"
exit 1
}
$elixirParams += "--rpc-eval"
$elixirParams += $key
$elixirParams += $value
break
}
"--boot-var" {
$private:key = $allArgs[++$i]
$private:value = $allArgs[++$i]
if ($null -eq $key) {
Write-Error "--boot-var: VAR must be present"
exit 1
}
if ($null -eq $value) {
Write-Error "--boot-var: Value for the '$key' var must be present"
exit 1
}
$elixirParams += "-boot_var"
$elixirParams += $key
$elixirParams += $value
break
}
Default {
$private:normalized = NormalizeArg $arg
$allOtherParams += $normalized
break
}
}
}
if ($null -eq $useIEx) {
$beforeExtras = @("-s", "elixir", "start_cli") + $beforeExtras
}
$beforeExtras = @("-pa", "$(Join-Path $scriptPath -ChildPath "../lib/elixir/ebin")") + $beforeExtras
$beforeExtras = @("-noshell", "-elixir_root", "$(Join-Path $scriptPath -ChildPath "../lib")") + $beforeExtras
$allParams = @()
if ($null -ne $env:ELIXIR_ERL_OPTIONS) {
$private:erlFlags = $env:ELIXIR_ERL_OPTIONS -split " "
$allParams += $erlFlags
}
$allParams += $erlangParams
$allParams += $beforeExtras
$allParams += "-extra"
$allParams += $elixirParams
$allParams += $allOtherParams
$binSuffix = ""
# The variable is available after PowerShell 7.2. Previous to that, PS only worked on Windows.
if ($isWindows -or ($null -eq $isWindows)) {
$binSuffix = ".exe"
}
$binPath = "$erlExec$binSuffix"
# We double the double-quotes because they are going to be escaped by arguments parsing.
$paramsPart = $allParams | ForEach-Object -Process { QuoteString($_ -replace "`"", "`"`"") }
if ($env:ELIXIR_CLI_DRY_RUN) {
Write-Host "$binPath $paramsPart"
}
else {
$output = Start-Process -FilePath $binPath -ArgumentList $paramsPart -NoNewWindow -Wait -PassThru
exit $output.ExitCode
}
-35
View File
@@ -1,35 +0,0 @@
#!/usr/bin/env pwsh
$scriptName = Split-Path -Leaf $PSCommandPath
if (($args.Count -eq 0) -or ($args[0] -in @("-h", "--help"))) {
Write-Host @"
Usage: $scriptName [elixir switches] [compiler switches] [.ex files]
-h, --help Prints this message and exits
-o The directory to output compiled files
-v, --version Prints Elixir version and exits (standalone)
--ignore-module-conflict Does not emit warnings if a module was previously defined
--no-debug-info Does not attach debug info to compiled modules
--no-docs Does not attach documentation to compiled modules
--profile time Profile the time to compile modules
--verbose Prints compilation status
--warnings-as-errors Treats warnings as errors and returns non-zero exit status
** Options given after -- are passed down to the executed code
** Options can be passed to the Erlang runtime using ELIXIR_ERL_OPTIONS
** Options can be passed to the Erlang compiler using ERL_COMPILER_OPTIONS
"@
exit
}
$scriptPath = Split-Path -Parent $PSCommandPath
$elixirMainScript = Join-Path -Path $scriptPath -ChildPath "elixir.ps1"
$prependedArgs = @("+elixirc")
$allArgs = $prependedArgs + $args
# The dot is going to evaluate the script with the vars defined here.
. $elixirMainScript
-30
View File
@@ -1,30 +0,0 @@
#!/usr/bin/env pwsh
$scriptName = Split-Path -Leaf $PSCommandPath
if ($args[0] -in @("-h", "--help")) {
Write-Host @"
Usage: $scriptName [options] [.exs file] [data]
The following options are exclusive to IEx:
--dbg pry Sets the backend for Kernel.dbg/2 to IEx.pry/0
--dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environment.
Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists.
If FILE is empty, then no file will be loaded.
--remsh NAME Connects to a node using a remote shell.
It accepts all other options listed by "elixir --help".
"@
exit
}
$scriptPath = Split-Path -Parent $PSCommandPath
$elixirMainScript = Join-Path -Path $scriptPath -ChildPath "elixir.ps1"
$prependedArgs = @("--no-halt", "--erl", "-user elixir", "+iex")
$allArgs = $prependedArgs + $args
# The dot is going to evaluate the script with the vars defined here.
. $elixirMainScript
+21 -11
View File
@@ -1,13 +1,23 @@
#!/usr/bin/env pwsh
# Store path to mix.bat as a FileInfo object
$mixBatPath = (Get-ChildItem (((Get-ChildItem $MyInvocation.MyCommand.Path).Directory.FullName) + '\mix.bat'))
$newArgs = @()
$scriptPath = Split-Path -Parent $PSCommandPath
$elixirMainScript = Join-Path -Path $scriptPath -ChildPath "elixir.ps1"
for ($i = 0; $i -lt $args.length; $i++)
{
if ($args[$i] -is [array])
{
# Commas created the array so we need to reintroduce those commas
for ($j = 0; $j -lt $args[$i].length - 1; $j++)
{
$newArgs += ($args[$i][$j] + ',')
}
$newArgs += $args[$i][-1]
}
else
{
$newArgs += $args[$i]
}
}
$mixFile = Join-Path -Path $scriptPath -ChildPath "mix"
$prependedArgs = @($mixFile)
$allArgs = $prependedArgs + $args
# The dot is going to evaluate the script with the vars defined here.
. $elixirMainScript
# Corrected arguments are ready to pass to batch file
& $mixBatPath $newArgs
+19 -20
View File
@@ -110,30 +110,12 @@ defmodule NaiveDateTime do
@spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t
def utc_now(calendar_or_time_unit \\ Calendar.ISO)
def utc_now(Calendar.ISO) do
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
Calendar.ISO.from_unix(:os.system_time(), :native)
%NaiveDateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: Calendar.ISO
}
end
def utc_now(time_unit) when time_unit in [:microsecond, :millisecond, :second, :native] do
utc_now(time_unit, Calendar.ISO)
end
def utc_now(calendar) do
calendar
|> DateTime.utc_now()
|> DateTime.to_naive()
utc_now(:native, calendar)
end
@doc """
@@ -158,7 +140,20 @@ defmodule NaiveDateTime do
@spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t
def utc_now(time_unit, calendar)
when time_unit in [:native, :microsecond, :millisecond, :second] do
DateTime.utc_now(time_unit, calendar) |> DateTime.to_naive()
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
Calendar.ISO.from_unix(System.os_time(time_unit), time_unit)
%NaiveDateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: Calendar.ISO
}
|> convert!(calendar)
end
@doc """
@@ -1265,6 +1260,10 @@ defmodule NaiveDateTime do
{:ok, t} | {:error, :incompatible_calendars}
# Keep it multiline for proper function clause errors.
def convert(%NaiveDateTime{calendar: calendar} = ndt, calendar) do
{:ok, ndt}
end
def convert(
%{
calendar: calendar,
+3 -1
View File
@@ -1709,7 +1709,9 @@ defmodule Code do
def put_compiler_option(:warnings_as_errors, _value) do
IO.warn(
":warnings_as_errors is deprecated as part of Code.put_compiler_option/2, " <>
"pass it as option to Kernel.ParallelCompiler instead"
"instead you must pass it as a --warnings-as-errors flag. " <>
"If you need to set it as a default in a mix task, you can also set it under aliases: " <>
"[compile: \"compile --warnings-as-errors\"]"
)
:ok
+23 -14
View File
@@ -1151,7 +1151,7 @@ defmodule Code.Fragment do
{rev_tokens, rev_terminators} =
with [close, open, {_, _, :__cursor__} = cursor | rev_tokens] <- rev_tokens,
{_, [_ | after_fn]} <- Enum.split_while(rev_terminators, &(elem(&1, 0) != :fn)),
true <- maybe_missing_stab?(rev_tokens),
true <- maybe_missing_stab?(rev_tokens, false),
[_ | rev_tokens] <- Enum.drop_while(rev_tokens, &(elem(&1, 0) != :fn)) do
{[close, open, cursor | rev_tokens], after_fn}
else
@@ -1165,7 +1165,7 @@ defmodule Code.Fragment do
tokens =
with {before_start, [_ | _] = after_start} <-
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
true <- maybe_missing_stab?(rev_tokens),
true <- maybe_missing_stab?(rev_tokens, true),
opts =
Keyword.put(opts, :check_terminators, {:cursor, before_start}),
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
@@ -1173,6 +1173,14 @@ defmodule Code.Fragment do
trailing_tokens =
reverse_tokens(meta[:line], meta[:column], trailing_rev_tokens, after_start)
# If the cursor has its own line, then we do not trim new lines trailing tokens.
# Otherwise we want to drop any newline so we drop the next tokens after eol.
trailing_tokens =
case rev_tokens do
[_close, _open, {_, _, :__cursor__}, {:eol, _} | _] -> trailing_tokens
_ -> Enum.drop_while(trailing_tokens, &match?({:eol, _}, &1))
end
Enum.reverse(rev_tokens, drop_tokens(trailing_tokens, 0))
else
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
@@ -1196,12 +1204,16 @@ defmodule Code.Fragment do
Enum.reverse(tokens, terminators)
end
# Otherwise we drop all tokens, trying to build a minimal AST
# for cursor completion.
defp drop_tokens([{:"}", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:"]", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:")", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:">>", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:end, _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:",", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:";", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:eol, _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:stab_op, _, :->} | _] = tokens, 0), do: tokens
defp drop_tokens([{:"}", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
@@ -1220,16 +1232,13 @@ defmodule Code.Fragment do
defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter)
defp drop_tokens([], 0), do: []
defp maybe_missing_stab?([{:after, _} | _]), do: true
defp maybe_missing_stab?([{:do, _} | _]), do: true
defp maybe_missing_stab?([{:fn, _} | _]), do: true
defp maybe_missing_stab?([{:else, _} | _]), do: true
defp maybe_missing_stab?([{:catch, _} | _]), do: true
defp maybe_missing_stab?([{:rescue, _} | _]), do: true
defp maybe_missing_stab?([{:stab_op, _, :->} | _]), do: false
defp maybe_missing_stab?([{:eol, _}, next | _]) when elem(next, 0) != :",", do: false
defp maybe_missing_stab?([_ | tail]), do: maybe_missing_stab?(tail)
defp maybe_missing_stab?([]), do: false
defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:fn, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:else, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:catch, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:rescue, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:stab_op, _, :->} | _], stab_choice?), do: stab_choice?
defp maybe_missing_stab?([_ | tail], stab_choice?), do: maybe_missing_stab?(tail, stab_choice?)
defp maybe_missing_stab?([], _stab_choice?), do: false
end
+5 -11
View File
@@ -27,11 +27,6 @@ defmodule Code.Typespec do
end
end
def spec_to_quoted(name, {:type, anno, :fun, []}) when is_atom(name) do
meta = meta(anno)
{:"::", meta, [{name, meta, []}, quote(do: term)]}
end
def spec_to_quoted(name, {:type, anno, :bounded_fun, [type, constrs]}) when is_atom(name) do
meta = meta(anno)
{:type, _, :fun, [{:type, _, :product, args}, result]} = type
@@ -288,7 +283,6 @@ defmodule Code.Typespec do
end
defp typespec_to_quoted({:type, anno, :binary, [arg1, arg2]}) do
[arg1, arg2] = for arg <- [arg1, arg2], do: typespec_to_quoted(arg)
line = meta(anno)[:line]
case {typespec_to_quoted(arg1), typespec_to_quoted(arg2)} do
@@ -317,10 +311,6 @@ defmodule Code.Typespec do
[{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}]
end
defp typespec_to_quoted({:type, anno, :fun, []}) do
typespec_to_quoted({:type, anno, :fun, [{:type, anno, :any}, {:type, anno, :any, []}]})
end
defp typespec_to_quoted({:type, anno, :range, [left, right]}) do
{:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]}
end
@@ -338,10 +328,14 @@ defmodule Code.Typespec do
{erl_to_ex_var(var), meta(anno), nil}
end
defp typespec_to_quoted({:op, anno, op, arg}) do
defp typespec_to_quoted({:op, anno, op, arg}) when op in [:+, :-] do
{op, meta(anno), [typespec_to_quoted(arg)]}
end
defp typespec_to_quoted({:op, anno, :*, arg1, arg2}) do
{:*, meta(anno), [typespec_to_quoted(arg1), typespec_to_quoted(arg2)]}
end
defp typespec_to_quoted({:remote_type, anno, [mod, name, args]}) do
remote_type(anno, mod, name, args)
end
+5 -1
View File
@@ -3837,6 +3837,9 @@ defmodule Enum do
@doc """
Enumerates the `enumerable`, removing all duplicate elements.
The first occurrence of each element is kept and all following
duplicates are removed. The overall order is preserved.
## Examples
iex> Enum.uniq([1, 2, 3, 3, 2, 1])
@@ -3862,7 +3865,8 @@ defmodule Enum do
considered duplicates if the return value of `fun` is equal for
both of them.
The first occurrence of each element is kept.
The first occurrence of each element is kept and all following
duplicates are removed. The overall order is preserved.
## Example
+1 -1
View File
@@ -2530,7 +2530,7 @@ defmodule ErlangError do
{:ok, reason, IO.iodata_to_binary([":\n\n" | Enum.map(args_errors, &arg_error/1)])}
general = extra[:general] ->
{:ok, reason, ": " <> general}
{:ok, reason, ": " <> IO.chardata_to_string(general)}
true ->
:error
+3 -9
View File
@@ -577,15 +577,9 @@ defmodule Float do
Returns a charlist which corresponds to the shortest text representation
of the given float.
The underlying algorithm changes depending on the Erlang/OTP version:
* For OTP >= 24, it uses the algorithm presented in "Ryū: fast
float-to-string conversion" in Proceedings of the SIGPLAN '2018
Conference on Programming Language Design and Implementation.
* For OTP < 24, it uses the algorithm presented in "Printing Floating-Point
Numbers Quickly and Accurately" in Proceedings of the SIGPLAN '1996
Conference on Programming Language Design and Implementation.
It uses the algorithm presented in "Ryū: fast float-to-string conversion"
in Proceedings of the SIGPLAN '2018 Conference on Programming Language
Design and Implementation.
For a configurable representation, use `:erlang.float_to_list/2`.
+52 -17
View File
@@ -146,7 +146,35 @@ end
defimpl JSON.Encoder, for: Map do
def encode(value, encoder) do
:elixir_json.encode_map(value, encoder)
case :maps.next(:maps.iterator(value)) do
:none ->
"{}"
{key, value, iterator} ->
[?{, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)]
end
end
defp next(iterator, encoder) do
case :maps.next(iterator) do
:none ->
"}"
{key, value, iterator} ->
[?,, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)]
end
end
# Erlang supports only numbers, binaries, and atoms as keys,
# we support anything that implements the String.Chars protocol.
defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder)
defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder)
defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder)
end
defimpl JSON.Encoder, for: [Date, Time, NaiveDateTime, DateTime, Duration] do
def encode(value, _encoder) do
[?", @for.to_iso8601(value), ?"]
end
end
@@ -169,17 +197,15 @@ defmodule JSON do
Elixir built-in data structures are encoded to JSON as follows:
| **Elixir** | **JSON** |
|------------------------|----------|
| `integer() \| float()` | Number |
| `true \| false ` | Boolean |
| `nil` | Null |
| `binary()` | String |
| `atom()` | String |
| `list()` | Array |
| `%{binary() => _}` | Object |
| `%{atom() => _}` | Object |
| `%{integer() => _}` | Object |
| **Elixir** | **JSON** |
|----------------------------|----------|
| `integer() \| float()` | Number |
| `true \| false ` | Boolean |
| `nil` | Null |
| `binary()` | String |
| `atom()` | String |
| `list()` | Array |
| `%{String.Chars.t() => _}` | Object |
You may also implement the `JSON.Encoder` protocol for custom data structures.
@@ -199,6 +225,8 @@ defmodule JSON do
@moduledoc since: "1.18.0"
@type encoder :: (term(), encoder() -> iodata())
@type decode_error_reason ::
{:unexpected_end, non_neg_integer()}
| {:invalid_byte, non_neg_integer(), byte()}
@@ -257,7 +285,8 @@ defmodule JSON do
For streaming decoding, see Erlang's `:json` module.
"""
@spec decode(binary(), term(), keyword()) :: {term(), term(), binary()} | decode_error_reason()
@spec decode(binary(), term(), keyword()) ::
{term(), term(), binary()} | {:error, decode_error_reason()}
def decode(binary, acc, decoders) when is_binary(binary) and is_list(decoders) do
decoders = Keyword.put_new(decoders, :null, nil)
@@ -326,13 +355,19 @@ defmodule JSON do
The second argument is a function that is recursively
invoked to encode a term.
> #### IO and performance {: .tip}
>
> If you need to encode data to be sent over the network
> or written to the filesystem, consider using the more
> efficient `encode_to_iodata!/2`.
## Examples
iex> JSON.encode!([123, "string", %{key: "value"}])
"[123,\"string\",{\"key\":\"value\"}]"
"""
@spec encode!(a, (a -> iodata())) :: binary() when a: var
@spec encode!(term(), encoder()) :: binary()
def encode!(term, encoder \\ &protocol_encode/2) do
IO.iodata_to_binary(encoder.(term, encoder))
end
@@ -353,7 +388,7 @@ defmodule JSON do
"[123,\"string\",{\"key\":\"value\"}]"
"""
@spec encode_to_iodata!(a, (a -> iodata())) :: iodata() when a: var
@spec encode_to_iodata!(term(), encoder()) :: iodata()
def encode_to_iodata!(term, encoder \\ &protocol_encode/2) do
encoder.(term, encoder)
end
@@ -365,7 +400,7 @@ defmodule JSON do
`encode!/2` and `encode_to_iodata!/2`. The default implementation
is an optimized dispatch to the `JSON.Encoder` protocol.
"""
@spec protocol_encode(a, (a -> iodata())) :: iodata() when a: var
@spec protocol_encode(term(), encoder()) :: iodata()
def protocol_encode(value, encoder) when is_atom(value) do
case value do
nil -> "null"
@@ -388,7 +423,7 @@ defmodule JSON do
do: :elixir_json.encode_list(value, encoder)
def protocol_encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
do: :elixir_json.encode_map(value, encoder)
do: JSON.Encoder.Map.encode(value, encoder)
def protocol_encode(value, encoder),
do: JSON.Encoder.encode(value, encoder)
+58 -48
View File
@@ -3306,7 +3306,7 @@ defmodule Kernel do
end
defp nest_pop_in(:map, h, [{:access, key}]) do
quote do
quote generated: true do
case unquote(h) do
nil -> {nil, nil}
h -> Access.pop(h, unquote(key))
@@ -3327,7 +3327,7 @@ defmodule Kernel do
end
defp nest_pop_in(_, h, [{:access, key}]) do
quote do
quote generated: true do
case unquote(h) do
nil -> :pop
h -> Access.pop(h, unquote(key))
@@ -3716,26 +3716,25 @@ defmodule Kernel do
case function? do
true ->
value =
case Module.__get_attribute__(env.module, name, line, false) do
{_, doc} when doc_attr? -> doc
other -> other
end
case Module.__get_attribute__(env.module, name, line, false) do
{_, doc} when doc_attr? ->
do_at_escape(name, doc)
try do
:elixir_quote.escape(value, :none, false)
rescue
ex in [ArgumentError] ->
raise ArgumentError,
"cannot inject attribute @#{name} into function/macro because " <>
Exception.message(ex)
%{__struct__: Regex, source: source, opts: opts} = regex ->
case :erlang.system_info(:otp_release) < [?2, ?8] do
true -> do_at_escape(name, regex)
false -> quote(do: Regex.compile!(unquote(source), unquote(opts)))
end
value ->
do_at_escape(name, value)
end
false when doc_attr? ->
quote do
case Module.__get_attribute__(__MODULE__, unquote(name), unquote(line), false) do
{_, doc} -> doc
other -> other
value -> value
end
end
@@ -3770,6 +3769,17 @@ defmodule Kernel do
raise ArgumentError, "expected 0 or 1 argument for @#{name}, got: #{length(args)}"
end
defp do_at_escape(name, value) do
try do
:elixir_quote.escape(value, :none, false)
rescue
ex in [ArgumentError] ->
raise ArgumentError,
"cannot inject attribute @#{name} into function/macro because " <>
Exception.message(ex)
end
end
# Those are always compile-time dependencies, so we can skip the trace.
defp collect_traces(:before_compile, arg, _env), do: {arg, []}
defp collect_traces(:after_compile, arg, _env), do: {arg, []}
@@ -4066,7 +4076,7 @@ defmodule Kernel do
end
defp stepless_range(nil, first, last, _caller) do
quote(do: Elixir.Range.new(unquote(first), unquote(last)))
quote(do: Function.identity(Elixir.Range.new(unquote(first), unquote(last))))
end
defp stepless_range(:guard, first, last, caller) do
@@ -4584,6 +4594,9 @@ defmodule Kernel do
defp in_var(true, {atom, _, context} = var, fun) when is_atom(atom) and is_atom(context),
do: fun.(var)
defp in_var(true, var, fun) when is_atom(var) or is_binary(var) or is_number(var),
do: fun.(var)
defp in_var(true, ast, fun) do
quote do
var = unquote(ast)
@@ -5460,33 +5473,21 @@ defmodule Kernel do
use `@type`.
"""
defmacro defstruct(fields) do
header =
quote bind_quoted: [fields: fields, bootstrapped?: bootstrapped?(Enum)] do
{struct, derive, escaped_struct, kv, body} =
Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?, __ENV__)
quote bind_quoted: [fields: fields, bootstrapped?: bootstrapped?(Enum)] do
{struct, derive, escaped_struct, kv, body} =
Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?, __ENV__)
case derive do
[] -> :ok
_ -> Protocol.__derive__(derive, __MODULE__, __ENV__)
end
case derive do
[] -> :ok
_ -> Protocol.__derive__(derive, __MODULE__, __ENV__)
end
# We attach the line: 0 to struct functions because we don't want
# the generated callbacks to count towards code coverage and metrics,
# especially since they are often expanded at compile-time.
functions =
quote line: 0, unquote: false do
def __struct__(), do: unquote(escaped_struct)
def __struct__(unquote(kv)), do: unquote(body)
end
def __struct__(), do: unquote(escaped_struct)
def __struct__(unquote(kv)), do: unquote(body)
footer =
quote do
Kernel.Utils.announce_struct(__MODULE__)
struct
end
{:__block__, [], [header, functions, footer]}
Kernel.Utils.announce_struct(__MODULE__)
struct
end
end
@doc ~S"""
@@ -6446,29 +6447,38 @@ defmodule Kernel do
"""
defmacro sigil_r(term, modifiers)
defmacro sigil_r({:<<>>, _meta, [string]}, options) when is_binary(string) do
binary = :elixir_interpolation.unescape_string(string, &regex_unescape_map/1)
regex = Regex.compile!(binary, :binary.list_to_bin(options))
Macro.escape(regex)
defmacro sigil_r({:<<>>, _meta, [binary]}, options) when is_binary(binary) do
binary = :elixir_interpolation.unescape_string(binary, &regex_unescape_map/1)
compile_regex(binary, options)
end
defmacro sigil_r({:<<>>, meta, pieces}, options) do
binary = {:<<>>, meta, unescape_tokens(pieces, &regex_unescape_map/1)}
quote(do: Regex.compile!(unquote(binary), unquote(:binary.list_to_bin(options))))
tuple = {:<<>>, meta, unescape_tokens(pieces, &regex_unescape_map/1)}
compile_regex(tuple, options)
end
defp regex_unescape_map(:newline), do: true
defp regex_unescape_map(_), do: false
@doc false
defmacro sigil_R({:<<>>, _meta, [string]}, options) when is_binary(string) do
defmacro sigil_R({:<<>>, _meta, [binary]}, options) when is_binary(binary) do
IO.warn(
"~R/.../ is deprecated, use ~r/.../ instead",
Macro.Env.stacktrace(__CALLER__)
)
regex = Regex.compile!(string, :binary.list_to_bin(options))
Macro.escape(regex)
compile_regex(binary, options)
end
defp compile_regex(binary_or_tuple, options) do
# TODO: Remove this when we require Erlang/OTP 28+
case is_binary(binary_or_tuple) and :erlang.system_info(:otp_release) < [?2, ?8] do
true ->
Macro.escape(Regex.compile!(binary_or_tuple, :binary.list_to_bin(options)))
false ->
quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(:binary.list_to_bin(options))))
end
end
@doc ~S"""
+10
View File
@@ -295,6 +295,16 @@ defmodule Kernel.CLI do
parse_argv(t, %{config | commands: [{:parallel_require, h} | config.commands]})
end
defp parse_argv([~c"--color" | t], config) do
Application.put_env(:elixir, :ansi_enabled, true)
parse_argv(t, config)
end
defp parse_argv([~c"--no-color" | t], config) do
Application.put_env(:elixir, :ansi_enabled, false)
parse_argv(t, config)
end
## Compiler
defp parse_argv([~c"-o", h | t], %{mode: :elixirc} = config) do
+4 -24
View File
@@ -257,29 +257,7 @@ defmodule Kernel.ParallelCompiler do
{status, modules_or_errors, info} =
try do
outcome = spawn_workers(schedulers, cache, files, output, options)
{outcome, Keyword.get(options, :warnings_as_errors, false)}
else
{{:ok, _, %{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info}, true}
when r_warnings != [] or c_warnings != [] ->
message =
"Compilation failed due to warnings while using the --warnings-as-errors option"
IO.puts(:stderr, message)
errors = Enum.map(r_warnings ++ c_warnings, &Map.replace!(&1, :severity, :error))
{:error, errors, %{info | runtime_warnings: [], compile_warnings: []}}
{{:ok, outcome, info}, _} ->
beam_timestamp = Keyword.get(options, :beam_timestamp)
{:ok, write_module_binaries(outcome, output, beam_timestamp), info}
{{:error, errors, info}, true} ->
%{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info
info = %{info | runtime_warnings: [], compile_warnings: []}
{:error, c_warnings ++ r_warnings ++ errors, info}
{{:error, errors, info}, _} ->
{:error, errors, info}
spawn_workers(schedulers, cache, files, output, options)
after
Module.ParallelChecker.stop(cache)
end
@@ -303,6 +281,7 @@ defmodule Kernel.ParallelCompiler do
{outcome, state} =
spawn_workers(files, %{}, %{}, [], %{}, [], [], %{
beam_timestamp: Keyword.get(options, :beam_timestamp),
dest: Keyword.get(options, :dest),
each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, [], []} end),
each_file: Keyword.get(options, :each_file, fn _, _ -> :ok end) |> each_file(),
@@ -360,9 +339,10 @@ defmodule Kernel.ParallelCompiler do
## Verification
defp verify_modules(result, compile_warnings, dependent_modules, state) do
modules = write_module_binaries(result, state.output, state.beam_timestamp)
runtime_warnings = maybe_check_modules(result, dependent_modules, state)
info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings}
{{:ok, result, info}, state}
{{:ok, modules, info}, state}
end
defp maybe_check_modules(result, runtime_modules, state) do
+26 -12
View File
@@ -493,6 +493,24 @@ defmodule Kernel.Typespec do
state}
end
defp typespec(
{:<<>>, meta, [{:"::", _, [{:_, _, ctx1}, {:*, prod_meta, [size, unit]}]}]},
_,
_,
state
)
when is_atom(ctx1) and is_integer(size) and size >= 0 and unit in 1..256 do
location = location(meta)
prod_location = location(prod_meta)
{{:type, location, :binary,
[
{:op, prod_location, :*, {:integer, prod_location, size},
{:integer, prod_location, unit}},
{:integer, location, 0}
]}, state}
end
defp typespec({:<<>>, meta, [{:"::", size_meta, [{:_, _, ctx}, size]}]}, _, _, state)
when is_atom(ctx) and is_integer(size) and size >= 0 do
location = location(meta)
@@ -668,14 +686,7 @@ defmodule Kernel.Typespec do
when is_list(args) do
{args, state} = fn_args(meta, args, vars, caller, state)
{spec, state} = typespec(return, vars, caller, state)
fun_args =
case [args, spec] do
[{:type, _, :any}, {:type, _, :any, []}] -> []
pair -> pair
end
{{:type, location(meta), :fun, fun_args}, state}
{{:type, location(meta), :fun, [args, spec]}, state}
end
# Handle type operator
@@ -945,7 +956,7 @@ defmodule Kernel.Typespec do
## Helpers
# This is a backport of Macro.expand/2 because we want to expand
# This is a modified backport of Macro.expand/2 because we want to expand
# aliases but we don't them to become compile-time references.
defp expand_remote({:__aliases__, meta, list} = alias, env) do
case :elixir_aliases.expand_or_concat(meta, list, env, true) do
@@ -953,9 +964,12 @@ defmodule Kernel.Typespec do
receiver
[head | tail] ->
case Macro.expand_once(head, env) do
head when is_atom(head) -> :elixir_aliases.concat([head | tail])
_ -> alias
case Macro.expand(head, env) do
head when is_atom(head) ->
:elixir_aliases.concat([head | tail])
_ ->
compile_error(env, "unexpected expression in typespec: #{Macro.to_string(alias)}")
end
end
end
+3 -3
View File
@@ -174,14 +174,14 @@ defmodule Kernel.Utils do
true ->
case enforce_keys do
[] ->
quote do
quote line: 0, generated: true do
Enum.reduce(kv, unquote(escaped_struct), fn {key, val}, map ->
%{map | key => val}
end)
end
_ ->
quote do
quote line: 0, generated: true do
{map, keys} =
Enum.reduce(kv, {unquote(escaped_struct), unquote(enforce_keys)}, fn
{key, val}, {map, keys} ->
@@ -201,7 +201,7 @@ defmodule Kernel.Utils do
end
false ->
quote do
quote line: 0, generated: true do
:lists.foldl(
fn {key, val}, acc -> %{acc | key => val} end,
unquote(escaped_struct),
+9 -3
View File
@@ -146,8 +146,6 @@ defmodule Macro do
* `:delimiter` - contains the opening delimiter for sigils, strings,
and charlists as a string (such as `"{"`, `"/"`, `"'"`, and the like)
* `:format` - set to `:keyword` when an atom is defined as a keyword
* `:do` - contains metadata about the `do` location in a function call with
`do`-`end` blocks (when `:token_metadata` is true)
@@ -159,10 +157,18 @@ defmodule Macro do
expressions inside "blocks of code", which are either direct children
of a `__block__` or the right side of `->`. The last expression of the
block does not have metadata if it is not followed by an end of line
character (either a newline or `;`)
character (either a newline or `;`). This entry may appear multiple times
in the same metadata if the expression is surround by parens
* `:format` - set to `:keyword` when an atom is defined as a keyword.
It may also be set to `:atom` to distinguish `nil`, `false`, and `true`
* `:indentation` - indentation of a sigil heredoc
* `:parens` - denotes a node was surrounded by parens for grouping.
This entry may appear multiple times in the same metadata if
multiple pairs are used for grouping
The following metadata keys are private:
* `:alias` - Used for alias hygiene.
+5 -1
View File
@@ -946,7 +946,8 @@ defmodule Module do
@doc """
Concatenates two aliases and returns a new alias.
It handles binaries and atoms.
It handles binaries and atoms. If one of the aliases
is nil, it is discarded.
## Examples
@@ -956,6 +957,9 @@ defmodule Module do
iex> Module.concat(Foo, "Bar")
Foo.Bar
iex> Module.concat(Foo, nil)
Foo
"""
@spec concat(binary | atom, binary | atom) :: atom
def concat(left, right)
+15 -7
View File
@@ -239,7 +239,7 @@ defmodule Module.ParallelChecker do
|> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
|> Kernel.++(behaviour_warnings)
|> group_warnings()
|> emit_warnings(log?)
|> emit_warnings(file, log?)
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
apply(verify_mod, verify_fun, [module])
@@ -320,9 +320,9 @@ defmodule Module.ParallelChecker do
Enum.sort(ungrouped ++ grouped)
end
defp emit_warnings(warnings, log?) do
defp emit_warnings(warnings, file, log?) do
Enum.flat_map(warnings, fn {locations, diagnostic} ->
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, &1))
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1))
log? and print_diagnostics(diagnostics)
diagnostics
end)
@@ -336,10 +336,12 @@ defmodule Module.ParallelChecker do
:elixir_errors.print_diagnostics(diagnostics)
end
defp to_diagnostic(diagnostic, {file, position, mfa}) when is_list(position) do
defp to_diagnostic(diagnostic, source, {file, position, mfa}) when is_list(position) do
file = Path.absname(file)
%{
severity: :warning,
source: file,
source: source,
file: file,
position: position_to_tuple(position),
stacktrace: [to_stacktrace(file, position, mfa)],
@@ -437,10 +439,16 @@ defmodule Module.ParallelChecker do
defp cache_chunk(table, module, exports) do
Enum.each(exports, fn {{fun, arity}, info} ->
# TODO: Match on signature directly in Elixir v1.22+
sig =
case info do
%{sig: {:strong, _, _} = sig} -> sig
%{sig: {:infer, _} = sig} -> sig
_ -> :none
end
:ets.insert(
table,
{{module, {fun, arity}}, Map.get(info, :deprecated), Map.get(info, :sig, :none)}
{{module, {fun, arity}}, Map.get(info, :deprecated), sig}
)
end)
+5 -2
View File
@@ -182,9 +182,12 @@ defmodule Module.Types do
end
defp warn_unused_clauses(defs, stack, context) do
for {fun_arity, pending} <- context.local_used, pending != [], reduce: context do
for {fun_arity, pending} <- context.local_used,
pending != [],
{_fun_arity, kind, meta, clauses} = List.keyfind(defs, fun_arity, 0),
not Keyword.get(meta, :from_super, false),
reduce: context do
context ->
{_fun_arity, kind, _meta, clauses} = List.keyfind(defs, fun_arity, 0)
{_kind, _inferred, mapping} = Map.fetch!(context.local_sigs, fun_arity)
clauses_indexes =
+123 -45
View File
@@ -269,7 +269,7 @@ defmodule Module.Types.Apply do
case signature(name, arity) do
:none -> {dynamic(), context}
info -> apply_remote(info, args_types, expr, stack, context)
info -> apply_remote(nil, name, info, args_types, expr, stack, context)
end
end
@@ -287,10 +287,14 @@ defmodule Module.Types.Apply do
{value_type, context}
:badtuple ->
{error_type(), badremote_error(expr, [integer(), tuple], stack, context)}
{error_type(),
badremote_error(:erlang, :element, expr, [integer(), tuple], stack, context)}
reason ->
{error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)}
:badindex ->
mfac = mfac(expr, :erlang, :element, 2)
{error_type(),
error({:badindex, mfac, expr, tuple, index - 1, context}, meta, stack, context)}
end
end
@@ -308,10 +312,16 @@ defmodule Module.Types.Apply do
{value_type, context}
:badtuple ->
{error_type(), badremote_error(expr, [integer(), tuple, value], stack, context)}
args_types = [integer(), tuple, value]
reason ->
{error_type(), error({reason, expr, tuple, index - 2, context}, meta, stack, context)}
{error_type(),
badremote_error(:erlang, :insert_element, expr, args_types, stack, context)}
:badindex ->
mfac = mfac(expr, :erlang, :insert_element, 3)
{error_type(),
error({:badindex, mfac, expr, tuple, index - 2, context}, meta, stack, context)}
end
end
@@ -322,10 +332,16 @@ defmodule Module.Types.Apply do
{value_type, context}
:badtuple ->
{error_type(), badremote_error(expr, [integer(), tuple], stack, context)}
args_types = [integer(), tuple]
reason ->
{error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)}
{error_type(),
badremote_error(:erlang, :delete_element, expr, args_types, stack, context)}
:badindex ->
mfac = mfac(expr, :erlang, :delete_element, 2)
{error_type(),
error({:badindex, mfac, expr, tuple, index - 1, context}, meta, stack, context)}
end
end
@@ -340,7 +356,7 @@ defmodule Module.Types.Apply do
{value_type, context}
:badnonemptylist ->
{error_type(), badremote_error(expr, [list], stack, context)}
{error_type(), badremote_error(:erlang, :hd, expr, [list], stack, context)}
end
end
@@ -350,7 +366,7 @@ defmodule Module.Types.Apply do
{value_type, context}
:badnonemptylist ->
{error_type(), badremote_error(expr, [list], stack, context)}
{error_type(), badremote_error(:erlang, :tl, expr, [list], stack, context)}
end
end
@@ -363,14 +379,14 @@ defmodule Module.Types.Apply do
match?({false, _}, map_fetch(left, :__struct__)) or
match?({false, _}, map_fetch(right, :__struct__)) ->
warning = {:struct_comparison, expr, context}
warning = {:struct_comparison, expr, name, left, right, context}
warn(__MODULE__, warning, elem(expr, 1), stack, context)
number_type?(left) and number_type?(right) ->
context
disjoint?(left, right) ->
warning = {:mismatched_comparison, expr, context}
warning = {:mismatched_comparison, expr, name, left, right, context}
warn(__MODULE__, warning, elem(expr, 1), stack, context)
true ->
@@ -384,18 +400,26 @@ defmodule Module.Types.Apply do
end
end
def remote(:erlang, name, [left, right] = args_types, expr, stack, context)
def remote(
:erlang,
name,
[left, right] = args_types,
{_, _, args} = expr,
stack,
context
)
when name in [:==, :"/=", :"=:=", :"=/="] do
context =
cond do
stack.mode == :infer ->
# We ignore quoted literals as they most likely come from generated code.
stack.mode == :infer or Macro.quoted_literal?(args) ->
context
name in [:==, :"/="] and number_type?(left) and number_type?(right) ->
context
disjoint?(left, right) ->
warning = {:mismatched_comparison, expr, context}
warning = {:mismatched_comparison, expr, name, left, right, context}
warn(__MODULE__, warning, elem(expr, 1), stack, context)
true ->
@@ -405,30 +429,36 @@ defmodule Module.Types.Apply do
{return(boolean(), args_types, stack), context}
end
def remote(mod, name, args_types, expr, stack, context) do
def remote(mod, fun, args_types, expr, stack, context) do
arity = length(args_types)
case :elixir_rewrite.inline(mod, name, arity) do
{mod, name} ->
remote(mod, name, args_types, expr, stack, context)
case :elixir_rewrite.inline(mod, fun, arity) do
{new_mod, new_fun} ->
expr = inline_meta(expr, mod, fun)
remote(new_mod, new_fun, args_types, expr, stack, context)
false ->
{info, context} = signature(mod, name, arity, elem(expr, 1), stack, context)
apply_remote(info, args_types, expr, stack, context)
{info, context} = signature(mod, fun, arity, elem(expr, 1), stack, context)
apply_remote(mod, fun, info, args_types, expr, stack, context)
end
end
defp apply_remote(info, args_types, expr, stack, context) do
defp apply_remote(mod, fun, info, args_types, expr, stack, context) do
case apply_signature(info, args_types, stack) do
{:ok, _indexes, type} ->
{type, context}
{:error, domain, clauses} ->
error = {:badremote, expr, args_types, domain, clauses, context}
mfac = mfac(expr, mod, fun, length(args_types))
error = {:badremote, mfac, expr, args_types, domain, clauses, context}
{error_type(), error(error, elem(expr, 1), stack, context)}
end
end
defp inline_meta({node, meta, args}, mod, fun) do
{node, [inline: {mod, fun}] ++ meta, args}
end
@doc """
Returns the type of a remote capture.
"""
@@ -718,22 +748,28 @@ defmodule Module.Types.Apply do
error(__MODULE__, warning, meta, stack, context)
end
defp badremote_error({{:., _, [mod, fun]}, meta, _} = expr, args_types, stack, context) do
{_type, domain, [{args, _} | _] = clauses} = signature(mod, fun, length(args_types))
error({:badremote, expr, args_types, domain || args, clauses, context}, meta, stack, context)
defp badremote_error(mod, fun, {_, meta, _} = expr, args_types, stack, context) do
arity = length(args_types)
mfac = mfac(expr, mod, fun, arity)
{_type, domain, [{args, _} | _] = clauses} = signature(mod, fun, arity)
domain = domain || args
tuple = {:badremote, mfac, expr, args_types, domain, clauses, context}
error(tuple, meta, stack, context)
end
## Diagnosstics
## Diagnostics
def format_diagnostic({:badindex, expr, type, index, context}) do
def format_diagnostic({:badindex, mfac, expr, type, index, context}) do
traces = collect_traces(expr, context)
{mod, fun, arity, _converter} = mfac
mfa = Exception.format_mfa(mod, fun, arity)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a tuple with at least #{pluralize(index + 1, "element", "elements")} in #{format_mfa(expr)}:
expected a tuple with at least #{pluralize(index + 1, "element", "elements")} in #{mfa}:
#{expr_to_string(expr) |> indent(4)}
@@ -749,7 +785,7 @@ defmodule Module.Types.Apply do
def format_diagnostic({:badlocal, expr, args_types, domain, clauses, context}) do
traces = collect_traces(expr, context)
converter = &Function.identity/1
{fun, _, _} = expr
{fun, meta, _} = expr
explanation =
empty_arg_reason(args_types) ||
@@ -758,16 +794,29 @@ defmodule Module.Types.Apply do
#{clauses_args_to_quoted_string(clauses, converter)}
"""
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
banner =
case fun == :super && meta[:default] && meta[:super] do
{_kind, fun} ->
"""
incompatible types given as default arguments to #{fun}/#{length(args_types)}:
"""
_ ->
"""
incompatible types given to #{fun}/#{length(args_types)}:
#{expr_to_string(expr) |> indent(4)}
given types:
"""
end
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
banner,
"""
#{args_to_quoted_string(args_types, domain, converter) |> indent(4)}
@@ -778,10 +827,9 @@ defmodule Module.Types.Apply do
}
end
def format_diagnostic({:badremote, expr, args_types, domain, clauses, context}) do
def format_diagnostic({:badremote, mfac, expr, args_types, domain, clauses, context}) do
traces = collect_traces(expr, context)
{{:., _, [mod, fun]}, _, args} = expr
{mod, fun, args, converter} = :elixir_rewrite.erl_to_ex(mod, fun, args)
{mod, fun, arity, converter} = mfac
explanation =
empty_arg_reason(converter.(args_types)) ||
@@ -790,12 +838,15 @@ defmodule Module.Types.Apply do
#{clauses_args_to_quoted_string(clauses, converter)}
"""
mfa_or_fa =
if mod, do: Exception.format_mfa(mod, fun, arity), else: "#{fun}/#{arity}"
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types given to #{Exception.format_mfa(mod, fun, length(args))}:
incompatible types given to #{mfa_or_fa}:
#{expr_to_string(expr) |> indent(4)}
@@ -810,7 +861,7 @@ defmodule Module.Types.Apply do
}
end
def format_diagnostic({:mismatched_comparison, expr, context}) do
def format_diagnostic({:mismatched_comparison, expr, name, left, right, context}) do
traces = collect_traces(expr, context)
%{
@@ -821,6 +872,10 @@ defmodule Module.Types.Apply do
comparison between distinct types found:
#{expr_to_string(expr) |> indent(4)}
given types:
#{type_comparison_to_string(name, left, right) |> indent(4)}
""",
format_traces(traces),
"""
@@ -833,7 +888,7 @@ defmodule Module.Types.Apply do
}
end
def format_diagnostic({:struct_comparison, expr, context}) do
def format_diagnostic({:struct_comparison, expr, name, left, right, context}) do
traces = collect_traces(expr, context)
%{
@@ -844,6 +899,10 @@ defmodule Module.Types.Apply do
comparison with structs found:
#{expr_to_string(expr) |> indent(4)}
given types:
#{type_comparison_to_string(name, left, right) |> indent(4)}
""",
format_traces(traces),
"""
@@ -924,15 +983,34 @@ defmodule Module.Types.Apply do
defp pluralize(1, singular, _), do: "1 #{singular}"
defp pluralize(i, _, plural), do: "#{i} #{plural}"
defp format_mfa({{:., _, [mod, fun]}, _, args}) do
{mod, fun, args, _} = :elixir_rewrite.erl_to_ex(mod, fun, args)
Exception.format_mfa(mod, fun, length(args))
defp mfac({_, [inline: {mod, fun}] ++ _, _}, _mod, _fun, arity) do
{mod, fun, arity, & &1}
end
defp mfac({{:., _, [mod, fun]}, _, args}, _mod, _fun, _arity)
when is_atom(mod) and is_atom(fun) do
{mod, fun, args, converter} = :elixir_rewrite.erl_to_ex(mod, fun, args)
{mod, fun, length(args), converter}
end
defp mfac(_, mod, fun, arity)
when is_atom(mod) and is_atom(fun) and is_integer(arity) do
{mod, fun, arity, & &1}
end
## Algebra helpers
alias Inspect.Algebra, as: IA
defp type_comparison_to_string(fun, left, right) do
{Kernel, fun, [left, right], _} = :elixir_rewrite.erl_to_ex(:erlang, fun, [left, right])
{fun, [], [to_quoted(left, collapse_structs: true), to_quoted(right, collapse_structs: true)]}
|> Code.Formatter.to_algebra()
|> Inspect.Algebra.format(98)
|> IO.iodata_to_binary()
end
defp clauses_args_to_quoted_string([{args, _return}], converter) do
"\n " <> (clause_args_to_quoted_string(args, converter) |> indent(4))
end
+313 -79
View File
@@ -367,18 +367,30 @@ defmodule Module.Types.Descr do
@doc """
Converts a descr to its quoted representation.
## Options
* `:collapse_structs` - do not show struct fields that match
their default type
"""
def to_quoted(descr) do
def to_quoted(descr, opts \\ []) do
if term_type?(descr) do
{:term, [], []}
else
# Dynamic always come first for visibility
{dynamic, descr} =
case :maps.take(:dynamic, descr) do
:error -> {[], descr}
{dynamic, descr} -> {to_quoted(:dynamic, dynamic, opts), descr}
end
# Merge empty list and list together if they both exist
{extra, descr} =
case descr do
# Merge empty list and list together if they both exist
%{list: list, bitmap: bitmap} when (bitmap &&& @bit_empty_list) != 0 ->
descr = descr |> Map.delete(:list) |> Map.update!(:bitmap, &(&1 - @bit_empty_list))
descr = descr |> Map.delete(:list) |> Map.replace!(:bitmap, bitmap - @bit_empty_list)
case list_to_quoted(list, :list) do
case list_to_quoted(list, :list, opts) do
[] -> {[{:empty_list, [], []}], descr}
unions -> {unions, descr}
end
@@ -387,26 +399,32 @@ defmodule Module.Types.Descr do
{[], descr}
end
case extra ++ Enum.flat_map(descr, fn {key, value} -> to_quoted(key, value) end) do
unions =
dynamic ++
Enum.sort(
extra ++ Enum.flat_map(descr, fn {key, value} -> to_quoted(key, value, opts) end)
)
case unions do
[] -> {:none, [], []}
unions -> unions |> Enum.sort() |> Enum.reduce(&{:or, [], [&2, &1]})
unions -> Enum.reduce(unions, &{:or, [], [&2, &1]})
end
end
end
defp to_quoted(:atom, val), do: atom_to_quoted(val)
defp to_quoted(:bitmap, val), do: bitmap_to_quoted(val)
defp to_quoted(:dynamic, descr), do: dynamic_to_quoted(descr)
defp to_quoted(:map, dnf), do: map_to_quoted(dnf)
defp to_quoted(:list, dnf), do: list_to_quoted(dnf, :non_empty_list)
defp to_quoted(:tuple, dnf), do: tuple_to_quoted(dnf)
defp to_quoted(:atom, val, _opts), do: atom_to_quoted(val)
defp to_quoted(:bitmap, val, _opts), do: bitmap_to_quoted(val)
defp to_quoted(:dynamic, descr, opts), do: dynamic_to_quoted(descr, opts)
defp to_quoted(:map, dnf, opts), do: map_to_quoted(dnf, opts)
defp to_quoted(:list, dnf, opts), do: list_to_quoted(dnf, :non_empty_list, opts)
defp to_quoted(:tuple, dnf, opts), do: tuple_to_quoted(dnf, opts)
@doc """
Converts a descr to its quoted string representation.
"""
def to_quoted_string(descr) do
def to_quoted_string(descr, opts \\ []) do
descr
|> to_quoted()
|> to_quoted(opts)
|> Code.Formatter.to_algebra()
|> Inspect.Algebra.format(98)
|> IO.iodata_to_binary()
@@ -785,17 +803,16 @@ defmodule Module.Types.Descr do
defp atom_to_quoted({:union, a}) do
if :sets.is_subset(@boolset, a) do
:sets.subtract(a, @boolset)
|> :sets.to_list()
|> Enum.sort()
|> Enum.reduce({:boolean, [], []}, &{:or, [], [&2, literal_to_quoted(&1)]})
entries =
:sets.subtract(a, @boolset)
|> :sets.to_list()
|> Enum.map(&literal_to_quoted/1)
[{:boolean, [], []} | entries]
else
:sets.to_list(a)
|> Enum.sort()
|> Enum.map(&literal_to_quoted/1)
|> Enum.reduce(&{:or, [], [&2, &1]})
end
|> List.wrap()
end
defp atom_to_quoted({:negation, a}) do
@@ -1035,16 +1052,16 @@ defmodule Module.Types.Descr do
end
end
defp list_to_quoted(dnf, name) do
defp list_to_quoted(dnf, name, opts) do
dnf = list_normalize(dnf)
for {list_type, last_type, negs} <- dnf, reduce: [] do
acc ->
arguments =
if subtype?(last_type, @empty_list) do
[to_quoted(list_type)]
[to_quoted(list_type, opts)]
else
[to_quoted(list_type), to_quoted(last_type)]
[to_quoted(list_type, opts), to_quoted(last_type, opts)]
end
if negs == [] do
@@ -1054,9 +1071,9 @@ defmodule Module.Types.Descr do
|> Enum.map(fn {ty, lst} ->
args =
if subtype?(lst, @empty_list) do
[to_quoted(ty)]
[to_quoted(ty, opts)]
else
[to_quoted(ty), to_quoted(lst)]
[to_quoted(ty, opts), to_quoted(lst, opts)]
end
{name, [], args}
@@ -1064,11 +1081,7 @@ defmodule Module.Types.Descr do
|> Enum.reduce(&{:or, [], [&2, &1]})
|> Kernel.then(
&[
{:and, [],
[
{name, [], arguments},
{:not, [], [&1]}
]}
{:and, [], [{name, [], arguments}, {:not, [], [&1]}]}
| acc
]
)
@@ -1170,7 +1183,7 @@ defmodule Module.Types.Descr do
end
end
defp dynamic_to_quoted(descr) do
defp dynamic_to_quoted(descr, opts) do
cond do
term_type?(descr) ->
[{:dynamic, [], []}]
@@ -1179,7 +1192,7 @@ defmodule Module.Types.Descr do
[single]
true ->
case to_quoted(descr) do
case to_quoted(descr, opts) do
{:none, _meta, []} = none -> [none]
descr -> [{:dynamic, [], [descr]}]
end
@@ -1251,8 +1264,115 @@ defmodule Module.Types.Descr do
defp map_only?(descr), do: empty?(Map.delete(descr, :map))
# Union is list concatenation
defp map_union(dnf1, dnf2), do: dnf1 ++ (dnf2 -- dnf1)
defp map_union(dnf1, dnf2) do
# Union is just concatenation, but we rely on some optimization strategies to
# avoid the list to grow when possible
# first pass trying to identify patterns where two maps can be fused as one
with [map1] <- dnf1,
[map2] <- dnf2,
optimized when optimized != nil <- maybe_optimize_map_union(map1, map2) do
[optimized]
else
# otherwise we just concatenate and remove structural duplicates
_ -> dnf1 ++ (dnf2 -- dnf1)
end
end
defp maybe_optimize_map_union({tag1, pos1, []} = map1, {tag2, pos2, []} = map2) do
case map_union_optimization_strategy(tag1, pos1, tag2, pos2) do
:all_equal ->
map1
:any_map ->
{:open, %{}, []}
{:one_key_difference, key, v1, v2} ->
new_pos = Map.put(pos1, key, union(v1, v2))
{tag1, new_pos, []}
:left_subtype_of_right ->
map2
:right_subtype_of_left ->
map1
nil ->
nil
end
end
defp maybe_optimize_map_union(_, _), do: nil
defp map_union_optimization_strategy(tag1, pos1, tag2, pos2)
defp map_union_optimization_strategy(tag, pos, tag, pos), do: :all_equal
defp map_union_optimization_strategy(:open, empty, _, _) when empty == %{}, do: :any_map
defp map_union_optimization_strategy(_, _, :open, empty) when empty == %{}, do: :any_map
defp map_union_optimization_strategy(tag, pos1, tag, pos2)
when map_size(pos1) == map_size(pos2) do
:maps.iterator(pos1)
|> :maps.next()
|> do_map_union_optimization_strategy(pos2, :all_equal)
end
defp map_union_optimization_strategy(:open, pos1, _, pos2)
when map_size(pos1) <= map_size(pos2) do
:maps.iterator(pos1)
|> :maps.next()
|> do_map_union_optimization_strategy(pos2, :right_subtype_of_left)
end
defp map_union_optimization_strategy(_, pos1, :open, pos2)
when map_size(pos1) >= map_size(pos2) do
:maps.iterator(pos2)
|> :maps.next()
|> do_map_union_optimization_strategy(pos1, :right_subtype_of_left)
|> case do
:right_subtype_of_left -> :left_subtype_of_right
nil -> nil
end
end
defp map_union_optimization_strategy(_, _, _, _), do: nil
defp do_map_union_optimization_strategy(:none, _, status), do: status
defp do_map_union_optimization_strategy({key, v1, iterator}, pos2, status) do
with %{^key => v2} <- pos2,
next_status when next_status != nil <- map_union_next_strategy(key, v1, v2, status) do
do_map_union_optimization_strategy(:maps.next(iterator), pos2, next_status)
else
_ -> nil
end
end
defp map_union_next_strategy(key, v1, v2, status)
# structurally equal values do not impact the ongoing strategy
defp map_union_next_strategy(_key, same, same, status), do: status
defp map_union_next_strategy(key, v1, v2, :all_equal) do
if key != :__struct__, do: {:one_key_difference, key, v1, v2}
end
defp map_union_next_strategy(_key, v1, v2, {:one_key_difference, _, d1, d2}) do
# we have at least two key differences now, we switch strategy
# if both are subtypes in one direction, keep checking
cond do
subtype?(d1, d2) and subtype?(v1, v2) -> :left_subtype_of_right
subtype?(d2, d1) and subtype?(v2, v1) -> :right_subtype_of_left
true -> nil
end
end
defp map_union_next_strategy(_key, v1, v2, :left_subtype_of_right) do
if subtype?(v1, v2), do: :left_subtype_of_right
end
defp map_union_next_strategy(_key, v1, v2, :right_subtype_of_left) do
if subtype?(v2, v1), do: :right_subtype_of_left
end
# Given two unions of maps, intersects each pair of maps.
defp map_intersection(dnf1, dnf2) do
@@ -1262,7 +1382,16 @@ defmodule Module.Types.Descr do
acc ->
try do
{tag, fields} = map_literal_intersection(tag1, pos1, tag2, pos2)
[{tag, fields, negs1 ++ negs2} | acc]
entry = {tag, fields, negs1 ++ negs2}
# Imagine a, b, c, where a is closed and b and c are open with
# no keys in common. The result in both cases will be a and we
# want to avoid adding duplicates, especially as intersection
# is a cartesian product.
case :lists.member(entry, acc) do
true -> acc
false -> [entry | acc]
end
catch
:empty -> acc
end
@@ -1316,11 +1445,10 @@ defmodule Module.Types.Descr do
:maps.next(iterator) |> map_literal_intersection_loop(acc)
_ ->
# If the key is marked as not_set in the open map, we can ignore it.
if type1 == @not_set do
:maps.next(iterator) |> map_literal_intersection_loop(acc)
else
throw(:empty)
# If the key is optional in the open map, we can ignore it
case type1 do
%{optional: 1} -> :maps.next(iterator) |> map_literal_intersection_loop(acc)
_ -> throw(:empty)
end
end
end
@@ -1691,7 +1819,7 @@ defmodule Module.Types.Descr do
if map_empty_negation?(tag, acc_fields, neg) do
{acc_fields, acc_negs}
else
case all_but_one?(tag, acc_fields, neg_tag, neg_fields) do
case map_all_but_one?(tag, acc_fields, neg_tag, neg_fields) do
{:one, diff_key} ->
{Map.update!(acc_fields, diff_key, &difference(&1, neg_fields[diff_key])),
acc_negs}
@@ -1704,10 +1832,45 @@ defmodule Module.Types.Descr do
{tag, fields, negs}
end)
|> map_fusion()
end
# Given a dnf, fuse maps when possible
# e.g. %{a: integer(), b: atom()} or %{a: float(), b: atom()} into %{a: number(), b: atom()}
defp map_fusion(dnf) do
# Steps:
# 1. Group maps by tags and keys
# 2. Try fusions for each group until no fusion is found
# 3. Merge the groups back into a dnf
{without_negs, with_negs} = Enum.split_with(dnf, fn {_tag, _fields, negs} -> negs == [] end)
without_negs =
without_negs
|> Enum.group_by(fn {tag, fields, _} -> {tag, Map.keys(fields)} end)
|> Enum.flat_map(fn {_, maps} -> map_non_negated_fuse(maps) end)
without_negs ++ with_negs
end
defp map_non_negated_fuse(maps) do
Enum.reduce(maps, [], fn map, acc ->
fuse_with_first_fusible(map, acc)
end)
end
defp fuse_with_first_fusible(map, []), do: [map]
defp fuse_with_first_fusible(map, [candidate | rest]) do
if fused = maybe_optimize_map_union(map, candidate) do
# we found a fusible candidate, we're done
[fused | rest]
else
[candidate | fuse_with_first_fusible(map, rest)]
end
end
# If all fields are the same except one, we can optimize map difference.
defp all_but_one?(tag1, fields1, tag2, fields2) do
defp map_all_but_one?(tag1, fields1, tag2, fields2) do
keys1 = Map.keys(fields1)
keys2 = Map.keys(fields2)
@@ -1735,55 +1898,77 @@ defmodule Module.Types.Descr do
end))
end
defp map_to_quoted(dnf) do
defp map_to_quoted(dnf, opts) do
dnf
|> map_normalize()
|> Enum.map(&map_each_to_quoted/1)
|> case do
[] -> []
dnf -> Enum.reduce(dnf, &{:or, [], [&2, &1]}) |> List.wrap()
end
|> Enum.map(&map_each_to_quoted(&1, opts))
end
defp map_each_to_quoted({tag, positive_map, negative_maps}) do
defp map_each_to_quoted({tag, positive_map, negative_maps}, opts) do
case negative_maps do
[] ->
map_literal_to_quoted({tag, positive_map})
map_literal_to_quoted({tag, positive_map}, opts)
_ ->
negative_maps
|> Enum.map(&map_literal_to_quoted/1)
|> Enum.map(&map_literal_to_quoted(&1, opts))
|> Enum.reduce(&{:or, [], [&2, &1]})
|> Kernel.then(
&{:and, [], [map_literal_to_quoted({tag, positive_map}), {:not, [], [&1]}]}
&{:and, [], [map_literal_to_quoted({tag, positive_map}, opts), {:not, [], [&1]}]}
)
end
end
def map_literal_to_quoted({:closed, fields}) when map_size(fields) == 0 do
def map_literal_to_quoted({:closed, fields}, _opts) when map_size(fields) == 0 do
{:empty_map, [], []}
end
def map_literal_to_quoted({tag, fields}) do
def map_literal_to_quoted({tag, fields}, opts) do
case tag do
:closed ->
with %{__struct__: struct_descr} <- fields,
{_, [struct]} <- atom_fetch(struct_descr) do
fields = Map.delete(fields, :__struct__)
fields =
with true <- Keyword.get(opts, :collapse_structs, false),
[_ | _] = info <- maybe_struct(struct),
true <- Enum.all?(info, &is_map_key(fields, &1.field)) do
Enum.reduce(info, fields, fn %{field: field}, acc ->
# TODO: This should consider the struct default value
if Map.fetch!(acc, field) == term() do
Map.delete(acc, field)
else
acc
end
end)
else
_ -> fields
end
{:%, [],
[
literal_to_quoted(struct),
{:%{}, [], map_fields_to_quoted(tag, Map.delete(fields, :__struct__))}
{:%{}, [], map_fields_to_quoted(tag, fields, opts)}
]}
else
_ -> {:%{}, [], map_fields_to_quoted(tag, fields)}
_ -> {:%{}, [], map_fields_to_quoted(tag, fields, opts)}
end
:open ->
{:%{}, [], [{:..., [], nil} | map_fields_to_quoted(tag, fields)]}
{:%{}, [], [{:..., [], nil} | map_fields_to_quoted(tag, fields, opts)]}
end
end
defp map_fields_to_quoted(tag, map) do
defp maybe_struct(struct) do
try do
struct.__info__(:struct)
rescue
_ -> nil
end
end
defp map_fields_to_quoted(tag, map, opts) do
sorted = Enum.sort(Map.to_list(map))
keyword? = Inspect.List.keyword?(sorted)
@@ -1799,9 +1984,9 @@ defmodule Module.Types.Descr do
{optional?, type} = pop_optional_static(type)
cond do
not optional? -> {key, to_quoted(type)}
not optional? -> {key, to_quoted(type, opts)}
empty?(type) -> {key, {:not_set, [], []}}
true -> {key, {:if_set, [], [to_quoted(type)]}}
true -> {key, {:if_set, [], [to_quoted(type, opts)]}}
end
end
end
@@ -1852,8 +2037,16 @@ defmodule Module.Types.Descr do
reduce: [] do
acc ->
case tuple_literal_intersection(tag1, elements1, tag2, elements2) do
{tag, fields} -> [{tag, fields, negs1 ++ negs2} | acc]
:empty -> acc
{tag, elements} ->
entry = {tag, elements, negs1 ++ negs2}
case :lists.member(entry, acc) do
true -> acc
false -> [entry | acc]
end
:empty ->
acc
end
end
|> case do
@@ -1922,37 +2115,78 @@ defmodule Module.Types.Descr do
# This is a cheap optimization that relies on structural equality.
defp tuple_union(left, right), do: left ++ (right -- left)
defp tuple_to_quoted(dnf) do
defp tuple_to_quoted(dnf, opts) do
dnf
|> tuple_simplify()
|> Enum.map(&tuple_each_to_quoted/1)
|> case do
[] -> []
dnf -> Enum.reduce(dnf, &{:or, [], [&2, &1]}) |> List.wrap()
end
|> tuple_fusion()
|> Enum.map(&tuple_each_to_quoted(&1, opts))
end
defp tuple_each_to_quoted({tag, positive_map, negative_maps}) do
case negative_maps do
# Given a dnf of tuples, fuses the tuple unions when possible,
# e.g. {integer(), atom()} or {float(), atom()} into {number(), atom()}
# The negations of two fused tuples are just concatenated.
defp tuple_fusion(dnf) do
# Steps:
# 1. Consider tuples without negations apart from those with
# 2. Group tuples by size and tag
# 3. Try fusions for each group until no fusion is found
# 4. Merge the groups back into a dnf
{without_negs, with_negs} = Enum.split_with(dnf, fn {_tag, _elems, negs} -> negs == [] end)
without_negs =
without_negs
|> Enum.group_by(fn {tag, elems, _} -> {tag, length(elems)} end)
|> Enum.flat_map(fn {_, tuples} -> tuple_non_negated_fuse(tuples) end)
without_negs ++ with_negs
end
defp tuple_non_negated_fuse(tuples) do
Enum.reduce(tuples, [], fn tuple, acc ->
case Enum.split_while(acc, &non_fusible_tuples?(tuple, &1)) do
{_, []} ->
[tuple | acc]
{others, [match | rest]} ->
fused = tuple_non_negated_fuse_pair(tuple, match)
others ++ [fused | rest]
end
end)
end
# Two tuples are fusible if they have no negations and differ in at most one element.
defp non_fusible_tuples?({_, elems1, []}, {_, elems2, []}) do
Enum.zip(elems1, elems2) |> Enum.count_until(fn {a, b} -> a != b end, 2) > 1
end
defp tuple_non_negated_fuse_pair({tag, elems1, []}, {_, elems2, []}) do
fused_elements =
Enum.zip_with(elems1, elems2, fn a, b -> if a == b, do: a, else: union(a, b) end)
{tag, fused_elements, []}
end
defp tuple_each_to_quoted({tag, positive_tuple, negative_tuples}, opts) do
case negative_tuples do
[] ->
tuple_literal_to_quoted({tag, positive_map})
tuple_literal_to_quoted({tag, positive_tuple}, opts)
_ ->
negative_maps
|> Enum.map(&tuple_literal_to_quoted/1)
negative_tuples
|> Enum.map(&tuple_literal_to_quoted(&1, opts))
|> Enum.reduce(&{:or, [], [&2, &1]})
|> Kernel.then(
&{:and, [], [tuple_literal_to_quoted({tag, positive_map}), {:not, [], [&1]}]}
&{:and, [], [tuple_literal_to_quoted({tag, positive_tuple}, opts), {:not, [], [&1]}]}
)
end
end
defp tuple_literal_to_quoted({:closed, []}), do: {:{}, [], []}
defp tuple_literal_to_quoted({:closed, []}, _opts), do: {:{}, [], []}
defp tuple_literal_to_quoted({tag, elements}) do
defp tuple_literal_to_quoted({tag, elements}, opts) do
case tag do
:closed -> {:{}, [], Enum.map(elements, &to_quoted/1)}
:open -> {:{}, [], Enum.map(elements, &to_quoted/1) ++ [{:..., [], nil}]}
:closed -> {:{}, [], Enum.map(elements, &to_quoted(&1, opts))}
:open -> {:{}, [], Enum.map(elements, &to_quoted(&1, opts)) ++ [{:..., [], nil}]}
end
end
+6 -4
View File
@@ -263,8 +263,9 @@ defmodule Module.Types.Expr do
{case_type, context} = of_expr(case_expr, stack, context)
# If we are only type checking the expression and the expression is a literal,
# let's mark it as generated, as it is most likely a macro code.
if is_atom(case_expr) and {:type_check, :expr} in meta do
# let's mark it as generated, as it is most likely a macro code. However, if
# no clause is matched, we should still check for that.
if Macro.quoted_literal?(case_expr) do
for {:->, meta, args} <- clauses, do: {:->, [generated: true] ++ meta, args}
else
clauses
@@ -444,7 +445,8 @@ defmodule Module.Types.Expr do
# to avoid export dependencies. So we do it here.
if Code.ensure_loaded?(exception) and function_exported?(exception, :__struct__, 0) do
{info, context} = Of.struct_info(exception, meta, stack, context)
{Of.struct_type(exception, info, args), context}
# TODO: For properly defined structs, this should not be dynamic
{dynamic(Of.struct_type(exception, info, args)), context}
else
# If the exception cannot be found or is invalid, fetch the signature to emit warnings.
{_, context} = Apply.signature(exception, :__struct__, 0, meta, stack, context)
@@ -516,7 +518,7 @@ defmodule Module.Types.Expr do
defp with_clause({:<-, _meta, [left, right]} = expr, stack, context) do
{pattern, guards} = extract_head([left])
{_type, context} = Pattern.of_match(pattern, guards, dynamic(), :with, expr, stack, context)
{_type, context} = Pattern.of_match(pattern, guards, dynamic(), expr, :with, stack, context)
{_, context} = of_expr(right, stack, context)
context
end
+11 -7
View File
@@ -85,6 +85,9 @@ defmodule Module.Types.Helpers do
@doc """
Collect traces from variables in expression.
This information is exposed to language servers and
therefore must remain backwards compatible.
"""
def collect_traces(expr, %{vars: vars}) do
{_, versions} =
@@ -135,16 +138,17 @@ defmodule Module.Types.Helpers do
formatter -> formatter.(expr)
end
# This information is exposed to language servers and
# therefore must remain backwards compatible.
%{
file: file,
line: meta[:line],
column: meta[:column],
hints: formatter_hints ++ expr_hints(expr),
meta: meta,
formatted_expr: formatted_expr,
formatted_type: Module.Types.Descr.to_quoted_string(type)
formatted_hints: format_hints(formatter_hints ++ expr_hints(expr)),
formatted_type: Module.Types.Descr.to_quoted_string(type, collapse_structs: true)
}
end)
|> Enum.sort_by(&{&1.line, &1.column})
|> Enum.sort_by(&{&1.meta[:line], &1.meta[:column]})
|> Enum.dedup()
end
@@ -161,7 +165,7 @@ defmodule Module.Types.Helpers do
location =
trace.file
|> Path.relative_to_cwd()
|> Exception.format_file_line(trace.line, trace.column)
|> Exception.format_file_line(trace.meta[:line], trace.meta[:column])
|> String.replace_suffix(":", "")
[
@@ -173,7 +177,7 @@ defmodule Module.Types.Helpers do
""",
indent(trace.formatted_expr, 4),
?\n,
format_hints(trace.hints)
trace.formatted_hints
]
end
+1 -3
View File
@@ -495,13 +495,11 @@ defmodule Module.Types.Of do
unknown key .#{key} in expression:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
the given type does not have the given key:
#{to_quoted_string(type) |> indent(4)}
"""),
""",
format_traces(traces)
])
}
+4
View File
@@ -335,6 +335,10 @@ defmodule Module.Types.Pattern do
{type, context}
end
def of_match_var({:<<>>, _meta, args}, _expected, _expr, stack, context) do
{binary(), Of.binary(args, :match, stack, context)}
end
def of_match_var(ast, expected, expr, stack, context) do
of_match(ast, expected, expr, :default, stack, context)
end
+3 -3
View File
@@ -884,7 +884,7 @@ defmodule Path do
* A `..` component would make it so that the path would traverse up above
the root of `relative_to`.
* A symbolic link in the path points to something above the root of `cwd`.
* A symbolic link in the path points to something above the root of `relative_to`.
## Examples
@@ -906,10 +906,10 @@ defmodule Path do
"""
@doc since: "1.14.0"
@spec safe_relative(t, t) :: {:ok, binary} | :error
def safe_relative(path, cwd \\ File.cwd!()) do
def safe_relative(path, relative_to \\ File.cwd!()) do
path = IO.chardata_to_string(path)
case :filelib.safe_relative_path(path, cwd) do
case :filelib.safe_relative_path(path, relative_to) do
:unsafe -> :error
relative_path -> {:ok, IO.chardata_to_string(relative_path)}
end
+10
View File
@@ -219,6 +219,16 @@ defmodule Process do
Inlined by the compiler.
> #### Differences to `Kernel.exit/1` {: .info }
>
> The functions `Kernel.exit/1` and `Process.exit/2` are
> named similarly but provide very different functionalities. The
> `Kernel:exit/1` function should be used when the intent is to stop the current
> process while `Process.exit/2` should be used when the intent is to send an
> exit signal to another process. Note also that `Kernel.exit/1` can be caught
> with `try/1` while `Process.exit/2` can only be handled by trapping exits and
> when the signal is different than `:kill`.
## Examples
Process.exit(pid, :kill)
+25 -8
View File
@@ -347,7 +347,7 @@ defmodule Protocol do
end
defp assert_impl!(protocol, base, extra) do
impl = Module.concat(protocol, base)
impl = Protocol.__concat__(protocol, base)
try do
Code.ensure_compiled!(impl)
@@ -678,7 +678,7 @@ defmodule Protocol do
end
defp load_impl(protocol, for) do
Module.concat(protocol, for)
Protocol.__concat__(protocol, for)
end
# Finally compile the module and emit its bytecode.
@@ -831,7 +831,7 @@ defmodule Protocol do
# Define the implementation for built-ins
:lists.foreach(
fn {guard, mod} ->
target = Module.concat(__MODULE__, mod)
target = Protocol.__concat__(__MODULE__, mod)
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
case Code.ensure_compiled(unquote(target)) do
@@ -875,7 +875,7 @@ defmodule Protocol do
# Internal handler for Structs
Kernel.defp struct_impl_for(struct) do
case Code.ensure_compiled(Module.concat(__MODULE__, struct)) do
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
end
@@ -948,7 +948,7 @@ defmodule Protocol do
quote do
protocol = unquote(protocol)
for = unquote(for)
name = Module.concat(protocol, for)
name = Protocol.__concat__(protocol, for)
Protocol.assert_protocol!(protocol)
Protocol.__ensure_defimpl__(protocol, for, __ENV__)
@@ -994,7 +994,7 @@ defmodule Protocol do
else
# TODO: Deprecate this on Elixir v1.22+
assert_impl!(protocol, Any, extra)
{Module.concat(protocol, Any), [for, Macro.struct!(for, env), opts]}
{Protocol.__concat__(protocol, Any), [for, Macro.struct!(for, env), opts]}
end
# Clean up variables from eval context
@@ -1006,7 +1006,7 @@ defmodule Protocol do
else
__ensure_defimpl__(protocol, for, env)
assert_impl!(protocol, Any, extra)
impl = Module.concat(protocol, Any)
impl = Protocol.__concat__(protocol, Any)
funs =
for {fun, arity} <- protocol.__protocol__(:functions) do
@@ -1031,7 +1031,11 @@ defmodule Protocol do
def __impl__(:for), do: unquote(for)
end
Module.create(Module.concat(protocol, for), [quoted | funs], Macro.Env.location(env))
Module.create(
Protocol.__concat__(protocol, for),
[quoted | funs],
Macro.Env.location(env)
)
end
end)
end
@@ -1070,4 +1074,17 @@ defmodule Protocol do
is_reference: Reference
]
end
@doc false
def __concat__(left, right) do
String.to_atom(
ensure_prefix(Atom.to_string(left)) <> "." <> remove_prefix(Atom.to_string(right))
)
end
defp ensure_prefix("Elixir." <> _ = left), do: left
defp ensure_prefix(left), do: "Elixir." <> left
defp remove_prefix("Elixir." <> right), do: right
defp remove_prefix(right), do: right
end
+1 -1
View File
@@ -192,7 +192,7 @@ defmodule Range do
IO.warn_once(
{__MODULE__, :new},
fn ->
"Range.new/2 has a default step of -1, please call Range.new/3 explicitly passing the step of -1 instead"
"Range.new/2 and first..last default to a step of -1 when last < first. Use Range.new(first, last, -1) or first..last//-1, or pass 1 if that was your intention"
end,
3
)
+6 -9
View File
@@ -209,17 +209,14 @@ defmodule Regex do
## Examples
iex> Regex.compile("foo")
{:ok, ~r/foo/}
Regex.compile("foo")
#=> {:ok, ~r/foo/}
iex> Regex.compile("*foo")
{:error, {~c"nothing to repeat", 0}}
Regex.compile("foo", "i")
#=>{:ok, ~r/foo/i}
iex> Regex.compile("foo", "i")
{:ok, ~r/foo/i}
iex> Regex.compile("foo", [:caseless])
{:ok, Regex.compile!("foo", [:caseless])}
Regex.compile("*foo")
#=> {:error, {~c"nothing to repeat", 0}}
"""
@spec compile(binary, binary | [term]) :: {:ok, t} | {:error, term}
+1 -1
View File
@@ -1444,7 +1444,7 @@ defmodule Stream do
defp check_cycle_first_element(reduce) do
fn acc ->
case reduce.(acc) do
{state, []} when state in [:done, :halted] ->
{state, []} when state in [:done, :halted] and elem(acc, 0) != :halt ->
raise ArgumentError, "cannot cycle over an empty enumerable"
other ->
+9
View File
@@ -488,6 +488,15 @@ defmodule String do
iex> String.split(String.normalize("é", :nfc), "e")
["é"]
When using both the `:trim` and the `:parts` option, the empty values
are removed as the parts are computed (if any). No trimming happens
after all parts are computed:
iex> String.split(" a b c ", " ", trim: true, parts: 2)
["a", " b c "]
iex> String.split(" a b c ", " ", trim: true, parts: 3)
["a", "b", " c "]
"""
@spec split(t, pattern | Regex.t(), keyword) :: [t]
def split(string, pattern, options \\ [])
@@ -110,7 +110,7 @@ When using `IEx`, you may pass `--dbg pry` as an option to "stop" the code execu
$ iex --dbg pry
```
Or to debug inside a of a project:
Or to debug inside of a project:
```console
$ iex --dbg pry -S mix
@@ -11,36 +11,40 @@ Keyword lists are a data-structure used to pass options to functions. Let's see
Imagine you want to split a string of numbers. Initially, we can invoke `String.split/2` passing two strings as arguments:
```elixir
iex> String.split("1 2 3", " ")
["1", "2", "3"]
iex> String.split("1 2 3 4", " ")
["1", "2", "3", "4"]
```
However, what happens if there is an additional space between the numbers:
What if you only want to split at most 2 times? The `String.split/3` function allows the `parts` option to be set to the maximum number of entries in the result:
```elixir
iex> String.split("1 2 3", " ")
["1", "", "2", "", "3"]
iex> String.split("1 2 3 4", " ", [parts: 3])
["1", "2", "3 4"]
```
As you can see, there are now empty strings in our results. Luckily, the `String.split/3` function allows the `trim` option to be set to true:
As you can see, we got 3 parts, the last one containing the remaining of the input without splitting it.
Now imagine that some of the inputs you must split on contains additional spaces between the numbers:
```elixir
iex> String.split("1 2 3", " ", [trim: true])
["1", "2", "3"]
iex> String.split("1 2 3 4", " ", [parts: 3])
["1", "", "2 3 4"]
```
We can also use options to limit the splitting algorithm to a maximum number of parts, as shown next:
As you can see, the additional spaces lead to empty entries in the output. Luckily, we can also set the `trim` option to `true` to remove them:
```elixir
iex> String.split("1 2 3", " ", [trim: true, parts: 2])
["1", "2 3"]
iex> String.split("1 2 3 4", " ", [parts: 3, trim: true])
["1", "2", " 3 4"]
```
`[trim: true]` and `[trim: true, parts: 2]` are keyword lists. When a keyword list is the last argument of a function, we can skip the brackets and write:
Once again we got 3 parts, with the last one containing the leftovers.
`[parts: 3]` and `[parts: 3, trim: true]` are keyword lists. When a keyword list is the last argument of a function, we can skip the brackets and write:
```elixir
iex> String.split("1 2 3", " ", trim: true, parts: 2)
["1", "2 3"]
iex> String.split("1 2 3 4", " ", parts: 3, trim: true)
["1", "2", " 3 4"]
```
As shown in the example above, keyword lists are mostly used as optional arguments to functions.
@@ -48,7 +52,7 @@ As shown in the example above, keyword lists are mostly used as optional argumen
As the name implies, keyword lists are simply lists. In particular, they are lists consisting of 2-item tuples where the first element (the key) is an atom and the second element can be any value. Both representations are the same:
```elixir
iex> [{:trim, true}, {:parts, 2}] == [trim: true, parts: 2]
iex> [{:parts, 3}, {:trim, true}] == [parts: 3, trim: true]
true
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@@ -157,7 +157,7 @@ say me
say me
```
Yes, it works! However, does it *scale*?
Yes, it works! However, can it handle more than one client?
Try to connect two telnet clients at the same time. When you do so, you will notice that the second client doesn't echo:
@@ -303,4 +303,8 @@ Luckily, this can be done by using `Supervisor.child_spec/2`, which allows us to
Now we have an always running acceptor that starts temporary task processes under an always running task supervisor.
## Wrapping up
In this chapter, we implemented a basic TCP acceptor while exploring concurrency and fault-tolerance. Our acceptor can manage concurrent connections, but it is still not ready for production. Production-ready TCP servers run a pool of acceptors, each with their own supervisor. Elixir's `PartitionSupervisor` might be used to partition and scale the acceptor, but it is out of scope for this guide. In practice, you will use existing packages tailored for this use-case, such as [Ranch](https://github.com/ninenines/ranch) (in Erlang) or [Thousand Island](https://github.com/mtrudel/thousand_island) (in Elixir).
In the next chapter, we will start parsing the client requests and sending responses, finishing our server.
@@ -8,12 +8,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.18 | Development
1.17 | Bug fixes and security patches
1.18 | Bug fixes and security patches
1.17 | Security patches only
1.16 | Security patches only
1.15 | Security patches only
1.14 | Security patches only
1.13 | Security patches only
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). All security releases [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
@@ -43,6 +42,7 @@ Erlang/OTP versioning is independent from the versioning of Elixir. Erlang relea
Elixir version | Supported Erlang/OTP versions
:------------- | :-------------------------------
1.18 | 25 - 27 (and Erlang/OTP 28 from v1.18.4)
1.17 | 25 - 27
1.16 | 24 - 26
1.15 | 24 - 26
@@ -82,6 +82,14 @@ The first column is the version the feature was hard deprecated. The second colu
Version | Deprecated feature | Replaced by (available since)
:-------| :-------------------------------------------------- | :---------------------------------------------------------------
[v1.18] | `<%#` in EEx | `<%!--` (v1.14) or `<% #` (v1.0)
[v1.18] | `handle_text/2` callback in EEx | `handle_text/3` (v1.14)
[v1.18] | Returning 2-arity fun from `Enumerable.slice/1` | Returning 3-arity (v1.14)
[v1.18] | Ranges with negative steps in `Range.new/2` | Explicit steps in ranges (v1.11)
[v1.18] | `Tuple.append/2` | `Tuple.insert_at/3` (v1.0)
[v1.18] | `mix cmd --app APP` | `mix do --app APP` (v1.14)
[v1.18] | `List.zip/1` | `Enum.zip/1` (v1.0)
[v1.18] | `Module.eval_quoted/3` | `Code.eval_quoted/3` (v1.0)
[v1.17] | Single-quoted charlists (`'foo'`) | `~c"foo"` (v1.0)
[v1.17] | `left..right` in patterns and guards | `left..right//step` (v1.11)
[v1.17] | `ExUnit.Case.register_test/4` | `register_test/6` (v1.10)
@@ -214,4 +222,5 @@ Version | Deprecated feature | Replaced by (ava
[v1.14]: https://github.com/elixir-lang/elixir/blob/v1.14/CHANGELOG.md#4-hard-deprecations
[v1.15]: https://github.com/elixir-lang/elixir/blob/v1.15/CHANGELOG.md#4-hard-deprecations
[v1.16]: https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md#4-hard-deprecations
[v1.17]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations
[v1.17]: https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md#4-hard-deprecations
[v1.18]: https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md#4-hard-deprecations
+14 -2
View File
@@ -1,7 +1,12 @@
# Typespecs reference
Elixir comes with a notation for declaring types and specifications. This document is a
reference into their uses and syntax.
> #### Typespecs are not set-theoretic types {: .warning}
>
> Elixir is in the process of implementing its
> [own type system](./gradual-set-theoretic-types.md) based on set-theoretic types.
> Typespecs, which are described in the following document, are a distinct notation
> for declaring types and specifications based on Erlang.
> Typespecs may be phased out as the set-theoretic type effort moves forward.
Elixir is a dynamically typed language, and as such, type specifications are never used by the compiler to optimize or modify code. Still, using type specifications is useful because:
@@ -48,6 +53,13 @@ The syntax Elixir provides for type specifications is similar to [the one in Erl
The notation to represent the union of types is the pipe `|`. For example, the typespec `type :: atom() | pid() | tuple()` creates a type `type` that can be either an `atom`, a `pid`, or a `tuple`. This is usually called a [sum type](https://en.wikipedia.org/wiki/Tagged_union) in other languages
> #### Differences with set-theoretic types {: .warning}
>
> While they do share some similarities, the types below do not map one-to-one
> to the new types from the set theoretic type system.
> For example, there is no plan to support subsets of the `integer()` type such
> as positive, ranges or literals.
### Basic types
type ::
+13 -7
View File
@@ -191,12 +191,19 @@ canonical = System.fetch_env!("CANONICAL")
before_closing_body_tag: fn
:html ->
"""
<script defer src="https://cdn.jsdelivr.net/npm/mermaid@10.2.3/dist/mermaid.min.js"></script>
<script>
function mermaidLoaded() {
mermaid.initialize({
startOnLoad: false,
theme: document.body.className.includes("dark") ? "dark" : "default"
});
let initialized = false;
window.addEventListener("exdoc:loaded", () => {
if (!initialized) {
mermaid.initialize({
startOnLoad: false,
theme: document.body.className.includes("dark") ? "dark" : "default"
});
initialized = true;
}
let id = 0;
for (const codeEl of document.querySelectorAll("pre code.mermaid")) {
const preEl = codeEl.parentElement;
@@ -210,9 +217,8 @@ canonical = System.fetch_env!("CANONICAL")
preEl.remove();
});
}
}
});
</script>
<script async src="https://cdn.jsdelivr.net/npm/mermaid@10.2.3/dist/mermaid.min.js" onload="mermaidLoaded();"></script>
"""
_ ->
+15 -4
View File
@@ -11,7 +11,7 @@
parallel_match(Meta, Expr, S, #{context := match} = E) ->
#elixir_ex{vars={_Read, Write}} = S,
Matches = unpack_match(Expr, Meta, []),
Matches = unpack_match(Expr, Meta, [], E),
{[{_, EHead} | ETail], EWrites, SM, EM} =
lists:foldl(fun({EMeta, Match}, {AccMatches, AccWrites, SI, EI}) ->
@@ -31,9 +31,13 @@ parallel_match(Meta, Expr, S, #{context := match} = E) ->
VWrite = (Write /= false) andalso elixir_env:merge_vars(Write, PWrites),
{EMatch, SM#elixir_ex{vars={VRead, VWrite}, prematch={PRead, PCycles, PInfo}}, EM}.
unpack_match({'=', Meta, [Left, Right]}, _Meta, Acc) ->
unpack_match(Left, Meta, unpack_match(Right, Meta, Acc));
unpack_match(Node, Meta, Acc) ->
unpack_match({'=', Meta, [{_, VarMeta, _} = Node, Node]}, _Meta, Acc, E) ->
%% TODO: remove this clause on Elixir v1.23
file_warn(VarMeta, ?key(E, file), ?MODULE, {duplicate_match, Node}),
unpack_match(Node, Meta, Acc, E);
unpack_match({'=', Meta, [Left, Right]}, _Meta, Acc, E) ->
unpack_match(Left, Meta, unpack_match(Right, Meta, Acc, E), E);
unpack_match(Node, Meta, Acc, _E) ->
[{Meta, Node} | Acc].
store_cycles([Write | Writes], {Cycles, SkipList}, Acc) ->
@@ -532,6 +536,13 @@ origin(Meta, Default) ->
false -> Default
end.
format_error({duplicate_match, Expr}) ->
String = 'Elixir.Macro':to_string(Expr),
io_lib:format(
"this pattern is matched against itself inside a match: ~ts = ~ts",
[String, String]
);
format_error({recursive, Vars, TypeExpr}) ->
Code =
case TypeExpr of
+1
View File
@@ -203,6 +203,7 @@ bootstrap_files() ->
<<"path.ex">>,
<<"file.ex">>,
<<"map.ex">>,
<<"function.ex">>,
<<"range.ex">>,
<<"access.ex">>,
<<"io.ex">>,
+7 -4
View File
@@ -222,7 +222,7 @@ store_definition(Meta, Kind, CheckClauses, Name, Arity, DefaultsArgs, Guards, Bo
[store_definition(none, Kind, Meta, Name, length(DefaultArgs), File,
Module, 0, [Default]) || {_, DefaultArgs, _, _} = Default <- Defaults],
run_on_definition_callbacks(Kind, Module, Name, DefaultsArgs, Guards, Body, E),
run_on_definition_callbacks(Meta, Kind, Module, Name, DefaultsArgs, Guards, Body, E),
Tuple.
env_for_expansion(Kind, Tuple, E) when Kind =:= defmacro; Kind =:= defmacrop ->
@@ -267,10 +267,13 @@ def_to_clauses(Kind, Meta, Args, Guards, Body, E) ->
elixir_errors:file_error(Meta, E, elixir_expand, {missing_option, Kind, [do]})
end.
run_on_definition_callbacks(Kind, Module, Name, Args, Guards, Body, E) ->
run_on_definition_callbacks(Meta, Kind, Module, Name, Args, Guards, Body, E) ->
{_, Bag} = elixir_module:data_tables(Module),
Callbacks = ets:lookup_element(Bag, {accumulate, on_definition}, 2),
_ = [Mod:Fun(E, Kind, Name, Args, Guards, Body) || {Mod, Fun} <- lists:reverse(Callbacks)],
_ = [begin
elixir_env:trace({remote_function, Meta, Mod, Fun, 6}, E),
Mod:Fun(E, Kind, Name, Args, Guards, Body)
end || {Mod, Fun} <- lists:reverse(Callbacks)],
ok.
store_definition(CheckClauses, Kind, Meta, Name, Arity, File, Module, Defaults, Clauses)
@@ -314,7 +317,7 @@ unpack_defaults(Kind, Meta, Name, Args, S, E) ->
unpack_expanded(Kind, Meta, Name, [{'\\\\', DefaultMeta, [Expr, _]} | T] = List, VersionOffset, Acc, Clauses) ->
Base = match_defaults(Acc, length(Acc) + VersionOffset, []),
{Args, Invoke} = extract_defaults(List, length(Base) + VersionOffset, [], []),
Clause = {Meta, Base ++ Args, [], {super, [{super, {Kind, Name}} | DefaultMeta], Base ++ Invoke}},
Clause = {Meta, Base ++ Args, [], {super, [{super, {Kind, Name}}, {default, true} | DefaultMeta], Base ++ Invoke}},
unpack_expanded(Kind, Meta, Name, T, VersionOffset, [Expr | Acc], [Clause | Clauses]);
unpack_expanded(Kind, Meta, Name, [H | T], VersionOffset, Acc, Clauses) ->
unpack_expanded(Kind, Meta, Name, T, VersionOffset, [H | Acc], Clauses);
+3 -1
View File
@@ -204,7 +204,9 @@ do_expand_import(Result, Meta, Name, Arity, Module, E, Trace) ->
{import, Receiver} ->
case expand_require(true, Meta, Receiver, Name, Arity, E, Trace) of
{macro, _, _} = Response -> Response;
error -> {function, Receiver, Name}
error ->
Trace andalso elixir_env:trace({remote_function, Meta, Receiver, Name, Arity}, E),
{function, Receiver, Name}
end;
false when Module == ?kernel ->
case elixir_rewrite:inline(Module, Name, Arity) of
+17 -2
View File
@@ -92,6 +92,9 @@ elixir_to_erl(Tree, Ann) when is_binary(Tree) ->
%% considers a string in a binary to be encoded in latin1, so the bytes
%% are not changed in any fashion.
{bin, Ann, [{bin_element, Ann, {string, Ann, binary_to_list(Tree)}, default, default}]};
elixir_to_erl(Tree, Ann) when is_bitstring(Tree) ->
Segments = [elixir_to_erl_bitstring_segment(X, Ann) || X <- bitstring_to_list(Tree)],
{bin, Ann, Segments};
elixir_to_erl(Tree, Ann) when is_function(Tree) ->
case (erlang:fun_info(Tree, type) == {type, external}) andalso
(erlang:fun_info(Tree, env) == {env, []}) of
@@ -111,6 +114,13 @@ elixir_to_erl(Tree, Ann) ->
elixir_to_erl_cons([H | T], Ann) -> {cons, Ann, elixir_to_erl(H, Ann), elixir_to_erl_cons(T, Ann)};
elixir_to_erl_cons(T, Ann) -> elixir_to_erl(T, Ann).
elixir_to_erl_bitstring_segment(Int, Ann) when is_integer(Int) ->
{bin_element, Ann, {integer, Ann, Int}, default, [integer]};
elixir_to_erl_bitstring_segment(Rest, Ann) when is_bitstring(Rest) ->
Size = bit_size(Rest),
<<Int:Size>> = Rest,
{bin_element, Ann, {integer, Ann, Int}, {integer, Ann, Size}, [integer]}.
%% Returns a scope for translation.
scope(_Meta, ExpandCaptures) ->
@@ -165,7 +175,12 @@ dynamic_form(#{module := Module, relative_file := RelativeFile,
{Def, Defmacro, Macros, Exports, Functions} =
split_definition(Definitions, Unreachable, Line, [], [], [], [], {[], []}),
FilteredOpts = lists:filter(fun({no_warn_undefined, _}) -> false; (_) -> true end, Opts),
FilteredOpts = lists:filter(fun(
{no_warn_undefined, _}) -> false;
(debug_info) -> false;
(_) -> true
end, Opts),
Location = {elixir_utils:characters_to_list(RelativeFile), Line},
Prefix = [{attribute, Line, file, Location},
@@ -526,7 +541,7 @@ docs_chunk(Map, Set, Module, Anno, Def, Defmacro, Types, Callbacks, ChunkOpts) -
TypeDocs = get_type_docs(Set, Types),
ModuleMeta = ModuleDocMeta#{
source_path => File,
source_path => elixir_utils:characters_to_list(File),
source_annos => [Anno],
behaviours => [Mod || {behaviour, Mod} <- Attributes]
},
+9 -1
View File
@@ -20,7 +20,15 @@ reduce_clauses([], Acc, OldStack, SAcc, _S) ->
{lists:reverse(Acc), SAcc#elixir_erl{stacktrace=OldStack}}.
each_clause({'catch', Meta, Raw, Expr}, S) ->
{Match, Guards} = elixir_utils:extract_splat_guards(Raw),
{Args, Guards} = elixir_utils:extract_splat_guards(Raw),
Match =
%% Elixir v1.17 and earlier emitted single argument
%% and may still be processed via debug_info
case Args of
[X] -> [throw, X];
[X, Y] -> [X, Y]
end,
{{clause, Line, [TKind, TMatches], TGuards, TBody}, TS} =
elixir_erl_clauses:clause(?ann(Meta), fun elixir_erl_pass:translate_args/3, Match, Expr, Guards, S),
+8 -5
View File
@@ -92,7 +92,10 @@ emit_diagnostic(Severity, Position, File, Message, Stacktrace, Options) ->
Diagnostic = #{
severity => Severity,
source => File,
source => case get(elixir_compiler_file) of
undefined -> File;
CompilerFile -> CompilerFile
end,
file => File,
position => Position,
message => unicode:characters_to_binary(Message),
@@ -216,12 +219,12 @@ highlight_at_position(Column, Severity, Length) ->
highlight_below_line(Line, Severity) ->
% Don't highlight leading whitespaces in line
{_, SpacesMatched} = trim_line(Line, 0),
{Rest, SpacesMatched} = trim_line(Line, 0),
Length = string:length(Line),
Length = string:length(Rest),
Highlight = case Severity of
warning -> highlight(lists:duplicate(Length - SpacesMatched, $~), warning);
error -> highlight(lists:duplicate(Length - SpacesMatched, $^), error)
warning -> highlight(lists:duplicate(Length, $~), warning);
error -> highlight(lists:duplicate(Length, $^), error)
end,
[n_spaces(SpacesMatched), Highlight].
+5 -4
View File
@@ -293,7 +293,7 @@ expand({'&', Meta, [{'/', ArityMeta, [{super, SuperMeta, Context}, Arity]} = Exp
case resolve_super(Meta, Arity, E) of
{Kind, Name, _} when Kind == def; Kind == defp ->
{{'&', Meta, [{'/', ArityMeta, [{Name, SuperMeta, Context}, Arity]}]}, S, E};
expand({'&', Meta, [{'/', ArityMeta, [{Name, SuperMeta, Context}, Arity]}]}, S, E);
_ ->
expand_fn_capture(Meta, Expr, S, E)
end;
@@ -343,7 +343,9 @@ expand({with, Meta, [_ | _] = Args}, S, E) ->
expand({super, Meta, Args}, S, E) when is_list(Args) ->
assert_no_match_or_guard_scope(Meta, "super", S, E),
{Kind, Name, _} = resolve_super(Meta, length(Args), E),
Arity = length(Args),
{Kind, Name, _} = resolve_super(Meta, Arity, E),
elixir_env:trace({local_function, Meta, Name, Arity}, E),
{EArgs, SA, EA} = expand_args(Args, S, E),
{{super, [{super, {Kind, Name}} | Meta], EArgs}, SA, EA};
@@ -899,8 +901,7 @@ expand_local(Meta, Name, Args, S, #{function := Function, context := Context} =
module_error(Meta, E, ?MODULE, {invalid_local_invocation, elixir_utils:guard_info(S), {Name, Meta, Args}});
nil ->
Arity = length(Args),
elixir_env:trace({local_function, Meta, Name, Arity}, E)
elixir_env:trace({local_function, Meta, Name, length(Args)}, E)
end,
{EArgs, SA, EA} = expand_args(Args, S, E),
+4
View File
@@ -112,6 +112,10 @@ capture_expr(Meta, Expr, S, E, Escaped, Sequential) ->
case escape(Expr, E, Escaped) of
{_, []} when not Sequential ->
invalid_capture(Meta, Expr, E);
{{{'.', _, [_, _]} = Dot, _, Args}, []} ->
Meta2 = lists:keydelete(no_parens, 1, Meta),
Fn = {fn, Meta2, [{'->', Meta2, [[], {Dot, Meta2, Args}]}]},
{expand, Fn, S, E};
{EExpr, EDict} ->
EVars = validate(Meta, EDict, 1, E),
Fn = {fn, Meta, [{'->', Meta, [EVars, EExpr]}]},
+5 -3
View File
@@ -155,6 +155,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
put_compiler_modules([Module | CompilerModules]),
{Result, ModuleE, CallbackE} = eval_form(Line, Module, DataBag, Block, Vars, Prune, E),
CheckerInfo = checker_info(),
BeamLocation = beam_location(ModuleAsCharlist),
{Binary, PersistedAttributes, Autoload} =
elixir_erl_compiler:spawn(fun() ->
@@ -177,7 +178,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
'Elixir.Module':'__check_attributes__'(E, DataSet, DataBag),
AfterVerify = bag_lookup_element(DataBag, {accumulate, after_verify}, 2),
[elixir_env:trace({remote_function, [], VerifyMod, VerifyFun, 1}, CallbackE) ||
[elixir_env:trace({remote_function, [{line, Line}], VerifyMod, VerifyFun, 1}, CallbackE) ||
{VerifyMod, VerifyFun} <- AfterVerify],
%% Ensure there are no errors before we infer types
@@ -219,7 +220,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
{Binary, PersistedAttributes, Autoload}
end),
Autoload andalso code:load_binary(Module, beam_location(ModuleAsCharlist), Binary),
Autoload andalso code:load_binary(Module, BeamLocation, Binary),
put_compiler_modules(CompilerModules),
eval_callbacks(Line, DataBag, after_compile, [CallbackE, Binary], CallbackE),
elixir_env:trace({on_module, Binary, none}, ModuleE),
@@ -294,7 +295,8 @@ validate_on_load_attribute({on_load, Def}, Defs, Bag, Line, E) ->
elixir_errors:module_error([{line, Line}], E, ?MODULE, {undefined_function, on_load, Def});
{_Def, Kind, _Meta, _Clauses} when Kind == defmacro; Kind == defmacrop ->
elixir_errors:module_error([{line, Line}], E, ?MODULE, {bad_macro, on_load, Def});
{_Def, Kind, _Meta, _Clauses} ->
{{Name, Arity}, Kind, _Meta, _Clauses} ->
elixir_env:trace({local_function, [{line, Line}], Name, Arity}, E),
(Kind == defp) andalso ets:insert(Bag, {used_private, Def})
end;
validate_on_load_attribute(false, _Defs, _Bag, _Line, _E) -> ok.
+2 -1
View File
@@ -75,8 +75,9 @@ store_not_overridden(Module) ->
%% Private
store(Set, Module, Tuple, {_, Count, Def, Overridden}, Hidden) ->
{{{def, {Name, Arity}}, Kind, Meta, File, _Check,
{{{def, {Name, Arity}}, Kind, BaseMeta, File, _Check,
{Defaults, _HasBody, _LastDefaults}}, Clauses} = Def,
Meta = [{from_super, Hidden} | BaseMeta],
{FinalKind, FinalName, FinalArity, FinalClauses} =
case Hidden of
+16 -4
View File
@@ -160,10 +160,19 @@ do_escape(BitString, _) when is_bitstring(BitString) ->
end;
do_escape(Map, Q) when is_map(Map) ->
TT = do_quote(lists:sort(maps:to_list(Map)), Q),
TT =
[if
is_reference(V) ->
argument_error(<<('Elixir.Kernel':inspect(Map, []))/binary, " contains a reference (",
('Elixir.Kernel':inspect(V, []))/binary, ") and therefore it cannot be escaped ",
"(it must be defined within a function instead). ", (bad_escape_hint())/binary>>);
true ->
{do_quote(K, Q), do_quote(V, Q)}
end || {K, V} <- lists:sort(maps:to_list(Map))],
{'%{}', [], TT};
do_escape([], _) -> [];
do_escape([], _) ->
[];
do_escape([H | T], #elixir_quote{unquote=false} = Q) ->
do_quote_simple_list(T, do_quote(H, Q), Q);
@@ -195,8 +204,11 @@ do_escape(Other, _) ->
bad_escape(Arg) ->
argument_error(<<"cannot escape ", ('Elixir.Kernel':inspect(Arg, []))/binary, ". ",
"The supported values are: lists, tuples, maps, atoms, numbers, bitstrings, ",
"PIDs and remote functions in the format &Mod.fun/arity">>).
(bad_escape_hint())/binary>>).
bad_escape_hint() ->
<<"The supported values are: lists, tuples, maps, atoms, numbers, bitstrings, ",
"PIDs and remote functions in the format &Mod.fun/arity">>.
%% Quote entry points
+78 -68
View File
@@ -266,11 +266,7 @@ tokenize([$" | T], Line, Column, Scope, Tokens) ->
%% TODO: Remove me in Elixir v2.0
tokenize([$' | T], Line, Column, Scope, Tokens) ->
Message = "single-quoted strings represent charlists. "
"Use ~c\"\" if you indeed want a charlist or use \"\" instead.\n"
"You may run \"mix format --migrate\" to fix this warning automatically.",
NewScope = prepend_warning(Line, Column, Message, Scope),
handle_strings(T, Line, Column + 1, $', NewScope, Tokens);
handle_strings(T, Line, Column + 1, $', Scope, Tokens);
% Operator atoms
@@ -786,7 +782,11 @@ handle_strings(T, Line, Column, H, Scope, Tokens) ->
"number do not require quotes",
[hd(Parts)]
),
prepend_warning(Line, Column, WarnMsg, InterScope);
prepend_warning(Line, Column-1, WarnMsg, InterScope);
false when H =:= $' ->
WarnMsg = "single quotes around keywords are deprecated. Use double quotes instead",
prepend_warning(Line, Column-1, WarnMsg, InterScope);
false ->
InterScope
@@ -814,7 +814,20 @@ handle_strings(T, Line, Column, H, Scope, Tokens) ->
error(Reason, Rest, NewScope, Tokens)
end;
{NewLine, NewColumn, Parts, Rest, NewScope} ->
{NewLine, NewColumn, Parts, Rest, InterScope} ->
NewScope =
case H of
$' ->
Message = "using single-quoted strings to represent charlists is deprecated.\n"
"Use ~c\"\" if you indeed want a charlist or use \"\" instead.\n"
"You may run \"mix format --migrate\" to change all single-quoted\n"
"strings to use the ~c sigil and fix this warning.",
prepend_warning(Line, Column-1, Message, InterScope);
_ ->
InterScope
end,
case unescape_tokens(Parts, Line, Column, NewScope) of
{ok, Unescaped} ->
Token = {string_type(H), {Line, Column - 1, nil}, Unescaped},
@@ -1750,84 +1763,81 @@ add_cursor(_Line, Column, noprune, Terminators, Tokens) ->
{Column, Terminators, Tokens};
add_cursor(Line, Column, prune_and_cursor, Terminators, Tokens) ->
PrePrunedTokens = prune_identifier(Tokens),
{PrunedTokens, PrunedTerminators} = prune_tokens(PrePrunedTokens, [], Terminators),
PrunedTokens = prune_tokens(PrePrunedTokens, []),
CursorTokens = [
{')', {Line, Column + 11, nil}},
{'(', {Line, Column + 10, nil}},
{paren_identifier, {Line, Column, nil}, '__cursor__'}
| PrunedTokens
],
{Column + 12, PrunedTerminators, CursorTokens}.
{Column + 12, Terminators, CursorTokens}.
prune_identifier([{identifier, _, _} | Tokens]) -> Tokens;
prune_identifier(Tokens) -> Tokens.
%%% Any terminator needs to be closed
prune_tokens([{'end', _} | Tokens], Opener, Terminators) ->
prune_tokens(Tokens, ['end' | Opener], Terminators);
prune_tokens([{')', _} | Tokens], Opener, Terminators) ->
prune_tokens(Tokens, [')' | Opener], Terminators);
prune_tokens([{']', _} | Tokens], Opener, Terminators) ->
prune_tokens(Tokens, [']' | Opener], Terminators);
prune_tokens([{'}', _} | Tokens], Opener, Terminators) ->
prune_tokens(Tokens, ['}' | Opener], Terminators);
prune_tokens([{'>>', _} | Tokens], Opener, Terminators) ->
prune_tokens(Tokens, ['>>' | Opener], Terminators);
prune_tokens([{'end', _} | Tokens], Opener) ->
prune_tokens(Tokens, ['end' | Opener]);
prune_tokens([{')', _} | Tokens], Opener) ->
prune_tokens(Tokens, [')' | Opener]);
prune_tokens([{']', _} | Tokens], Opener) ->
prune_tokens(Tokens, [']' | Opener]);
prune_tokens([{'}', _} | Tokens], Opener) ->
prune_tokens(Tokens, ['}' | Opener]);
prune_tokens([{'>>', _} | Tokens], Opener) ->
prune_tokens(Tokens, ['>>' | Opener]);
%%% Close opened terminators
prune_tokens([{'fn', _} | Tokens], ['end' | Opener], Terminators) ->
prune_tokens(Tokens, Opener, Terminators);
prune_tokens([{'do', _} | Tokens], ['end' | Opener], Terminators) ->
prune_tokens(Tokens, Opener, Terminators);
prune_tokens([{'(', _} | Tokens], [')' | Opener], Terminators) ->
prune_tokens(Tokens, Opener, Terminators);
prune_tokens([{'[', _} | Tokens], [']' | Opener], Terminators) ->
prune_tokens(Tokens, Opener, Terminators);
prune_tokens([{'{', _} | Tokens], ['}' | Opener], Terminators) ->
prune_tokens(Tokens, Opener, Terminators);
prune_tokens([{'<<', _} | Tokens], ['>>' | Opener], Terminators) ->
prune_tokens(Tokens, Opener, Terminators);
%%% Handle anonymous functions
prune_tokens([{'(', _}, {capture_op, _, _} | Tokens], [], [{'(', _, _} | Terminators]) ->
prune_tokens(Tokens, [], Terminators);
prune_tokens([{'fn', _} | Tokens], ['end' | Opener]) ->
prune_tokens(Tokens, Opener);
prune_tokens([{'do', _} | Tokens], ['end' | Opener]) ->
prune_tokens(Tokens, Opener);
prune_tokens([{'(', _} | Tokens], [')' | Opener]) ->
prune_tokens(Tokens, Opener);
prune_tokens([{'[', _} | Tokens], [']' | Opener]) ->
prune_tokens(Tokens, Opener);
prune_tokens([{'{', _} | Tokens], ['}' | Opener]) ->
prune_tokens(Tokens, Opener);
prune_tokens([{'<<', _} | Tokens], ['>>' | Opener]) ->
prune_tokens(Tokens, Opener);
%%% or it is time to stop...
prune_tokens([{';', _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
prune_tokens([{'eol', _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
prune_tokens([{',', _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
prune_tokens([{'fn', _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
prune_tokens([{'do', _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
prune_tokens([{'(', _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
prune_tokens([{'[', _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
prune_tokens([{'{', _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
prune_tokens([{'<<', _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
prune_tokens([{identifier, _, _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
prune_tokens([{block_identifier, _, _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
prune_tokens([{kw_identifier, _, _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
prune_tokens([{kw_identifier_safe, _, _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
prune_tokens([{kw_identifier_unsafe, _, _} | _] = Tokens, [], Terminators) ->
{Tokens, Terminators};
prune_tokens([{OpType, _, _} | _] = Tokens, [], Terminators)
prune_tokens([{';', _} | _] = Tokens, []) ->
Tokens;
prune_tokens([{'eol', _} | _] = Tokens, []) ->
Tokens;
prune_tokens([{',', _} | _] = Tokens, []) ->
Tokens;
prune_tokens([{'fn', _} | _] = Tokens, []) ->
Tokens;
prune_tokens([{'do', _} | _] = Tokens, []) ->
Tokens;
prune_tokens([{'(', _} | _] = Tokens, []) ->
Tokens;
prune_tokens([{'[', _} | _] = Tokens, []) ->
Tokens;
prune_tokens([{'{', _} | _] = Tokens, []) ->
Tokens;
prune_tokens([{'<<', _} | _] = Tokens, []) ->
Tokens;
prune_tokens([{identifier, _, _} | _] = Tokens, []) ->
Tokens;
prune_tokens([{block_identifier, _, _} | _] = Tokens, []) ->
Tokens;
prune_tokens([{kw_identifier, _, _} | _] = Tokens, []) ->
Tokens;
prune_tokens([{kw_identifier_safe, _, _} | _] = Tokens, []) ->
Tokens;
prune_tokens([{kw_identifier_unsafe, _, _} | _] = Tokens, []) ->
Tokens;
prune_tokens([{OpType, _, _} | _] = Tokens, [])
when OpType =:= comp_op; OpType =:= at_op; OpType =:= unary_op; OpType =:= and_op;
OpType =:= or_op; OpType =:= arrow_op; OpType =:= match_op; OpType =:= in_op;
OpType =:= in_match_op; OpType =:= type_op; OpType =:= dual_op; OpType =:= mult_op;
OpType =:= power_op; OpType =:= concat_op; OpType =:= range_op; OpType =:= xor_op;
OpType =:= pipe_op; OpType =:= stab_op; OpType =:= when_op; OpType =:= assoc_op;
OpType =:= rel_op; OpType =:= ternary_op; OpType =:= capture_op; OpType =:= ellipsis_op ->
{Tokens, Terminators};
Tokens;
%%% or we traverse until the end.
prune_tokens([_ | Tokens], Opener, Terminators) ->
prune_tokens(Tokens, Opener, Terminators);
prune_tokens([], [], Terminators) ->
{[], Terminators}.
prune_tokens([_ | Tokens], Opener) ->
prune_tokens(Tokens, Opener);
prune_tokens([], _Opener) ->
[].
+27 -1
View File
@@ -1315,9 +1315,15 @@ defmodule CodeFragmentTest do
assert cc2q!("(fn x ->", trailing_fragment: ":ok end)") ==
s2q!("(fn x -> __cursor__() end)")
assert cc2q!("(fn x ->", trailing_fragment: ":ok end)") ==
assert cc2q!("(fn x ->", trailing_fragment: "\n:ok end)") ==
s2q!("(fn x -> __cursor__() end)")
assert cc2q!("(fn x when ", trailing_fragment: "-> :ok end)") ==
s2q!("(fn x when __cursor__() -> :ok end)")
assert cc2q!("(fn x when ", trailing_fragment: "->\n:ok end)") ==
s2q!("(fn x when __cursor__() -> :ok end)")
assert cc2q!("(fn") == s2q!("(__cursor__())")
assert cc2q!("(fn x") == s2q!("(__cursor__())")
assert cc2q!("(fn x,") == s2q!("(__cursor__())")
@@ -1327,6 +1333,26 @@ defmodule CodeFragmentTest do
assert cc2q!("(fn x, y -> x + y end") == s2q!("(__cursor__())")
end
test "do -> end" do
assert cc2q!("if do\nx ->\n", trailing_fragment: "y\nz ->\nw\nend") ==
s2q!("if do\nx ->\n__cursor__()\nz -> \nw\nend")
assert cc2q!("if do\nx ->\ny", trailing_fragment: "\nz ->\nw\nend") ==
s2q!("if do\nx ->\n__cursor__()\nz -> \nw\nend")
assert cc2q!("if do\nx ->\ny\n", trailing_fragment: "\nz ->\nw\nend") ==
s2q!("if do\nx ->\ny\n__cursor__()\nz -> \nw\nend")
assert cc2q!("for x <- [], reduce: %{} do\ny, ", trailing_fragment: "-> :ok\nend") ==
s2q!("for x <- [], reduce: %{} do\ny, __cursor__() -> :ok\nend")
assert cc2q!("for x <- [], reduce: %{} do\ny, z when ", trailing_fragment: "-> :ok\nend") ==
s2q!("for x <- [], reduce: %{} do\ny, z when __cursor__() -> :ok\nend")
assert cc2q!("case do\na -> a\nb = ", trailing_fragment: "c -> c\nend") ==
s2q!("case do\na -> a\nb = __cursor__() -> c\nend")
end
test "removes tokens until opening" do
assert cc2q!("(123") == s2q!("(__cursor__())")
assert cc2q!("[foo") == s2q!("[__cursor__()]")
+12
View File
@@ -1517,6 +1517,18 @@ defmodule EnumTest do
assert Enum.zip([], []) == []
end
test "zip/2 with infinite streams" do
assert Enum.zip([], Stream.cycle([1, 2])) == []
assert Enum.zip([], Stream.cycle(1..2)) == []
assert Enum.zip(.., Stream.cycle([1, 2])) == []
assert Enum.zip(.., Stream.cycle(1..2)) == []
assert Enum.zip(Stream.cycle([1, 2]), ..) == []
assert Enum.zip(Stream.cycle(1..2), ..) == []
assert Enum.zip(Stream.cycle([1, 2]), ..) == []
assert Enum.zip(Stream.cycle(1..2), ..) == []
end
test "zip/1" do
assert Enum.zip([[:a, :b], [1, 2], ["foo", "bar"]]) == [{:a, 1, "foo"}, {:b, 2, "bar"}]
+10
View File
@@ -46,6 +46,12 @@ defmodule ExceptionTest do
{:io, :put_chars, [self(), <<222>>],
[error_info: %{module: __MODULE__, function: :dummy_error_extras}]}
])
assert %ErlangError{original: {:failed_load_cacerts, :enoent}, reason: ": this is chardata"} =
Exception.normalize(:error, {:failed_load_cacerts, :enoent}, [
{:pubkey_os_cacerts, :get, 0,
[error_info: %{module: __MODULE__, function: :dummy_error_chardata}]}
])
end
test "format/2 without stacktrace" do
@@ -1026,4 +1032,8 @@ defmodule ExceptionTest do
end
def dummy_error_extras(_exception, _stacktrace), do: %{general: "foo"}
def dummy_error_chardata(_exception, _stacktrace) do
%{general: ~c"this is " ++ [~c"chardata"], reason: ~c"this " ++ [~c"too"]}
end
end
@@ -1,29 +0,0 @@
defmodule Dialyzer.ProtocolOpaque do
def circus() do
duck = Dialyzer.ProtocolOpaque.Duck.new()
Dialyzer.ProtocolOpaque.Entity.speak(duck)
end
end
defprotocol Dialyzer.ProtocolOpaque.Entity do
@fallback_to_any true
def speak(entity)
end
defmodule Dialyzer.ProtocolOpaque.Duck do
@opaque t :: %__MODULE__{feathers: :white_and_grey}
defstruct feathers: :white_and_grey
@spec new :: t
def new(), do: %__MODULE__{}
defimpl Dialyzer.ProtocolOpaque.Entity do
def speak(%Dialyzer.ProtocolOpaque.Duck{}), do: "Quack!"
end
end
defimpl Dialyzer.ProtocolOpaque.Entity, for: Any do
def speak(_any) do
"I can be anything"
end
end
+12 -4
View File
@@ -33,8 +33,8 @@ defmodule JSONTest do
end
test "maps" do
assert JSON.encode!(%{1 => 2, 3.0 => 4.0, key: :bar}) ==
"{\"1\":2,\"3.0\":4.0,\"key\":\"bar\"}"
assert JSON.encode!(%{1 => 2, 3.0 => 4.0, ~c"list" => ~c"list", key: :bar}) ==
"{\"1\":2,\"3.0\":4.0,\"key\":\"bar\",\"list\":[108,105,115,116]}"
end
test "lists" do
@@ -46,6 +46,14 @@ defmodule JSONTest do
assert JSON.encode!(%Token{value: :example}) == "[\"example\"]"
assert JSON.encode!(%Token{value: "hello\0world"}) == "[\"hello\\u0000world\"]"
end
test "calendar" do
assert JSON.encode!(~D[2010-04-17]) == "\"2010-04-17\""
assert JSON.encode!(~T[14:00:00.123]) == "\"14:00:00.123\""
assert JSON.encode!(~N[2010-04-17 14:00:00.123]) == "\"2010-04-17T14:00:00.123\""
assert JSON.encode!(~U[2010-04-17 14:00:00.123Z]) == "\"2010-04-17T14:00:00.123Z\""
assert JSON.encode!(Duration.new!(month: 2, hour: 3)) == "\"P2MT3H\""
end
end
describe "JSON.Encoder" do
@@ -72,8 +80,8 @@ defmodule JSONTest do
end
test "maps" do
assert protocol_encode(%{1 => 2, 3.0 => 4.0, key: :bar}) ==
"{\"1\":2,\"3.0\":4.0,\"key\":\"bar\"}"
assert protocol_encode(%{1 => 2, 3.0 => 4.0, ~c"list" => ~c"list", key: :bar}) ==
"{\"1\":2,\"3.0\":4.0,\"key\":\"bar\",\"list\":[108,105,115,116]}"
end
test "lists" do
+1 -3
View File
@@ -64,9 +64,7 @@ end
test_parameters =
if(PathHelpers.windows?(),
do: [%{cli_extension: ".bat"}],
else:
[%{cli_extension: ""}] ++
if(System.find_executable("pwsh"), do: [%{cli_extension: ".ps1"}], else: [])
else: [%{cli_extension: ""}]
)
defmodule Kernel.CLI.ExecutableTest do
@@ -859,6 +859,35 @@ defmodule Kernel.DiagnosticsTest do
purge(Sample)
end
@tag :tmp_dir
test "simple warning with tabs (line + file)", %{tmp_dir: tmp_dir} do
path = make_relative_tmp(tmp_dir, "long-warning.ex")
source = """
defmodule Sample do
\t@file "#{path}"
\tdefp a do
\t\tUnknown.b()
\tend
end
"""
File.write!(path, source)
expected = """
warning: Unknown.b/0 is undefined (module Unknown is not available or is yet to be defined). Make sure the module name is correct and has been specified in full (or that an alias has been defined)
│
4 │ \t\tUnknown.b()
│ ~~~~~~~~~~~
│
└─ #{path}:4: Sample.a/0
"""
assert capture_eval(source, columns: false) =~ expected
after
purge(Sample)
end
test "simple warning (no file)" do
source = """
defmodule Sample do
@@ -119,23 +119,6 @@ defmodule Kernel.DialyzerTest do
assert_dialyze_no_warnings!(context)
end
@tag warnings: [:specdiffs]
test "no warnings on protocol calls with opaque types", context do
alias Dialyzer.ProtocolOpaque
copy_beam!(context, ProtocolOpaque)
copy_beam!(context, ProtocolOpaque.Entity)
copy_beam!(context, ProtocolOpaque.Entity.Any)
copy_beam!(context, ProtocolOpaque.Duck)
assert_dialyze_no_warnings!(context)
# Also ensure no warnings after consolidation.
Code.prepend_path(context.base_dir)
{:ok, binary} = Protocol.consolidate(ProtocolOpaque.Entity, [ProtocolOpaque.Duck, Any])
File.write!(Path.join(context.outdir, "#{ProtocolOpaque.Entity}.beam"), binary)
assert_dialyze_no_warnings!(context)
end
test "no warnings on and/2 and or/2", context do
copy_beam!(context, Dialyzer.BooleanCheck)
assert_dialyze_no_warnings!(context)
+1 -1
View File
@@ -255,7 +255,7 @@ defmodule Kernel.DocsTest do
assert module_doc == "Module doc"
file = __ENV__.file
file = String.to_charlist(__ENV__.file)
source_annos = [:erl_anno.new({line + 3, 19})]
@@ -190,17 +190,6 @@ defmodule Kernel.ExpansionTest do
end
test "errors on directly recursive definitions" do
assert_compile_error(
~r"""
recursive variable definition in patterns:
x = x
the variable "x" \(context Kernel.ExpansionTest\) is defined in function of itself
""",
fn -> expand(quote(do: (x = x) = :ok)) end
)
assert_compile_error(
~r"""
recursive variable definition in patterns:
@@ -1214,6 +1203,11 @@ defmodule Kernel.ExpansionTest do
[{:->, [{:line, 1}], [[{:capture, [line: 1], nil}], {:capture, [line: 1], nil}]}]}
end
test "removes no_parens when expanding 0-arity capture to fn" do
assert expand(quote(do: &foo().bar/0)) ==
quote(do: fn -> foo().bar() end)
end
test "expands remotes" do
assert expand(quote(do: &List.flatten/2)) ==
quote(do: &:"Elixir.List".flatten/2)
@@ -95,6 +95,12 @@ defmodule Kernel.FnTest do
assert (&mod.flatten/1) == (&List.flatten/1)
end
test "capture with module from local call" do
assert (&math_mod().pi/0).() == :math.pi()
end
defp math_mod, do: :math
test "local partial application" do
assert (&atb(&1, :utf8)).(:a) == "a"
assert (&atb(List.to_atom(&1), :utf8)).(~c"a") == "a"
@@ -517,5 +517,55 @@ defmodule Kernel.LexicalTrackerTest do
refute URI in exports
refute URI in runtime
end
test "imported functions from quote adds dependencies" do
{{compile, exports, runtime, _}, _binding} =
Code.eval_string("""
defmodule Kernel.LexicalTrackerTest.QuotedFun do
import URI
defmacro parse_root() do
quote do
parse("/")
end
end
end
defmodule Kernel.LexicalTrackerTest.UsingQuotedFun do
require Kernel.LexicalTrackerTest.QuotedFun, as: QF
QF.parse_root()
Kernel.LexicalTracker.references(__ENV__.lexical_tracker)
end |> elem(3)
""")
assert URI in compile
refute URI in exports
refute URI in runtime
end
test "imported macro from quote adds dependencies" do
{{compile, exports, runtime, _}, _binding} =
Code.eval_string("""
defmodule Kernel.LexicalTrackerTest.QuotedMacro do
import Config
defmacro config_env() do
quote do
config_env()
end
end
end
defmodule Kernel.LexicalTrackerTest.UsingQuotedMacro do
require Kernel.LexicalTrackerTest.QuotedMacro, as: QM
def fun(), do: QM.config_env()
Kernel.LexicalTracker.references(__ENV__.lexical_tracker)
end |> elem(3)
""")
assert Config in compile
refute Config in exports
refute Config in runtime
end
end
end
@@ -421,40 +421,6 @@ defmodule Kernel.ParallelCompilerTest do
end)
end
test "supports warnings as errors" do
[fixture] =
write_tmp(
"warnings_as_errors",
warnings_as_errors: """
defmodule WarningsSample do
def hello(a), do: a
def hello(b), do: b
end
"""
)
output = tmp_path("not_to_be_used")
try do
msg =
capture_io(:stderr, fn ->
assert {:error, [error], []} =
Kernel.ParallelCompiler.compile_to_path([fixture], output,
warnings_as_errors: true
)
assert {^fixture, {3, 7}, "this clause " <> _} = error
end)
assert msg =~
"Compilation failed due to warnings while using the --warnings-as-errors option\n"
after
purge([WarningsSample])
end
refute File.exists?(output)
end
test "does not use incorrect line number when error originates in another file" do
File.mkdir_p!(tmp_path())
@@ -587,33 +553,5 @@ defmodule Kernel.ParallelCompilerTest do
"cannot compile module WithBehaviourAndStruct (errors have been logged)"
end) =~ expected_msg
end
test "supports warnings as errors" do
[fixture] =
write_tmp(
"warnings_as_errors",
warnings_as_errors: """
defmodule WarningsSample do
def hello(a), do: a
def hello(b), do: b
end
"""
)
try do
msg =
capture_io(:stderr, fn ->
assert {:error, [error], []} =
Kernel.ParallelCompiler.require([fixture], warnings_as_errors: true)
assert {^fixture, {3, 7}, "this clause " <> _} = error
end)
assert msg =~
"Compilation failed due to warnings while using the --warnings-as-errors option\n"
after
purge([WarningsSample])
end
end
end
end
+80 -2
View File
@@ -10,11 +10,15 @@ defmodule Kernel.TracersTest do
end
def trace(event, %Macro.Env{} = env) do
send(self(), {event, env})
for {pid, _} <- Registry.lookup(__MODULE__, :tracers) do
send(pid, {event, env})
end
:ok
end
setup_all do
start_supervised!({Registry, keys: :duplicate, name: __MODULE__})
Code.put_compiler_option(:tracers, [__MODULE__])
on_exit(fn ->
@@ -22,6 +26,11 @@ defmodule Kernel.TracersTest do
end)
end
setup do
Registry.register(__MODULE__, :tracers, :unused)
:ok
end
test "traces start and stop" do
compile_string("""
Foo
@@ -255,7 +264,7 @@ defmodule Kernel.TracersTest do
assert meta[:from_brackets]
compile_string("""
defmodule Foo do
defmodule TracerBracketAccess do
@foo %{bar: 3}
def a() do
@foo[:bar]
@@ -274,6 +283,75 @@ defmodule Kernel.TracersTest do
assert meta[:from_brackets]
end
test "traces on_load" do
compile_string("""
defmodule TracerOnLoad do
@on_load :init
def init, do: :ok
end
""")
assert_received {{:local_function, meta, :init, 0}, _}
assert meta[:line] == 1
end
def __before_compile__(_), do: :ok
def __after_compile__(_, _), do: :ok
def __after_verify__(_), do: :ok
def __on_definition__(_, _, _, _, _, _), do: :ok
test "traces compile time attributes" do
compile_string("""
defmodule TracerCompileAttributes do
@before_compile Kernel.TracersTest
@after_compile Kernel.TracersTest
@on_definition Kernel.TracersTest
@after_verify Kernel.TracersTest
def hello, do: :world
end
""")
assert_received {{:remote_function, meta, __MODULE__, :__before_compile__, 1}, _}
assert meta[:line] == 1
assert_received {{:remote_function, meta, __MODULE__, :__after_compile__, 2}, _}
assert meta[:line] == 1
assert_received {{:remote_function, meta, __MODULE__, :__after_verify__, 1}, _}
assert meta[:line] == 1
assert_received {{:remote_function, meta, __MODULE__, :__on_definition__, 6}, _}
assert meta[:line] == 6
end
test "traces super" do
compile_string("""
defmodule TracerOverridable do
def local(x), do: x
defoverridable [local: 1]
def local(x), do: super(x)
defmacro macro(x), do: x
defoverridable [macro: 1]
defmacro macro(x), do: super(x)
def capture(x), do: x
defoverridable [capture: 1]
def capture(x), do: tap(x, &super/1)
def capture_arg(x), do: x
defoverridable [capture_arg: 1]
def capture_arg(x), do: tap(x, &super(&1))
end
""")
assert_received {{:local_function, _, :"local (overridable 1)", 1}, _}
assert_received {{:local_function, _, :"macro (overridable 1)", 1}, _}
assert_received {{:local_function, _, :"capture (overridable 1)", 1}, _}
assert_received {{:local_function, _, :"capture_arg (overridable 1)", 1}, _}
refute_received {{:local_function, _, _, _}, _}
end
test "does not trace bind quoted twice" do
compile_string("""
quote bind_quoted: [foo: List.flatten([])] do
+15 -1
View File
@@ -190,7 +190,7 @@ defmodule Kernel.WarningTest do
)
assert_warn_eval(
["nofile:1:3", "found quoted keyword \"foo\" but the quotes are not required"],
["nofile:1:2", "found quoted keyword \"foo\" but the quotes are not required"],
~s/["foo": :bar]/
)
@@ -264,6 +264,20 @@ defmodule Kernel.WarningTest do
purge(Sample)
end
test "duplicate pattern" do
output =
capture_eval("""
defmodule Sample do
var = quote(do: x)
def hello(unquote(var) = unquote(var)), do: unquote(var)
end
""")
assert output =~ "this pattern is matched against itself inside a match: x = x"
after
purge(Sample)
end
test "unused compiler variable" do
output =
capture_eval("""
+6
View File
@@ -205,6 +205,10 @@ defmodule KernelTest do
defmodule User do
assert is_map(defstruct name: "john")
# Ensure we keep the line information around.
# It is important for debugging tools, ExDoc, etc.
{:v1, :def, anno, _clauses} = Module.get_definition(__MODULE__, {:__struct__, 1})
anno[:line] == __ENV__.line - 4
end
test "struct/1 and struct/2" do
@@ -1136,6 +1140,8 @@ defmodule KernelTest do
test "pop_in/1" do
users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
assert pop_in(users["john"]) == {%{age: 27}, %{"meg" => %{age: 23}}}
assert pop_in(users["john"][:age]) == {27, %{"john" => %{}, "meg" => %{age: 23}}}
assert pop_in(users["john"][:name]) == {nil, %{"john" => %{age: 27}, "meg" => %{age: 23}}}
assert pop_in(users["bob"][:age]) == {nil, %{"john" => %{age: 27}, "meg" => %{age: 23}}}
+6
View File
@@ -136,6 +136,12 @@ defmodule MacroTest do
test "does not add context to quote" do
assert Macro.escape({:quote, [], [[do: :foo]]}) == {:{}, [], [:quote, [], [[do: :foo]]]}
end
test "inspects container when a reference cannot be escaped" do
assert_raise ArgumentError, ~r"~r/foo/ contains a reference", fn ->
Macro.escape(%{~r/foo/ | re_pattern: make_ref()})
end
end
end
describe "expand_once/2" do
+9
View File
@@ -395,6 +395,15 @@ defmodule MapTest do
assert quoted == {:%, [], [User, {:%{}, [], [{:foo, 1}]}]}
end
test "structs with bitstring defaults" do
defmodule WithBitstring do
defstruct bitstring: <<255, 127::7>>
end
info = Macro.struct_info!(WithBitstring, __ENV__)
assert info == [%{default: <<255, 127::7>>, field: :bitstring}]
end
test "defstruct can only be used once in a module" do
message =
"defstruct has already been called for TestMod, " <>
@@ -101,6 +101,46 @@ defmodule Module.Types.DescrTest do
assert union(difference(list(term()), list(integer())), list(integer()))
|> equal?(list(term()))
end
test "optimizations" do
# The tests are checking the actual implementation, not the semantics.
# This is why we are using structural comparisons.
# It's fine to remove these if the implementation changes, but breaking
# these might have an important impact on compile times.
# Optimization one: same tags, all but one key are structurally equal
assert union(
open_map(a: float(), b: atom()),
open_map(a: integer(), b: atom())
) == open_map(a: union(float(), integer()), b: atom())
assert union(
closed_map(a: float(), b: atom()),
closed_map(a: integer(), b: atom())
) == closed_map(a: union(float(), integer()), b: atom())
# Optimization two: we can tell that one map is a trivial subtype of the other:
assert union(
closed_map(a: term(), b: term()),
closed_map(a: float(), b: binary())
) == closed_map(a: term(), b: term())
assert union(
open_map(a: term()),
closed_map(a: float(), b: binary())
) == open_map(a: term())
assert union(
closed_map(a: float(), b: binary()),
open_map(a: term())
) == open_map(a: term())
assert union(
closed_map(a: term(), b: tuple([term(), term()])),
closed_map(a: float(), b: tuple([atom(), binary()]))
) == closed_map(a: term(), b: tuple([term(), term()]))
end
end
describe "intersection" do
@@ -163,6 +203,9 @@ defmodule Module.Types.DescrTest do
assert intersection(closed_map(a: integer()), open_map(b: not_set())) ==
closed_map(a: integer())
assert intersection(closed_map(a: integer()), open_map(b: if_set(integer()))) ==
closed_map(a: integer())
assert equal?(
intersection(closed_map(a: integer()), closed_map(a: if_set(integer()))),
closed_map(a: integer())
@@ -1184,7 +1227,7 @@ defmodule Module.Types.DescrTest do
test "boolean" do
assert boolean() |> to_quoted_string() == "boolean()"
assert atom([true, false, :a]) |> to_quoted_string() == "boolean() or :a"
assert atom([true, false, :a]) |> to_quoted_string() == ":a or boolean()"
assert atom([true, :a]) |> to_quoted_string() == ":a or true"
assert difference(atom(), boolean()) |> to_quoted_string() == "atom() and not boolean()"
end
@@ -1199,7 +1242,7 @@ defmodule Module.Types.DescrTest do
assert intersection(atom(), dynamic()) |> to_quoted_string() == "dynamic(atom())"
assert union(atom([:foo, :bar]), dynamic()) |> to_quoted_string() ==
"dynamic() or (:bar or :foo)"
"dynamic() or :bar or :foo"
assert intersection(dynamic(), closed_map(a: integer())) |> to_quoted_string() ==
"dynamic(%{a: integer()})"
@@ -1256,7 +1299,7 @@ defmodule Module.Types.DescrTest do
assert open_tuple([integer(), atom()]) |> to_quoted_string() == "{integer(), atom(), ...}"
assert union(tuple([integer(), atom()]), open_tuple([atom()])) |> to_quoted_string() ==
"{integer(), atom()} or {atom(), ...}"
"{atom(), ...} or {integer(), atom()}"
assert difference(tuple([integer(), atom()]), open_tuple([atom()])) |> to_quoted_string() ==
"{integer(), atom()}"
@@ -1268,6 +1311,96 @@ defmodule Module.Types.DescrTest do
# assert difference(tuple([number(), term()]), tuple([integer(), atom()]))
# |> to_quoted_string() ==
# "{float(), term()} or {number(), term() and not atom()}"
assert union(tuple([integer(), atom()]), tuple([integer(), atom()])) |> to_quoted_string() ==
"{integer(), atom()}"
assert union(tuple([integer(), atom()]), tuple([float(), atom()])) |> to_quoted_string() ==
"{float() or integer(), atom()}"
assert union(tuple([integer(), atom()]), tuple([float(), atom()]))
|> union(tuple([pid(), pid(), port()]))
|> union(tuple([pid(), pid(), atom()]))
|> to_quoted_string() ==
"{float() or integer(), atom()} or {pid(), pid(), atom() or port()}"
assert union(open_tuple([integer()]), open_tuple([float()])) |> to_quoted_string() ==
"{float() or integer(), ...}"
# {:ok, {term(), integer()}} or {:ok, {term(), float()}} or {:exit, :kill} or {:exit, :timeout}
assert tuple([atom([:ok]), tuple([term(), empty_list()])])
|> union(tuple([atom([:ok]), tuple([term(), open_map()])]))
|> union(tuple([atom([:exit]), atom([:kill])]))
|> union(tuple([atom([:exit]), atom([:timeout])]))
|> to_quoted_string() ==
"{:exit, :kill or :timeout} or {:ok, {term(), %{...} or empty_list()}}"
# Detection of duplicates
assert tuple([atom([:ok]), term()])
|> union(tuple([atom([:ok]), term()]))
|> to_quoted_string() == "{:ok, term()}"
assert tuple([closed_map(a: integer(), b: atom()), open_map()])
|> union(tuple([closed_map(a: integer(), b: atom()), open_map()]))
|> to_quoted_string() ==
"{%{a: integer(), b: atom()}, %{...}}"
# Nested fusion
assert tuple([closed_map(a: integer(), b: atom()), open_map()])
|> union(tuple([closed_map(a: float(), b: atom()), open_map()]))
|> to_quoted_string() ==
"{%{a: float() or integer(), b: atom()}, %{...}}"
# Complex simplification of map/tuple combinations. Initial type is:
# ```
# dynamic(
# :error or
# ({%Decimal{coef: :inf, exp: integer(), sign: integer()}, binary()} or
# {%Decimal{coef: :NaN, exp: integer(), sign: integer()}, binary()} or
# {%Decimal{coef: integer(), exp: integer(), sign: integer()}, term()} or
# {%Decimal{coef: :inf, exp: integer(), sign: integer()} or
# %Decimal{coef: :NaN, exp: integer(), sign: integer()} or
# %Decimal{coef: integer(), exp: integer(), sign: integer()}, term()})
# )
# ```
decimal_inf =
closed_map(
__struct__: atom([Decimal]),
coef: atom([:inf]),
exp: integer(),
sign: integer()
)
decimal_nan =
closed_map(
__struct__: atom([Decimal]),
coef: atom([:NaN]),
exp: integer(),
sign: integer()
)
decimal_int =
closed_map(__struct__: atom([Decimal]), coef: integer(), exp: integer(), sign: integer())
assert atom([:error])
|> union(
tuple([decimal_inf, binary()])
|> union(
tuple([decimal_nan, binary()])
|> union(
tuple([decimal_int, term()])
|> union(tuple([union(decimal_inf, union(decimal_nan, decimal_int)), term()]))
)
)
)
|> dynamic()
|> to_quoted_string() ==
"""
dynamic(
:error or {%Decimal{coef: :NaN or :inf, exp: integer(), sign: integer()}, binary()} or
{%Decimal{coef: :NaN or :inf or integer(), exp: integer(), sign: integer()}, term()}
)\
"""
end
test "map" do
@@ -1311,6 +1444,50 @@ defmodule Module.Types.DescrTest do
assert difference(open_map(a: number(), b: atom()), open_map(a: integer()))
|> to_quoted_string() == "%{..., a: float(), b: atom()}"
# Basic map fusion
assert union(closed_map(a: integer()), closed_map(a: integer())) |> to_quoted_string() ==
"%{a: integer()}"
assert union(closed_map(a: integer()), closed_map(a: float())) |> to_quoted_string() ==
"%{a: float() or integer()}"
# Nested fusion
assert union(closed_map(a: integer(), b: atom()), closed_map(a: float(), b: atom()))
|> union(closed_map(x: pid(), y: pid(), z: port()))
|> union(closed_map(x: pid(), y: pid(), z: atom()))
|> to_quoted_string() ==
"%{a: float() or integer(), b: atom()} or %{x: pid(), y: pid(), z: atom() or port()}"
# Open map fusion
assert union(open_map(a: integer()), open_map(a: float())) |> to_quoted_string() ==
"%{..., a: float() or integer()}"
# Fusing complex nested maps with unions
assert closed_map(status: atom([:ok]), data: closed_map(value: term(), count: empty_list()))
|> union(
closed_map(status: atom([:ok]), data: closed_map(value: term(), count: open_map()))
)
|> union(closed_map(status: atom([:error]), reason: atom([:timeout])))
|> union(closed_map(status: atom([:error]), reason: atom([:crash])))
|> to_quoted_string() ==
"%{data: %{count: %{...} or empty_list(), value: term()}, status: :ok} or\n %{reason: :crash or :timeout, status: :error}"
# Difference and union tests
assert closed_map(status: atom([:ok]), value: term())
|> difference(closed_map(status: atom([:ok]), value: float()))
|> union(
closed_map(status: atom([:ok]), value: term())
|> difference(closed_map(status: atom([:ok]), value: integer()))
)
|> to_quoted_string() ==
"%{status: :ok, value: term()}"
# Nested map fusion
assert closed_map(data: closed_map(x: integer(), y: atom()), meta: open_map())
|> union(closed_map(data: closed_map(x: float(), y: atom()), meta: open_map()))
|> to_quoted_string() ==
"%{data: %{x: float() or integer(), y: atom()}, meta: %{...}}"
# Test complex combinations
assert intersection(open_map(a: number(), b: atom()), open_map(a: integer(), c: boolean()))
|> union(difference(open_map(x: atom()), open_map(x: boolean())))
@@ -243,6 +243,35 @@ defmodule Module.Types.ExprTest do
"""
end
test "calling a function with invalid arguments on variables" do
assert typeerror!(
(
x = List
x.to_tuple(123)
)
)
|> strip_ansi() ==
~l"""
incompatible types given to List.to_tuple/1:
x.to_tuple(123)
given types:
integer()
but expected one of:
list(term())
where "x" was given the type:
# type: List
# from: types_test.ex:LINE-5
x = List
"""
end
test "capture a function with non atoms" do
assert typeerror!([<<x::integer>>], &x.foo_bar/2) ==
~l"""
@@ -798,9 +827,9 @@ defmodule Module.Types.ExprTest do
x.foo_bar
where "x" was given the type:
the given type does not have the given key:
# type: dynamic(%URI{
dynamic(%URI{
authority: term(),
fragment: term(),
host: term(),
@@ -810,6 +839,10 @@ defmodule Module.Types.ExprTest do
scheme: term(),
userinfo: term()
})
where "x" was given the type:
# type: dynamic(%URI{})
# from: types_test.ex:LINE-4:43
x = %URI{}
"""
@@ -827,7 +860,11 @@ defmodule Module.Types.ExprTest do
test "in dynamic mode" do
assert typedyn!([x = 123, y = 456.0], x < y) == dynamic(boolean())
assert typedyn!([x = 123, y = 456.0], x == y) == dynamic(boolean())
assert typedyn!(123 == 456) == boolean()
assert typedyn!([x = 123, y = 456], x == y) == dynamic(boolean())
end
test "using literals" do
assert typecheck!(:foo == :bar) == boolean()
end
test "min/max" do
@@ -845,6 +882,10 @@ defmodule Module.Types.ExprTest do
min(x, y)
given types:
min(dynamic(:foo), integer())
where "x" was given the type:
# type: dynamic(:foo)
@@ -868,6 +909,10 @@ defmodule Module.Types.ExprTest do
x === y
given types:
integer() === float()
where "x" was given the type:
# type: integer()
@@ -893,6 +938,10 @@ defmodule Module.Types.ExprTest do
mod.<=(x, y)
given types:
dynamic(:foo) <= dynamic(%Point{})
where "mod" was given the type:
# type: dynamic(Kernel)
@@ -907,7 +956,7 @@ defmodule Module.Types.ExprTest do
where "y" was given the type:
# type: dynamic(%Point{x: term(), y: term(), z: term()})
# type: dynamic(%Point{})
# from: types_test.ex:LINE-2
y = %Point{}
@@ -1015,6 +1064,15 @@ defmodule Module.Types.ExprTest do
end
describe "case" do
test "does not type check literals" do
assert typecheck!(
case :dev do
:dev -> :ok
:prod -> :error
end
) == atom([:ok, :error])
end
test "returns unions of all clauses" do
assert typecheck!(
[x],
@@ -1070,7 +1128,7 @@ defmodule Module.Types.ExprTest do
end
describe "conditionals" do
test "if does not report on literal atoms" do
test "if does not report on literals" do
assert typecheck!(
if true do
:ok
@@ -1078,15 +1136,15 @@ defmodule Module.Types.ExprTest do
) == atom([:ok, nil])
end
test "and does not report on literal atoms" do
test "and does not report on literals" do
assert typecheck!(false and true) == boolean()
end
test "and reports on non-atom literals" do
assert typeerror!(1 and true) == ~l"""
test "and reports violations" do
assert typeerror!([x = 123], x and true) =~ """
the following conditional expression will always evaluate to integer():
1
x
"""
end
end
@@ -1224,16 +1282,18 @@ defmodule Module.Types.ExprTest do
e
end
) ==
union(
closed_map(
__struct__: atom([ArgumentError]),
__exception__: atom([true]),
message: term()
),
closed_map(
__struct__: atom([RuntimeError]),
__exception__: atom([true]),
message: term()
dynamic(
union(
closed_map(
__struct__: atom([ArgumentError]),
__exception__: atom([true]),
message: term()
),
closed_map(
__struct__: atom([RuntimeError]),
__exception__: atom([true]),
message: term()
)
)
)
end
@@ -1382,6 +1442,16 @@ defmodule Module.Types.ExprTest do
assert typewarn!(:string.__info__(:functions)) ==
{dynamic(), ":string.__info__/1 is undefined or private"}
assert typeerror!([x], x.__info__(:whatever)) |> strip_ansi() =~ """
incompatible types given to __info__/1:
x.__info__(:whatever)
given types:
:whatever
"""
end
test "behaviour_info/1" do
@@ -36,7 +36,7 @@ defmodule Module.Types.IntegrationTest do
"""
}
modules = compile(files)
modules = compile_modules(files)
assert [
{{:c, 0}, %{}},
@@ -195,6 +195,118 @@ defmodule Module.Types.IntegrationTest do
assert_warnings(files, warnings)
end
test "unused generated private clauses" do
files = %{
"a.ex" => """
defmodule A do
use B
def public(x), do: private(List.to_tuple(x))
end
""",
"b.ex" => """
defmodule B do
defmacro __using__(_) do
quote generated: true do
defp private({:ok, ok}), do: ok
defp private(:error), do: :error
end
end
end
"""
}
assert_no_warnings(files)
end
test "unused overridable private clauses" do
files = %{
"a.ex" => """
defmodule A do
use B
def public(x), do: private(x)
defp private(x), do: super(List.to_tuple(x))
end
""",
"b.ex" => """
defmodule B do
defmacro __using__(_) do
quote do
defp private({:ok, ok}), do: ok
defp private(:error), do: :error
defoverridable private: 1
end
end
end
"""
}
assert_no_warnings(files)
end
test "incompatible default argument" do
files = %{
"a.ex" => """
defmodule A do
def ok(x = :ok \\\\ nil) do
x
end
end
"""
}
warnings = [
~S"""
warning: incompatible types given as default arguments to ok/1:
-nil-
but expected one of:
dynamic(:ok)
typing violation found at:
│
2 │ def ok(x = :ok \\ nil) do
│ ~
│
└─ a.ex:2:18: A.ok/0
"""
]
assert_warnings(files, warnings)
end
test "returns diagnostics with source and file" do
files = %{
"a.ex" => """
defmodule A do
@file "generated.ex"
def fun(arg) do
:ok = List.to_tuple(arg)
end
end
"""
}
{_modules, warnings} = with_compile_warnings(files)
assert [
%{
message: "the following pattern will never match" <> _,
file: file,
source: source
}
] = warnings.runtime_warnings
assert String.ends_with?(source, "a.ex")
assert Path.type(source) == :absolute
assert String.ends_with?(file, "generated.ex")
assert Path.type(file) == :absolute
after
:code.delete(A)
:code.purge(A)
end
end
describe "undefined warnings" do
@@ -932,26 +1044,39 @@ defmodule Module.Types.IntegrationTest do
defp capture_compile_warnings(files) do
in_tmp(fn ->
paths = generate_files(files)
capture_io(:stderr, fn -> compile_files(paths) end)
capture_io(:stderr, fn -> compile_to_path(paths) end)
end)
end
defp compile(files) do
defp with_compile_warnings(files) do
in_tmp(fn ->
paths = generate_files(files)
compile_files(paths)
with_io(:stderr, fn -> compile_to_path(paths) end) |> elem(0)
end)
end
defp compile_files(paths) do
{:ok, modules, _warnings} = Kernel.ParallelCompiler.compile_to_path(paths, ".")
defp compile_modules(files) do
in_tmp(fn ->
paths = generate_files(files)
{modules, _warnings} = compile_to_path(paths)
Map.new(modules, fn module ->
{^module, binary, _filename} = :code.get_object_code(module)
Map.new(modules, fn module ->
{^module, binary, _filename} = :code.get_object_code(module)
{module, binary}
end)
end)
end
defp compile_to_path(paths) do
{:ok, modules, warnings} =
Kernel.ParallelCompiler.compile_to_path(paths, ".", return_diagnostics: true)
for module <- modules do
:code.delete(module)
:code.purge(module)
{module, binary}
end)
end
{modules, warnings}
end
defp generate_files(files) do
@@ -166,9 +166,13 @@ defmodule Module.Types.PatternTest do
x.foo_bar
the given type does not have the given key:
dynamic(%Point{x: term(), y: term(), z: term()})
where "x" was given the type:
# type: dynamic(%Point{x: term(), y: term(), z: term()})
# type: dynamic(%Point{})
# from: types_test.ex:LINE-1
x = %Point{}
"""
@@ -247,6 +251,10 @@ defmodule Module.Types.PatternTest do
assert typecheck!([<<x::utf8>>], x) == integer()
end
test "nested" do
assert typecheck!([<<0, <<x::bitstring>>::binary>>], x) == binary()
end
test "error" do
assert typeerror!([<<x::binary-size(2), x::float>>], x) == ~l"""
incompatible types assigned to "x":
@@ -428,10 +428,9 @@ end
defmodule OptionsParserDeprecationsTest do
use ExUnit.Case, async: true
@warning ~r[not passing the :switches or :strict option to OptionParser is deprecated]
def assert_deprecated(fun) do
assert ExUnit.CaptureIO.capture_io(:stderr, fun) =~ @warning
assert ExUnit.CaptureIO.capture_io(:stderr, fun) =~
"not passing the :switches or :strict option to OptionParser is deprecated"
end
test "parses boolean option" do
+5 -2
View File
@@ -106,15 +106,18 @@ defmodule ProtocolTest do
assert Sample.impl_for(%ImplStruct{}) == Sample.ProtocolTest.ImplStruct
assert Sample.impl_for(%ImplStructExplicitFor{}) == Sample.ProtocolTest.ImplStructExplicitFor
assert Sample.impl_for(%NoImplStruct{}) == nil
assert is_nil(Sample.impl_for(%{__struct__: nil}))
end
test "protocol implementation with Any and struct fallbacks" do
assert WithAny.impl_for(%NoImplStruct{}) == WithAny.Any
# Derived
assert WithAny.impl_for(%ImplStruct{}) == ProtocolTest.WithAny.ProtocolTest.ImplStruct
assert WithAny.impl_for(%{__struct__: nil}) == WithAny.Any
assert WithAny.impl_for(%{__struct__: "foo"}) == WithAny.Map
assert WithAny.impl_for(%{}) == WithAny.Map
assert WithAny.impl_for(self()) == WithAny.Any
# Derived
assert WithAny.impl_for(%ImplStruct{}) == ProtocolTest.WithAny.ProtocolTest.ImplStruct
end
test "protocol not implemented" do
+1 -1
View File
@@ -35,7 +35,7 @@ defmodule RangeTest do
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
assert Range.new(3, 1) == 3..1//-1
end) =~ "has a default step of -1"
end) =~ "default to a step of -1"
end
test "fields" do
+17 -70
View File
@@ -3,41 +3,25 @@ Code.require_file("test_helper.exs", __DIR__)
defmodule RegexTest do
use ExUnit.Case, async: true
@re_21_3_little %Regex{
re_pattern:
{:re_pattern, 1, 0, 0,
<<69, 82, 67, 80, 94, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255,
255, 99, 0, 0, 0, 0, 0, 1, 0, 0, 0, 64, 0, 6, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 102, 111, 111, 0, 131, 0, 20, 29, 99, 133,
0, 7, 0, 1, 29, 100, 119, 0, 5, 29, 101, 120, 0, 12, 120, 0, 20, 0>>},
re_version: {"8.42 2018-03-20", :little},
source: "c(?<foo>d|e)"
}
@re_21_3_big %Regex{
re_pattern:
{:re_pattern, 1, 0, 0,
<<80, 67, 82, 69, 0, 0, 0, 86, 0, 0, 0, 0, 0, 0, 0, 17, 255, 255, 255, 255, 255, 255, 255,
255, 0, 99, 0, 0, 0, 0, 0, 1, 0, 0, 0, 56, 0, 6, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 102, 111, 111, 0, 131, 0, 20, 29, 99, 133, 0, 7, 0, 1, 29, 100, 119,
0, 5, 29, 101, 120, 0, 12, 120, 0, 20, 0>>},
re_version: {"8.42 2018-03-20", :big},
source: "c(?<foo>d|e)"
}
@re_19_3_little %Regex{
re_pattern:
{:re_pattern, 1, 0, 0,
<<69, 82, 67, 80, 94, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255,
255, 99, 0, 0, 0, 0, 0, 1, 0, 0, 0, 64, 0, 6, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 102, 111, 111, 0, 125, 0, 20, 29, 99, 127,
0, 7, 0, 1, 29, 100, 113, 0, 5, 29, 101, 114, 0, 12, 114, 0, 20, 0>>},
re_version: {"8.33 2013-05-29", :little},
source: "c(?<foo>d|e)"
}
doctest Regex
if System.otp_release() >= "28" do
test "module attribute" do
defmodule ModAttr do
@regex ~r/example/
def regex, do: @regex
@bare_regex :erlang.term_to_binary(@regex)
def bare_regex, do: :erlang.binary_to_term(@bare_regex)
# We don't rewrite outside of functions
assert @regex.re_pattern == :erlang.binary_to_term(@bare_regex).re_pattern
end
assert ModAttr.regex().re_pattern != ModAttr.bare_regex().re_pattern
end
end
test "multiline" do
refute Regex.match?(~r/^b$/, "a\nb\nc")
assert Regex.match?(~r/^b$/m, "a\nb\nc")
@@ -68,16 +52,9 @@ defmodule RegexTest do
test "literal source" do
assert Regex.source(Regex.compile!("foo")) == "foo"
assert Regex.source(~r"foo") == "foo"
assert Regex.re_pattern(Regex.compile!("foo")) == Regex.re_pattern(~r"foo")
assert Regex.source(Regex.compile!("\a\b\d\e\f\n\r\s\t\v")) == "\a\b\d\e\f\n\r\s\t\v"
assert Regex.source(~r<\a\b\d\e\f\n\r\s\t\v>) == "\\a\\b\\d\\e\\f\\n\\r\\s\\t\\v"
assert Regex.re_pattern(Regex.compile!("\a\b\d\e\f\n\r\s\t\v")) ==
Regex.re_pattern(~r"\x07\x08\x7F\x1B\x0C\x0A\x0D\x20\x09\x0B")
assert Regex.re_pattern(Regex.compile!("\\a\\b\\d\e\f\\n\\r\\s\\t\\v")) ==
Regex.re_pattern(~r"\a\b\d\e\f\n\r\s\t\v")
end
test "Unicode" do
@@ -116,16 +93,6 @@ defmodule RegexTest do
end
end
test "recompile/1" do
new_regex = ~r/foo/
{:ok, %Regex{}} = Regex.recompile(new_regex)
assert %Regex{} = Regex.recompile!(new_regex)
old_regex = Map.delete(~r/foo/, :re_version)
{:ok, %Regex{}} = Regex.recompile(old_regex)
assert %Regex{} = Regex.recompile!(old_regex)
end
test "opts/1" do
assert Regex.opts(Regex.compile!("foo", "i")) == [:caseless]
assert Regex.opts(Regex.compile!("foo", [:ucp])) == [:ucp]
@@ -179,16 +146,6 @@ defmodule RegexTest do
assert Regex.run(~r"bar", "foobar", offset: 2, return: :index) == [{3, 3}]
end
test "run/3 with regexes compiled in different systems" do
assert Regex.run(@re_21_3_little, "abcd abce", capture: :all_names) == ["d"]
assert Regex.run(@re_21_3_big, "abcd abce", capture: :all_names) == ["d"]
assert Regex.run(@re_19_3_little, "abcd abce", capture: :all_names) == ["d"]
end
test "run/3 with regexes with options compiled in different systems" do
assert Regex.run(%{~r/foo/i | re_version: "bad version"}, "FOO") == ["FOO"]
end
test "scan/2" do
assert Regex.scan(~r"c(d|e)", "abcd abce") == [["cd", "d"], ["ce", "e"]]
assert Regex.scan(~r"c(?:d|e)", "abcd abce") == [["cd"], ["ce"]]
@@ -207,16 +164,6 @@ defmodule RegexTest do
assert Regex.scan(~r"^foo", "foobar", offset: 1) == []
end
test "scan/2 with regexes compiled in different systems" do
assert Regex.scan(@re_21_3_little, "abcd abce", capture: :all_names) == [["d"], ["e"]]
assert Regex.scan(@re_21_3_big, "abcd abce", capture: :all_names) == [["d"], ["e"]]
assert Regex.scan(@re_19_3_little, "abcd abce", capture: :all_names) == [["d"], ["e"]]
end
test "scan/2 with regexes with options compiled in different systems" do
assert Regex.scan(%{~r/foo/i | re_version: "bad version"}, "FOO") == [["FOO"]]
end
test "split/2,3" do
assert Regex.split(~r",", "") == [""]
assert Regex.split(~r",", "", trim: true) == []
+32 -13
View File
@@ -75,6 +75,14 @@ defmodule TypespecTest do
@type my_type :: %URI.t(){}
end
end
assert_raise Kernel.TypespecError,
~r"unexpected expression in typespec: t\.Foo",
fn ->
test_module do
@type my_type :: t.Foo
end
end
end
test "invalid function specification" do
@@ -120,7 +128,7 @@ defmodule TypespecTest do
test "redefined type" do
assert_raise Kernel.TypespecError,
~r"type foo/0 is already defined in .*test/elixir/typespec_test.exs:126",
~r"type foo/0 is already defined in .*test/elixir/typespec_test.exs:134",
fn ->
test_module do
@type foo :: atom
@@ -129,7 +137,7 @@ defmodule TypespecTest do
end
assert_raise Kernel.TypespecError,
~r"type foo/2 is already defined in .*test/elixir/typespec_test.exs:136",
~r"type foo/2 is already defined in .*test/elixir/typespec_test.exs:144",
fn ->
test_module do
@type foo :: atom
@@ -139,7 +147,7 @@ defmodule TypespecTest do
end
assert_raise Kernel.TypespecError,
~r"type foo/0 is already defined in .*test/elixir/typespec_test.exs:145",
~r"type foo/0 is already defined in .*test/elixir/typespec_test.exs:153",
fn ->
test_module do
@type foo :: atom
@@ -385,6 +393,7 @@ defmodule TypespecTest do
@type size :: <<_::3>>
@type unit :: <<_::_*8>>
@type size_and_unit :: <<_::3, _::_*8>>
@type size_prod_unit :: <<_::3*8>>
end
assert [
@@ -393,17 +402,15 @@ defmodule TypespecTest do
type: {:size, {:type, _, :binary, [{:integer, _, 3}, {:integer, _, 0}]}, []},
type:
{:size_and_unit, {:type, _, :binary, [{:integer, _, 3}, {:integer, _, 8}]}, []},
type:
{:size_prod_unit,
{:type, _, :binary,
[{:op, _, :*, {:integer, _, 3}, {:integer, _, 8}}, {:integer, _, 0}]}, []},
type: {:unit, {:type, _, :binary, [{:integer, _, 0}, {:integer, _, 8}]}, []}
] = types(bytecode)
end
test "@type with invalid binary spec" do
assert_raise Kernel.TypespecError, ~r"invalid binary specification", fn ->
test_module do
@type my_type :: <<_::3*8>>
end
end
assert_raise Kernel.TypespecError, ~r"invalid binary specification", fn ->
test_module do
@type my_type :: <<_::atom()>>
@@ -710,7 +717,8 @@ defmodule TypespecTest do
@type my_type :: (... -> any)
end
assert [type: {:my_type, {:type, _, :fun, []}, []}] = types(bytecode)
assert [type: {:my_type, {:type, _, :fun, [{:type, _, :any}, {:type, _, :any, []}]}, []}] =
types(bytecode)
end
test "@type with a fun with multiple arguments and return type" do
@@ -840,6 +848,19 @@ defmodule TypespecTest do
assert [{:atom, _, Keyword}, {:atom, _, :t}, [{:var, _, :value}]] = kw_with_value_args
end
test "@type with macro in alias" do
bytecode =
test_module do
defmacro module() do
quote do: __MODULE__
end
@type my_type :: module().Foo
end
assert [type: {:my_type, {:atom, _, TypespecTest.TypespecSample.Foo}, []}] = types(bytecode)
end
test "@type with a reserved signature" do
assert_raise Kernel.TypespecError,
~r"type required\/1 is a reserved type and it cannot be defined",
@@ -1195,6 +1216,7 @@ defmodule TypespecTest do
quote(do: @type(binary_type1() :: <<_::_*8>>)),
quote(do: @type(binary_type2() :: <<_::3>>)),
quote(do: @type(binary_type3() :: <<_::3, _::_*8>>)),
quote(do: @type(binary_type4() :: <<_::3*8>>)),
quote(do: @type(tuple_type() :: {integer()})),
quote(do: @type(ftype() :: (-> any()) | (-> integer()) | (integer() -> integer()))),
quote(do: @type(cl() :: charlist())),
@@ -1520,9 +1542,6 @@ defmodule TypespecTest do
assert ast_string ==
"@type literal_struct_all_fields_key_type() :: %TypespecTest.SomeStruct{key: integer()}"
{:built_in_fun, _, _} ->
assert ast_string == "@type built_in_fun() :: (... -> any())"
{:built_in_nonempty_list, _, _} ->
assert ast_string == "@type built_in_nonempty_list() :: [...]"
+1 -1
View File
@@ -786,7 +786,7 @@ defmodule ExUnit.Assertions do
## Examples
assert_raise ArithmeticError, "bad argument in arithmetic expression", fn ->
1 + "test"
1 / 0
end
assert_raise RuntimeError, ~r/^today's lucky number is 0\.\d+!$/, fn ->
+10 -1
View File
@@ -315,6 +315,11 @@ defmodule ExUnit.Case do
end
end
@keys [:async, :group, :parameterize, :register]
@doc false
def __keys__(opts), do: Keyword.take(opts, @keys)
@doc false
def __register__(module, opts) do
if not Keyword.keyword?(opts) do
@@ -324,7 +329,7 @@ defmodule ExUnit.Case do
end
{register?, opts} = Keyword.pop(opts, :register, true)
{next_opts, opts} = Keyword.split(opts, [:async, :group, :parameterize])
{next_opts, opts} = Keyword.split(opts, @keys)
if opts != [] do
IO.warn("unknown options given to ExUnit.Case: #{inspect(opts)}")
@@ -561,6 +566,10 @@ defmodule ExUnit.Case do
group = Keyword.get(opts, :group, nil)
parameterize = Keyword.get(opts, :parameterize, nil)
if not is_boolean(async?) do
raise ArgumentError, ":async must be a boolean, got: #{inspect(async?)}"
end
if not (parameterize == nil or (is_list(parameterize) and Enum.all?(parameterize, &is_map/1))) do
raise ArgumentError, ":parameterize must be a list of maps, got: #{inspect(parameterize)}"
end
+1 -1
View File
@@ -83,7 +83,7 @@ defmodule ExUnit.CaseTemplate do
# We inject this code in the module that calls "use MyTemplate".
def __proxy__(module, opts) do
quote do
use ExUnit.Case, unquote(opts)
use ExUnit.Case, ExUnit.Case.__keys__(unquote(opts))
setup_all context do
unquote(module).__ex_unit__(:setup_all, context)
+3 -1
View File
@@ -878,7 +878,8 @@ defmodule ExUnit.Diff do
end
defp rebuild_split_strings(%{contents: contents, delimiter: delimiter}, right) do
%{contents: contents ++ [{false, right}], delimiter: delimiter}
{new_right, diff} = extract_diff_meta(right)
%{contents: contents ++ [{diff, new_right}], delimiter: delimiter}
end
defp rebuild_concat_string(literal, nil, []) do
@@ -1155,6 +1156,7 @@ defmodule ExUnit.Diff do
else
other
|> Map.to_list()
|> Enum.map(&escape_pair/1)
|> build_map_or_struct(struct)
end
end
+1 -3
View File
@@ -149,8 +149,6 @@ defmodule ExUnit.DocTest do
suite run.
"""
@opaque_type_regex ~r/#[\w\.]+</
defmodule Error do
@moduledoc """
Exception raised when there's an error with the syntax or semantics of a doctest.
@@ -554,7 +552,7 @@ defmodule ExUnit.DocTest do
message = "Doctest did not compile, got: #{ex_message}"
message =
if e.__struct__ == TokenMissingError and expr =~ @opaque_type_regex do
if e.__struct__ == TokenMissingError and expr =~ ~r/#[\w\.]+</ do
message <>
"""
\nIf you are planning to assert on the result of an iex> expression \
+2 -2
View File
@@ -125,7 +125,7 @@ defmodule ExUnit.Runner do
# Run all sync modules directly
for pair <- sync_modules do
running = spawn_modules(config, [[pair]], false, %{})
running = spawn_modules(config, [{nil, [pair]}], false, %{})
running != %{} and wait_until_available(config, running)
end
@@ -161,7 +161,7 @@ defmodule ExUnit.Runner do
running
end
defp spawn_modules(config, [[_ | _] = modules | groups], async?, running) do
defp spawn_modules(config, [{_group, [_ | _] = modules} | groups], async?, running) do
if max_failures_reached?(config) do
running
else
+55 -63
View File
@@ -57,9 +57,10 @@ defmodule ExUnit.Server do
state = %{
loaded: System.monotonic_time(),
waiting: nil,
async_groups: %{},
groups: %{},
async_groups: [],
async_modules: :queue.new(),
sync_modules: :queue.new()
sync_modules: []
}
{:ok, state}
@@ -72,31 +73,31 @@ defmodule ExUnit.Server do
# Called once after all async modules have been sent and reverts the state.
def handle_call(:take_sync_modules, _from, state) do
%{waiting: nil, loaded: :done, async_modules: async_modules} = state
0 = :queue.len(async_modules)
%{waiting: nil, loaded: :done, async_groups: []} = state
true = :queue.is_empty(state.async_modules)
{:reply, :queue.to_list(state.sync_modules),
%{state | sync_modules: :queue.new(), loaded: System.monotonic_time()}}
{:reply, state.sync_modules, %{state | sync_modules: [], loaded: System.monotonic_time()}}
end
# Called by the runner when --repeat-until-failure is used.
def handle_call({:restore_modules, async_modules, sync_modules}, _from, state) do
{async_modules, async_groups} =
Enum.map_reduce(async_modules, %{}, fn
{nil, [module]}, {modules, groups} ->
{[{:module, module} | modules], groups}
{async_modules, async_groups, groups} =
Enum.reduce(async_modules, {[], [], []}, fn
{nil, [module]}, {async_modules, async_groups, groups} ->
{[module | async_modules], async_groups, groups}
{group, group_modules}, {modules, groups} ->
{[{:group, group} | modules], Map.put(groups, group, group_modules)}
{group, group_modules}, {async_modules, async_groups, groups} ->
{async_modules, [group | async_groups], [{group, group_modules} | groups]}
end)
{:reply, :ok,
%{
state
| loaded: :done,
groups: Map.new(groups),
async_groups: async_groups,
async_modules: :queue.from_list(async_modules),
sync_modules: :queue.from_list(sync_modules)
sync_modules: sync_modules
}}
end
@@ -108,22 +109,24 @@ defmodule ExUnit.Server do
when is_integer(loaded) do
state =
if uniq? do
async_groups =
Map.new(state.async_groups, fn {group, modules} ->
{group, Enum.uniq(modules)}
end)
groups = Map.new(state.groups, fn {group, modules} -> {group, Enum.uniq(modules)} end)
async_groups = state.async_groups |> Enum.uniq() |> Enum.reverse()
async_modules = :queue.to_list(state.async_modules) |> Enum.uniq() |> :queue.from_list()
sync_modules = :queue.to_list(state.sync_modules) |> Enum.uniq() |> :queue.from_list()
sync_modules = state.sync_modules |> Enum.uniq() |> Enum.reverse()
%{
state
| async_groups: async_groups,
| groups: groups,
async_groups: async_groups,
async_modules: async_modules,
sync_modules: sync_modules
}
else
state
%{
state
| async_groups: Enum.reverse(state.async_groups),
sync_modules: Enum.reverse(state.sync_modules)
}
end
diff = System.convert_time_unit(System.monotonic_time() - loaded, :native, :microsecond)
@@ -132,9 +135,7 @@ defmodule ExUnit.Server do
def handle_call({:add, false = _async, _group, names}, _from, %{loaded: loaded} = state)
when is_integer(loaded) do
state =
update_in(state.sync_modules, &Enum.reduce(names, &1, fn name, q -> :queue.in(name, q) end))
state = update_in(state.sync_modules, &Enum.reverse(names, &1))
{:reply, :ok, state}
end
@@ -143,7 +144,7 @@ defmodule ExUnit.Server do
state =
update_in(
state.async_modules,
&Enum.reduce(names, &1, fn name, q -> :queue.in({:module, name}, q) end)
&Enum.reduce(names, &1, fn name, q -> :queue.in(name, q) end)
)
{:reply, :ok, take_modules(state)}
@@ -151,17 +152,16 @@ defmodule ExUnit.Server do
def handle_call({:add, true = _async, group, names}, _from, %{loaded: loaded} = state)
when is_integer(loaded) do
{async_groups, async_modules} =
case state.async_groups do
%{^group => entries} = async_groups ->
{%{async_groups | group => names ++ entries}, state.async_modules}
{groups, async_groups} =
case state.groups do
%{^group => entries} = groups ->
{%{groups | group => Enum.reverse(names, entries)}, state.async_groups}
%{} = async_groups ->
{Map.put(async_groups, group, names), :queue.in({:group, group}, state.async_modules)}
%{} = groups ->
{Map.put(groups, group, names), [group | state.async_groups]}
end
{:reply, :ok,
take_modules(%{state | async_groups: async_groups, async_modules: async_modules})}
{:reply, :ok, take_modules(%{state | groups: groups, async_groups: async_groups})}
end
def handle_call({:add, _async?, _group, _names}, _from, state) do
@@ -173,50 +173,42 @@ defmodule ExUnit.Server do
end
defp take_modules(%{waiting: {from, count}} = state) do
has_async_modules? = not :queue.is_empty(state.async_modules)
cond do
not has_async_modules? and state.loaded == :done ->
not :queue.is_empty(state.async_modules) ->
{reply, remaining_modules} = take_until(count, state.async_modules)
GenServer.reply(from, reply)
%{state | async_modules: remaining_modules, waiting: nil}
state.async_groups != [] and state.loaded == :done ->
{groups, remaining_groups} = Enum.split(state.async_groups, count)
{reply, groups} =
Enum.map_reduce(groups, state.groups, fn group, acc ->
{entries, acc} = Map.pop!(acc, group)
{{group, Enum.reverse(entries)}, acc}
end)
GenServer.reply(from, reply)
%{state | groups: groups, async_groups: remaining_groups, waiting: nil}
state.loaded == :done ->
GenServer.reply(from, nil)
%{state | waiting: nil}
not has_async_modules? ->
state
true ->
{async_modules, remaining_modules} = take_until(count, state.async_modules)
{async_modules, remaining_groups} =
Enum.map_reduce(async_modules, state.async_groups, fn
{:module, module}, async_groups ->
{[module], async_groups}
{:group, group}, async_groups ->
{group_modules, async_groups} = Map.pop!(async_groups, group)
{Enum.reverse(group_modules), async_groups}
end)
GenServer.reply(from, async_modules)
%{
state
| async_groups: remaining_groups,
async_modules: remaining_modules,
waiting: nil
}
state
end
end
# :queue.split fails if the provided count is larger than the queue size;
# as we also want to return the values as a list later, we directly
# return {list, queue} instead of {queue, queue}
# :queue.split fails if the provided count is larger than the queue size.
# We also want to return the values as tuples of shape {group, [modules]}.
defp take_until(n, queue), do: take_until(n, queue, [])
defp take_until(0, queue, acc), do: {Enum.reverse(acc), queue}
defp take_until(n, queue, acc) do
case :queue.out(queue) do
{{:value, item}, queue} -> take_until(n - 1, queue, [item | acc])
{{:value, item}, queue} -> take_until(n - 1, queue, [{nil, [item]} | acc])
{:empty, queue} -> {Enum.reverse(acc), queue}
end
end

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