Compare commits
110
Commits
v1.19.0-rc.2
...
v1.18.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e35ffc5a90 | ||
|
|
178643f9eb | ||
|
|
75677b9dab | ||
|
|
25ab648502 | ||
|
|
8493f1934a | ||
|
|
c63aeb9f77 | ||
|
|
d89fabe5e4 | ||
|
|
d51153b56e | ||
|
|
69990a5d1d | ||
|
|
930ec69739 | ||
|
|
d7478095e0 | ||
|
|
b823f9efdf | ||
|
|
6ecb430614 | ||
|
|
b615c8435a | ||
|
|
120467f83c | ||
|
|
4b09a836e0 | ||
|
|
55c05d943e | ||
|
|
8e61baacab | ||
|
|
7426acb1b9 | ||
|
|
175c8243b2 | ||
|
|
2c1a836db3 | ||
|
|
f29e18bca1 | ||
|
|
329442c481 | ||
|
|
01a88e7137 | ||
|
|
4def31f8ab | ||
|
|
88c75bcbf5 | ||
|
|
786f3ce797 | ||
|
|
98aeee34b6 | ||
|
|
6f7eaf1122 | ||
|
|
9593cef2f5 | ||
|
|
c78f426bf5 | ||
|
|
ff29628602 | ||
|
|
f5e42ba11a | ||
|
|
b0dae83353 | ||
|
|
40da7e8857 | ||
|
|
ac910c1cc2 | ||
|
|
5c340e488e | ||
|
|
defef37856 | ||
|
|
432d6fa1c2 | ||
|
|
24714c68a7 | ||
|
|
f088fc9cdb | ||
|
|
dddb8f7b19 | ||
|
|
2da3300bfc | ||
|
|
c7a841baae | ||
|
|
f3dfb72c69 | ||
|
|
7e149619f5 | ||
|
|
6dbb932bf3 | ||
|
|
0cb79b0ba1 | ||
|
|
265b9aaeb3 | ||
|
|
b9bd0e3dd9 | ||
|
|
6a3301f237 | ||
|
|
4911916f62 | ||
|
|
546a0db392 | ||
|
|
463f1aa593 | ||
|
|
a3a632efd1 | ||
|
|
b7a5fd7b56 | ||
|
|
04378bd9a6 | ||
|
|
fae36c5e49 | ||
|
|
712f24af0a | ||
|
|
8c9f303e37 | ||
|
|
1a2be16109 | ||
|
|
4adaac702e | ||
|
|
a1be4fbc86 | ||
|
|
d4e6a558cb | ||
|
|
4e3203f19a | ||
|
|
4a5fb1eb66 | ||
|
|
0fe6f68c34 | ||
|
|
5aa049ccb3 | ||
|
|
e4c86d4b5f | ||
|
|
e5033c94ce | ||
|
|
5d65d60721 | ||
|
|
3564c68cdc | ||
|
|
9df42f5a0a | ||
|
|
c6597bcb20 | ||
|
|
ca6edfd389 | ||
|
|
ed83c407a5 | ||
|
|
a426520b66 | ||
|
|
1c0f585f40 | ||
|
|
7c86c4bdb3 | ||
|
|
4931ab40a5 | ||
|
|
c7f78e8015 | ||
|
|
6a24bafbda | ||
|
|
8ddef06e81 | ||
|
|
472c4e1a24 | ||
|
|
305d8e603c | ||
|
|
1c7fc86e9c | ||
|
|
617dc72759 | ||
|
|
4329a3c2f2 | ||
|
|
8e8c9a7334 | ||
|
|
36c06c3a56 | ||
|
|
048cc2ddb1 | ||
|
|
048ae58d6d | ||
|
|
85e00cddd5 | ||
|
|
6c866e8ab2 | ||
|
|
55eca5dff5 | ||
|
|
070a6d51bf | ||
|
|
9c28fb7a39 | ||
|
|
cda1a09c58 | ||
|
|
3ca0bddd9d | ||
|
|
e07a91594b | ||
|
|
dfc659104c | ||
|
|
a4fa71ab23 | ||
|
|
2273bafb47 | ||
|
|
a78f1715dc | ||
|
|
b709865976 | ||
|
|
da7e04a540 | ||
|
|
66c5908619 | ||
|
|
31d8da6c77 | ||
|
|
3bdf5a912c | ||
|
|
0e9b5ec070 |
@@ -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
|
||||
|
||||
+131
-27
@@ -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,101 @@ 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.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 +332,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 +361,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 +385,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 +436,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,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
@@ -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
@@ -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
|
||||
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.18.0-dev
|
||||
ELIXIR_VERSION=1.18.3
|
||||
|
||||
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
@@ -1,6 +1,6 @@
|
||||
@echo off
|
||||
|
||||
set ELIXIR_VERSION=1.18.0-dev
|
||||
set ELIXIR_VERSION=1.18.3
|
||||
|
||||
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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
+34
-35
@@ -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))
|
||||
@@ -4066,7 +4066,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 +4584,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 +5463,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 +6437,37 @@ 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, ®ex_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, ®ex_unescape_map/1)
|
||||
compile_regex(binary, options)
|
||||
end
|
||||
|
||||
defmacro sigil_r({:<<>>, meta, pieces}, options) do
|
||||
binary = {:<<>>, meta, unescape_tokens(pieces, ®ex_unescape_map/1)}
|
||||
quote(do: Regex.compile!(unquote(binary), unquote(:binary.list_to_bin(options))))
|
||||
tuple = {:<<>>, meta, unescape_tokens(pieces, ®ex_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
|
||||
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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -257,28 +257,13 @@ 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)}
|
||||
spawn_workers(schedulers, cache, files, output, options)
|
||||
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}, _} ->
|
||||
{: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} ->
|
||||
{:error, errors, info}
|
||||
after
|
||||
Module.ParallelChecker.stop(cache)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1316,11 +1329,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 +1703,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 +1716,55 @@ 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 ->
|
||||
case Enum.split_while(acc, &non_fusible_maps?(map, &1)) do
|
||||
{_, []} ->
|
||||
[map | acc]
|
||||
|
||||
{others, [match | rest]} ->
|
||||
fused = map_non_negated_fuse_pair(map, match)
|
||||
others ++ [fused | rest]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# Two maps are fusible if they differ in at most one element.
|
||||
defp non_fusible_maps?({_, fields1, []}, {_, fields2, []}) do
|
||||
Enum.count_until(fields1, fn {key, value} -> Map.fetch!(fields2, key) != value end, 2) > 1
|
||||
end
|
||||
|
||||
defp map_non_negated_fuse_pair({tag, fields1, []}, {_, fields2, []}) do
|
||||
fields =
|
||||
symmetrical_merge(fields1, fields2, fn _k, v1, v2 ->
|
||||
if v1 == v2, do: v1, else: union(v1, v2)
|
||||
end)
|
||||
|
||||
{tag, fields, []}
|
||||
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 +1792,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 +1878,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
|
||||
@@ -1922,37 +2001,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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 \\ [])
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
@@ -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 ::
|
||||
|
||||
@@ -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>
|
||||
"""
|
||||
|
||||
_ ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -203,6 +203,7 @@ bootstrap_files() ->
|
||||
<<"path.ex">>,
|
||||
<<"file.ex">>,
|
||||
<<"map.ex">>,
|
||||
<<"function.ex">>,
|
||||
<<"range.ex">>,
|
||||
<<"access.ex">>,
|
||||
<<"io.ex">>,
|
||||
|
||||
@@ -314,7 +314,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);
|
||||
|
||||
@@ -165,7 +165,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 +531,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]
|
||||
},
|
||||
|
||||
@@ -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].
|
||||
|
||||
@@ -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]}]},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) ->
|
||||
[].
|
||||
|
||||
@@ -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__()]")
|
||||
|
||||
@@ -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"}]
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("""
|
||||
|
||||
@@ -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}}}
|
||||
|
||||
@@ -163,6 +163,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 +1187,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 +1202,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 +1259,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 +1271,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 +1404,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{}
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,39 +3,6 @@ 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
|
||||
|
||||
test "multiline" do
|
||||
@@ -68,16 +35,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 +76,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 +129,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 +147,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) == []
|
||||
|
||||
@@ -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() :: [...]"
|
||||
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -707,7 +707,7 @@ defmodule ExUnit.AssertionsTest do
|
||||
rescue
|
||||
error in [ExUnit.AssertionError] ->
|
||||
"foo" = error.left
|
||||
~r{a} = error.right
|
||||
%Regex{} = error.right
|
||||
end
|
||||
end
|
||||
|
||||
@@ -722,7 +722,7 @@ defmodule ExUnit.AssertionsTest do
|
||||
rescue
|
||||
error in [ExUnit.AssertionError] ->
|
||||
"foo" = error.left
|
||||
~r"o" = error.right
|
||||
%Regex{} = error.right
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ defmodule ExUnit.NestedCase do
|
||||
end
|
||||
|
||||
defmodule ExUnit.CaseTemplateTest do
|
||||
use ExUnit.SampleCase, async: true
|
||||
use ExUnit.SampleCase, async: true, another_option: 123
|
||||
use ExUnit.NestedCase
|
||||
|
||||
two = 2
|
||||
|
||||
@@ -330,6 +330,8 @@ defmodule ExUnit.DiffTest do
|
||||
"[[[[], \"Hello-,- \"] | \"world\"] | \"!\"]",
|
||||
"[[[[], \"Hello \"] | \"world\"] | \"!\"]"
|
||||
)
|
||||
|
||||
refute_diff(:foo = %{bar: [:a | :b]}, "", "")
|
||||
end
|
||||
|
||||
test "proper lists" do
|
||||
@@ -1133,6 +1135,12 @@ defmodule ExUnit.DiffTest do
|
||||
"-<<trap::binary-size(3)>> <> \"baz\"-",
|
||||
"+\"foobar\"+"
|
||||
)
|
||||
|
||||
refute_diff(
|
||||
"hello " <> <<_::binary-size(6)>> = "hello world",
|
||||
"\"hello \" <> -<<_::binary-size(6)>>-",
|
||||
"\"hello +world+\""
|
||||
)
|
||||
end
|
||||
|
||||
test "underscore" do
|
||||
|
||||
@@ -503,6 +503,17 @@ defmodule ExUnit.FormatterTest do
|
||||
"""
|
||||
end
|
||||
|
||||
test "formats assertions with nested improper list diffing" do
|
||||
failure = [{:error, catch_assertion(assert :foo = %{bar: [1 | 2]}), []}]
|
||||
|
||||
assert format_test_all_failure(test_module(), failure, 1, :infinity, &diff_formatter/2) =~ """
|
||||
match (=) failed
|
||||
code: assert :foo = %{bar: [1 | 2]}
|
||||
left: :foo
|
||||
right: %{bar: [1 | 2]}
|
||||
"""
|
||||
end
|
||||
|
||||
defmodule BadInspect do
|
||||
defstruct key: 0
|
||||
|
||||
|
||||
@@ -997,6 +997,24 @@ defmodule ExUnitTest do
|
||||
assert length(runs) == 6
|
||||
end
|
||||
|
||||
test "repeats tests up to the configured number of times with groups" do
|
||||
defmodule TestGroupedRepeatUntilFailureReached do
|
||||
use ExUnit.Case, async: true, group: :example
|
||||
test __ENV__.line, do: assert(true)
|
||||
end
|
||||
|
||||
configure_and_reload_on_exit(repeat_until_failure: 5)
|
||||
|
||||
output =
|
||||
capture_io(fn ->
|
||||
assert ExUnit.run() == %{total: 1, failures: 0, skipped: 0, excluded: 0}
|
||||
end)
|
||||
|
||||
runs = String.split(output, "Running ExUnit", trim: true)
|
||||
# 6 runs in total, 5 repeats
|
||||
assert length(runs) == 6
|
||||
end
|
||||
|
||||
test "stops on failure" do
|
||||
{:ok, pid} = Agent.start_link(fn -> 0 end)
|
||||
Process.register(pid, :ex_unit_repeat_until_failure_count)
|
||||
|
||||
+16
-12
@@ -391,16 +391,17 @@ defmodule IEx do
|
||||
|
||||
The supported options are:
|
||||
|
||||
* `:colors`
|
||||
* `:inspect`
|
||||
* `:width`
|
||||
* `:history_size`
|
||||
* `:default_prompt`
|
||||
* `:continuation_prompt`
|
||||
* `:alive_prompt`
|
||||
* `:auto_reload`
|
||||
* `:alive_continuation_prompt`
|
||||
* `:parser`
|
||||
* `:alive_prompt`
|
||||
* `:colors`
|
||||
* `:continuation_prompt`
|
||||
* `:default_prompt`
|
||||
* `:dot_iex`
|
||||
* `:history_size`
|
||||
* `:inspect`
|
||||
* `:parser`
|
||||
* `:width`
|
||||
|
||||
They are discussed individually in the sections below.
|
||||
|
||||
@@ -488,9 +489,6 @@ defmodule IEx do
|
||||
* `:alive_continuation_prompt` - used when `Node.alive?/0` returns
|
||||
`true` and more input is expected
|
||||
|
||||
* `:auto_reload` - when set to `true`, automatically purges in-memory
|
||||
modules when they get invalidated by a concurrent compilation
|
||||
|
||||
The following values in the prompt string will be replaced appropriately:
|
||||
|
||||
* `%counter` - the index of the history
|
||||
@@ -510,10 +508,16 @@ defmodule IEx do
|
||||
|
||||
If the parser raises, the buffer is reset to an empty string.
|
||||
|
||||
## dot_iex
|
||||
## `.iex`
|
||||
|
||||
Configure the file loaded into your IEx session when it starts.
|
||||
See more information [in the `.iex.exs` documentation](`m:IEx#module-the-iex-exs-file`).
|
||||
|
||||
## Auto reloading
|
||||
|
||||
When set to `true`, the `:auto_reload` option automatically purges
|
||||
in-memory modules when they get invalidated by a concurrent compilation
|
||||
happening in the Operating System.
|
||||
"""
|
||||
@spec configure(keyword()) :: :ok
|
||||
def configure(options) do
|
||||
|
||||
@@ -363,7 +363,7 @@ defmodule IEx.Autocomplete do
|
||||
end)
|
||||
|
||||
entries =
|
||||
for {key, _value} <- pairs,
|
||||
for key when key != :__struct__ <- Map.keys(pairs),
|
||||
name = Atom.to_string(key),
|
||||
if(hint == "",
|
||||
do: not String.starts_with?(name, "_"),
|
||||
@@ -378,7 +378,8 @@ defmodule IEx.Autocomplete do
|
||||
case Code.Fragment.container_cursor_to_quoted(code) do
|
||||
{:ok, quoted} ->
|
||||
case Macro.path(quoted, &match?({:__cursor__, _, []}, &1)) do
|
||||
[cursor, {:%{}, _, pairs}, {:%, _, [{:__aliases__, _, aliases}, _map]} | _] ->
|
||||
[cursor, {:%{}, _, pairs}, {:%, _, [{:__aliases__, _, aliases = [h | _]}, _map]} | _]
|
||||
when is_atom(h) ->
|
||||
container_context_struct(cursor, pairs, aliases, shell)
|
||||
|
||||
[
|
||||
@@ -386,8 +387,9 @@ defmodule IEx.Autocomplete do
|
||||
pairs,
|
||||
{:|, _, _},
|
||||
{:%{}, _, _},
|
||||
{:%, _, [{:__aliases__, _, aliases}, _map]} | _
|
||||
] ->
|
||||
{:%, _, [{:__aliases__, _, aliases = [h | _]}, _map]} | _
|
||||
]
|
||||
when is_atom(h) ->
|
||||
container_context_struct(cursor, pairs, aliases, shell)
|
||||
|
||||
[cursor, pairs, {:|, _, [{variable, _, nil} | _]}, {:%{}, _, _} | _] ->
|
||||
|
||||
@@ -12,10 +12,18 @@ defmodule IEx.MixListener do
|
||||
|
||||
@doc """
|
||||
Unloads all modules invalidated by external compilations.
|
||||
|
||||
Returns `:noop` if there is no module to purge or if
|
||||
the listener is not running (it may happen when connecting
|
||||
via --remsh to a node that was started without IEx).
|
||||
"""
|
||||
@spec purge :: :ok | :noop
|
||||
def purge do
|
||||
GenServer.call(@name, :purge, :infinity)
|
||||
if Process.whereis(@name) do
|
||||
GenServer.call(@name, :purge, :infinity)
|
||||
else
|
||||
:noop
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
@@ -416,6 +416,9 @@ defmodule IEx.AutocompleteTest do
|
||||
assert {:yes, ~c"ry: ", []} = expand(~c"%URI{path: \"foo\", que")
|
||||
assert {:no, [], []} = expand(~c"%URI{path: \"foo\", unkno")
|
||||
assert {:no, [], []} = expand(~c"%Unknown{path: \"foo\", unkno")
|
||||
|
||||
assert {:yes, [], _} = expand(~c"%__MODULE__{")
|
||||
assert {:yes, [], _} = expand(~c"%__MODULE__.Some{")
|
||||
end
|
||||
|
||||
test "completion for struct keys in update syntax" do
|
||||
@@ -430,6 +433,19 @@ defmodule IEx.AutocompleteTest do
|
||||
assert {:yes, ~c"ry: ", []} = expand(~c"%URI{var | path: \"foo\", que")
|
||||
assert {:no, [], []} = expand(~c"%URI{var | path: \"foo\", unkno")
|
||||
assert {:no, [], []} = expand(~c"%Unknown{var | path: \"foo\", unkno")
|
||||
|
||||
eval("var = %URI{}")
|
||||
|
||||
assert {:yes, ~c"", entries} = expand(~c"%{var | ")
|
||||
assert ~c"path:" in entries
|
||||
assert ~c"query:" in entries
|
||||
|
||||
assert {:yes, ~c"", entries} = expand(~c"%{var | path: \"foo\",")
|
||||
assert ~c"path:" not in entries
|
||||
assert ~c"query:" in entries
|
||||
|
||||
assert {:yes, ~c"ry: ", []} = expand(~c"%{var | path: \"foo\", que")
|
||||
assert {:no, [], []} = expand(~c"%URI{var | path: \"foo\", unkno")
|
||||
end
|
||||
|
||||
test "completion for map keys in update syntax" do
|
||||
|
||||
@@ -349,7 +349,7 @@ defmodule IEx.HelpersTest do
|
||||
assert captured =~ "-spec sleep(Time) -> ok when Time :: timeout()."
|
||||
else
|
||||
assert captured =~ "sleep(Time)"
|
||||
assert captured =~ "@spec sleep(time) :: :ok when time: timeout()"
|
||||
assert captured =~ "@spec sleep(time) :: :ok when time: "
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -352,6 +352,11 @@ defmodule Mix do
|
||||
* `MIX_INSTALL_DIR` *(since v1.12.0)* - specifies directory where `Mix.install/2` keeps
|
||||
install cache
|
||||
|
||||
* `MIX_OS_CONCURRENCY_LOCK` - when set to `0` or `false`, disables mix compilation locking.
|
||||
While not recommended, this may be necessary in cases where hard links or TCP sockets are
|
||||
not available. When opting for this behaviour, make sure to not start concurrent compilations
|
||||
of the same project.
|
||||
|
||||
* `MIX_PATH` - appends extra code paths
|
||||
|
||||
* `MIX_PROFILE` - a list of comma-separated Mix tasks to profile the time spent on
|
||||
|
||||
@@ -208,14 +208,18 @@ defmodule Mix.Compilers.Elixir do
|
||||
end
|
||||
|
||||
Mix.Task.Compiler.notify_modules_compiled(lazy_modules_diff)
|
||||
|
||||
unless_previous_warnings_as_errors(previous_warnings, opts, {:ok, all_warnings})
|
||||
unless_warnings_as_errors(opts, {:ok, all_warnings})
|
||||
|
||||
{:error, errors, %{runtime_warnings: r_warnings, compile_warnings: c_warnings}, state} ->
|
||||
{errors, warnings} =
|
||||
if opts[:warnings_as_errors],
|
||||
do: {errors ++ r_warnings ++ c_warnings, []},
|
||||
else: {errors, r_warnings ++ c_warnings}
|
||||
|
||||
# In case of errors, we show all previous warnings and all new ones.
|
||||
{_, _, sources, _, _, _} = state
|
||||
errors = Enum.map(errors, &diagnostic/1)
|
||||
warnings = Enum.map(r_warnings ++ c_warnings, &diagnostic/1)
|
||||
warnings = Enum.map(warnings, &diagnostic/1)
|
||||
all_warnings = Keyword.get(opts, :all_warnings, errors == [])
|
||||
{:error, previous_warnings(sources, all_warnings) ++ warnings ++ errors}
|
||||
after
|
||||
@@ -254,8 +258,7 @@ defmodule Mix.Compilers.Elixir do
|
||||
|
||||
all_warnings = Keyword.get(opts, :all_warnings, true)
|
||||
previous_warnings = previous_warnings(sources, all_warnings)
|
||||
|
||||
unless_previous_warnings_as_errors(previous_warnings, opts, {status, previous_warnings})
|
||||
unless_warnings_as_errors(opts, {status, previous_warnings})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1012,8 +1015,8 @@ defmodule Mix.Compilers.Elixir do
|
||||
File.rm(manifest <> ".checkpoint")
|
||||
end
|
||||
|
||||
defp unless_previous_warnings_as_errors(previous_warnings, opts, {status, all_warnings}) do
|
||||
if previous_warnings != [] and opts[:warnings_as_errors] do
|
||||
defp unless_warnings_as_errors(opts, {status, all_warnings}) do
|
||||
if all_warnings != [] and opts[:warnings_as_errors] do
|
||||
message = "Compilation failed due to warnings while using the --warnings-as-errors option"
|
||||
IO.puts(:stderr, message)
|
||||
{:error, all_warnings}
|
||||
@@ -1049,7 +1052,6 @@ defmodule Mix.Compilers.Elixir do
|
||||
threshold = opts[:long_compilation_threshold] || 10
|
||||
profile = opts[:profile]
|
||||
verbose = opts[:verbose] || false
|
||||
warnings_as_errors = opts[:warnings_as_errors] || false
|
||||
|
||||
pid =
|
||||
spawn_link(fn ->
|
||||
@@ -1071,8 +1073,7 @@ defmodule Mix.Compilers.Elixir do
|
||||
long_compilation_threshold: threshold,
|
||||
profile: profile,
|
||||
beam_timestamp: timestamp,
|
||||
return_diagnostics: true,
|
||||
warnings_as_errors: warnings_as_errors
|
||||
return_diagnostics: true
|
||||
]
|
||||
|
||||
response = Kernel.ParallelCompiler.compile_to_path(stale, dest, compile_opts)
|
||||
|
||||
@@ -917,7 +917,10 @@ defmodule Mix.Project do
|
||||
build_path = build_path(config)
|
||||
|
||||
on_taken = fn os_pid ->
|
||||
Mix.shell().info("Waiting for lock on the build directory (held by process #{os_pid})")
|
||||
Mix.shell().error([
|
||||
IO.ANSI.reset(),
|
||||
"Waiting for lock on the build directory (held by process #{os_pid})"
|
||||
])
|
||||
end
|
||||
|
||||
Mix.Sync.Lock.with_lock(build_path, fun, on_taken: on_taken)
|
||||
@@ -931,7 +934,10 @@ defmodule Mix.Project do
|
||||
deps_path = deps_path(config)
|
||||
|
||||
on_taken = fn os_pid ->
|
||||
Mix.shell().info("Waiting for lock on the deps directory (held by process #{os_pid})")
|
||||
Mix.shell().error([
|
||||
IO.ANSI.reset(),
|
||||
"Waiting for lock on the deps directory (held by process #{os_pid})"
|
||||
])
|
||||
end
|
||||
|
||||
Mix.Sync.Lock.with_lock(deps_path, fun, on_taken: on_taken)
|
||||
|
||||
@@ -126,17 +126,17 @@ defmodule Mix.SCM.Git do
|
||||
update_origin(opts[:git])
|
||||
|
||||
# Fetch external data
|
||||
rev = get_lock_rev(opts[:lock], opts) || get_opts_rev(opts)
|
||||
lock_rev = get_lock_rev(opts[:lock], opts)
|
||||
|
||||
["--git-dir=.git", "fetch", "--force", "--quiet"]
|
||||
|> Kernel.++(progress_switch(git_version()))
|
||||
|> Kernel.++(tags_switch(opts[:tag]))
|
||||
|> Kernel.++(depth_switch(opts[:depth]))
|
||||
|> Kernel.++(refspec_switch(opts, rev))
|
||||
|> Kernel.++(refspec_switch(opts, lock_rev || get_opts_rev(opts)))
|
||||
|> git!()
|
||||
|
||||
# Migrate the Git repo
|
||||
rev = rev || default_branch()
|
||||
rev = lock_rev || get_origin_opts_rev(opts) || default_branch()
|
||||
git!(["--git-dir=.git", "checkout", "--quiet", rev])
|
||||
|
||||
if opts[:submodules] do
|
||||
@@ -314,6 +314,14 @@ defmodule Mix.SCM.Git do
|
||||
opts[:branch] || opts[:ref] || opts[:tag]
|
||||
end
|
||||
|
||||
defp get_origin_opts_rev(opts) do
|
||||
if branch = opts[:branch] do
|
||||
"origin/#{branch}"
|
||||
else
|
||||
opts[:ref] || opts[:tag]
|
||||
end
|
||||
end
|
||||
|
||||
defp redact_uri(git) do
|
||||
case URI.parse(git) do
|
||||
%{userinfo: nil} -> git
|
||||
|
||||
@@ -100,12 +100,12 @@ defmodule Mix.Shell.IO do
|
||||
"""
|
||||
def cmd(command, opts \\ []) do
|
||||
print_app? = Keyword.get(opts, :print_app, true)
|
||||
windows? = match?({:win32, _}, :os.type())
|
||||
|
||||
Mix.Shell.cmd(command, opts, fn data ->
|
||||
if print_app?, do: print_app()
|
||||
# Due to encoding of shell command on Windows,
|
||||
# let's write the data as is
|
||||
IO.binwrite(data)
|
||||
# Due to encoding of shell command on Windows, write the data as is.
|
||||
if windows?, do: IO.binwrite(data), else: IO.write(data)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -79,6 +79,10 @@ defmodule Mix.Sync.Lock do
|
||||
This function can also be called if this process already has the
|
||||
lock. In such case the function is executed immediately.
|
||||
|
||||
When the `MIX_OS_CONCURRENCY_LOCK` environment variable is set to
|
||||
a falsy value, the lock is ignored and the function is executed
|
||||
immediately.
|
||||
|
||||
## Options
|
||||
|
||||
* `:on_taken` - a one-arity function called if the lock is held
|
||||
@@ -93,12 +97,12 @@ defmodule Mix.Sync.Lock do
|
||||
opts = Keyword.validate!(opts, [:on_taken])
|
||||
|
||||
hash = key |> :erlang.md5() |> Base.url_encode64(padding: false)
|
||||
path = Path.join([System.tmp_dir!(), "mix_lock", hash])
|
||||
path = Path.join(base_path(), hash)
|
||||
|
||||
pdict_key = {__MODULE__, path}
|
||||
has_lock? = Process.get(pdict_key)
|
||||
has_lock? = Process.get(pdict_key, false)
|
||||
|
||||
if has_lock? do
|
||||
if has_lock? or lock_disabled?() do
|
||||
fun.()
|
||||
else
|
||||
lock = lock(path, opts[:on_taken])
|
||||
@@ -115,6 +119,14 @@ defmodule Mix.Sync.Lock do
|
||||
end
|
||||
end
|
||||
|
||||
defp base_path do
|
||||
# We include user in the dir to avoid permission conflicts across users
|
||||
user = System.get_env("USER", "default")
|
||||
Path.join(System.tmp_dir!(), "mix_lock_#{Base.url_encode64(user, padding: false)}")
|
||||
end
|
||||
|
||||
defp lock_disabled?(), do: System.get_env("MIX_OS_CONCURRENCY_LOCK") in ~w(0 false)
|
||||
|
||||
defp lock(path, on_taken) do
|
||||
File.mkdir_p!(path)
|
||||
|
||||
@@ -198,11 +210,12 @@ defmodule Mix.Sync.Lock do
|
||||
:invalidated
|
||||
|
||||
{:error, reason} ->
|
||||
raise File.LinkError,
|
||||
reason: reason,
|
||||
action: "create hard link",
|
||||
existing: port_path,
|
||||
new: lock_path
|
||||
Mix.raise("""
|
||||
could not create hard link from #{port_path} to "#{lock_path}: #{:file.format_error(reason)}.
|
||||
|
||||
Hard link support is required for Mix compilation locking. If your system \
|
||||
does not support hard links, set MIX_OS_CONCURRENCY_LOCK=0\
|
||||
""")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user