Compare commits

...
Author SHA1 Message Date
José Valim 6a3301f237 Release v1.18.1 2024-12-24 10:17:42 +01:00
José Valim 4911916f62 Do not warn when comparing literals 2024-12-24 10:02:29 +01:00
José Valim 546a0db392 Use division by zero to show exception
Closes #14111.
2024-12-24 09:42:21 +01:00
José Valim 463f1aa593 Do not recompile if compilation fails due to --warnings-as-errors 2024-12-23 18:27:11 +01:00
José Valim a3a632efd1 Improve warning as errors deprecation notice 2024-12-23 18:26:37 +01:00
José Valim b7a5fd7b56 Improve container_cursor_to_quoted with trailing fragment, closes #14087 2024-12-23 12:24:03 +01:00
Jonatan Kłosko 04378bd9a6 Add build lock to deps.loadpaths (#14108) 2024-12-23 10:56:17 +01:00
José Valim fae36c5e49 Queue async groups until state is loaded 2024-12-23 10:56:15 +01:00
José Valim 712f24af0a Do not run async groups on load and support --repeat-until-failure (#14107) 2024-12-23 10:32:12 +01:00
José Valim 8c9f303e37 Avoid crash when typing violation is detected on dynamic dispatch
Closes #14105.
2024-12-22 17:56:59 +01:00
José Valim 1a2be16109 Track source for warnings, closes #14093 2024-12-22 11:08:13 +01:00
José Valim 4adaac702e Do not validate clauses of overridable functions, closes #14094 2024-12-22 09:55:23 +01:00
José Valim a1be4fbc86 Verify no warnings are emitted on generated clauses, see #14094 2024-12-22 09:55:21 +01:00
Jonatan Männchen d4e6a558cb Fix Release Signing Again (#14099) (#14102)
This time, fix the Docs publishing.
2024-12-21 18:31:26 +01:00
Jonatan Männchen 4e3203f19a Fix Release Signing (#14099) (#14100) 2024-12-21 18:31:21 +01:00
José Valim 4a5fb1eb66 Use binwrite only on Windows, closes #14101 2024-12-21 18:25:45 +01:00
Jonatan Kłosko 0fe6f68c34 Fix mix escript.build when protocol consolidation is disabled (#14098) 2024-12-21 12:22:54 +01:00
Jean Klingler 5aa049ccb3 Remove no_parens when using capture with arity (#14090) 2024-12-20 22:28:19 +09:00
Jonatan Männchen e4c86d4b5f Hash release files after signing (#14085) 2024-12-20 11:29:29 +01:00
José Valim e5033c94ce Keep traces backwards compatible 2024-12-20 10:43:48 +01:00
José Valim 5d65d60721 Comment out canonical and update release notes 2024-12-19 20:34:17 +01:00
José Valim 3564c68cdc Release v1.18.0 2024-12-19 18:30:20 +01:00
José Valim 9df42f5a0a Update CHANGELOG 2024-12-19 17:26:59 +01:00
José Valim c6597bcb20 Collapse structs for better pretty printing in different scenarios 2024-12-19 15:13:18 +01:00
José Valim ca6edfd389 Include types in comparison warning 2024-12-19 15:13:14 +01:00
José Valim ed83c407a5 Simplify and optimize NaiveDateTime.utc_now 2024-12-19 15:13:12 +01:00
José Valim a426520b66 Improvements to descr pretty printing 2024-12-18 09:32:08 +01:00
Guillaume Duboc 1c0f585f40 Fuse maps and tuples for printing (#14079) 2024-12-18 09:32:06 +01:00
José Valim 7c86c4bdb3 Update CHANGELOG 2024-12-17 10:11:15 +01:00
José Valim 4931ab40a5 Do not warn for CaseTemplate options, closes #14077 2024-12-17 10:03:53 +01:00
José Valim c7f78e8015 Emit warnings for duplicate patterns instead of errors 2024-12-16 19:30:54 +01:00
José Valim 6a24bafbda Tag exceptions as dynamic, closes #14074 2024-12-16 16:13:04 +01:00
José Valim 8ddef06e81 Also encode Duration to JSON 2024-12-16 12:23:47 +01:00
José Valim 472c4e1a24 Avoid crashes on diagnostics with tabs, closes #14073 2024-12-16 12:09:18 +01:00
José Valim 305d8e603c Do not convert fun() into (... -> any()) and vice-versa in typespecs 2024-12-15 15:18:52 +01:00
Jonatan Kłosko 1c7fc86e9c Make :source_path in docs chunk a charlist (#14071) 2024-12-13 16:57:54 +01:00
José Valim 617dc72759 Fix Range.new deprecation assertion 2024-12-13 10:14:47 +01:00
José Valim 4329a3c2f2 Address bootstrap 2024-12-13 10:01:18 +01:00
José Valim 8e8c9a7334 Wrap Range.new in Function.identity to avoid tail call optimization 2024-12-13 09:55:39 +01:00
Michał Łępicki 36c06c3a56 Fix JSON.decode/3 spec (#14068)
The error clause was missing the :error tuple wrapping
2024-12-13 09:52:42 +01:00
Mathias Polligkeit 048cc2ddb1 Further improve warning message for range with negative step (#14070) 2024-12-13 09:52:42 +01:00
Wojtek Mach 048ae58d6d Do not set docs source_ref to -latest tags (#14065) 2024-12-12 11:51:41 +01:00
José Valim 85e00cddd5 Clarify warnings happen on compiled code 2024-12-12 09:52:30 +01:00
Jean Klingler 6c866e8ab2 Fix type warnings in pop_in/1 (#14064) 2024-12-12 17:47:23 +09:00
José Valim 55eca5dff5 Implement JSON.Encoder for Calendar types (#14061) 2024-12-12 00:38:44 +01:00
Panagiotis Nezis 070a6d51bf Improve warning in Range.new/2 with negative step (#14062) 2024-12-12 00:38:40 +01:00
José Valim 9c28fb7a39 Improve JSON and Float docs 2024-12-11 21:08:20 +01:00
Andrea Leopardi cda1a09c58 Improve error message on empty "mix do" (#14055) 2024-12-11 12:44:47 +01:00
José Valim 3ca0bddd9d Fix encoding specs 2024-12-11 12:16:33 +01:00
José Valim e07a91594b Update deprecations 2024-12-11 12:02:01 +01:00
José Valim dfc659104c Touch the compile path when writing .app file for the first time (#14053)
Closes #14049.
2024-12-11 10:01:00 +01:00
Shenghang Tsai a4fa71ab23 Update CHANGELOG.md (#14052) 2024-12-11 09:43:42 +01:00
EksperimentalandWojtek Mach 2273bafb47 Improve grammar in CHANGELOG.md (#14050)
Co-authored-by: Wojtek Mach <wojtekmach@users.noreply.github.com>
2024-12-11 08:41:11 +01:00
José Valim a78f1715dc Fix error message around single quote keywords 2024-12-10 19:30:39 +01:00
José Valim b709865976 Improvements to markdown rendering in CHANGELOG 2024-12-10 19:18:32 +01:00
José Valim da7e04a540 Improve docs around auto_reloading 2024-12-10 19:16:09 +01:00
José Valim 66c5908619 Release v1.18.0-rc.0 2024-12-10 18:32:26 +01:00
José Valim 31d8da6c77 Avoid mtime races on compile.app_test.exs 2024-12-10 15:41:16 +01:00
Daniel Gomez de Souza 3bdf5a912c Fix typo (#14046) 2024-12-10 15:34:32 +01:00
José Valim 0e9b5ec070 Branch out v1.18 2024-12-10 15:01:43 +01:00
68 changed files with 1214 additions and 539 deletions
+78 -36
View File
@@ -55,7 +55,6 @@ jobs:
git push origin $ref_name --force git push origin $ref_name --force
build: build:
needs: create_draft_release
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
@@ -80,26 +79,6 @@ jobs:
otp: ${{ matrix.otp }} otp: ${{ matrix.otp }}
build_docs: ${{ matrix.build_docs }} 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" - name: "Attest docs provenance"
uses: actions/attest-build-provenance@v2 uses: actions/attest-build-provenance@v2
id: attest-docs-provenance id: attest-docs-provenance
@@ -112,11 +91,23 @@ jobs:
env: env:
ATTESTATION: "${{ steps.attest-docs-provenance.outputs.bundle-path }}" 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 uses: actions/upload-artifact@v4
with: with:
name: elixir-otp-${{ matrix.otp }} name: build-linux-elixir-otp-${{ matrix.otp }}
path: 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" - name: "Upload doc artifacts"
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -124,21 +115,27 @@ jobs:
with: with:
name: Docs name: Docs
path: Docs.zip* path: Docs.zip*
sign:
needs: [build]
strategy:
fail-fast: true
matrix:
otp: [26, 27]
flavor: [windows, linux]
upload-release: env:
needs: build RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }}
runs-on: windows-2022
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-22.04' || 'windows-2022' }}
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with:
- run: | name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
mv elixir-otp-*/* .
mv Docs/* .
shell: bash
- name: "Sign files with Trusted Signing" - 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 uses: azure/trusted-signing-action@v0.5.0
with: with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -153,6 +150,50 @@ jobs:
timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256 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 - name: Upload Pre-built
shell: bash shell: bash
env: env:
@@ -179,7 +220,7 @@ jobs:
Docs.zip.sigstore Docs.zip.sigstore
upload-builds-hex-pm: upload-builds-hex-pm:
needs: build needs: [build, sign]
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
concurrency: builds-hex-pm concurrency: builds-hex-pm
env: env:
@@ -193,6 +234,9 @@ jobs:
OTP_GENERIC_VERSION: "25" OTP_GENERIC_VERSION: "25"
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with:
pattern: '{sign-*-elixir-otp-*,Docs}'
merge-multiple: true
- name: Init purge keys file - name: Init purge keys file
run: | run: |
@@ -202,7 +246,6 @@ jobs:
run: | run: |
ref_name=${{ github.ref_name }} ref_name=${{ github.ref_name }}
mv elixir-otp-*/* .
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
dest=${zip/elixir/${ref_name}} dest=${zip/elixir/${ref_name}}
surrogate_key=${dest/.zip$/} surrogate_key=${dest/.zip$/}
@@ -221,7 +264,6 @@ jobs:
done done
- name: Upload Docs to S3 - name: Upload Docs to S3
working-directory: Docs
run: | run: |
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g') version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
@@ -19,8 +19,6 @@ runs:
run: | run: |
make Precompiled.zip make Precompiled.zip
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.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 echo "$PWD/bin" >> $GITHUB_PATH
- name: Install NSIS - name: Install NSIS
shell: bash shell: bash
@@ -34,8 +32,6 @@ runs:
export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
(cd lib/elixir/scripts/windows_installer && ./build.sh) (cd lib/elixir/scripts/windows_installer && ./build.sh)
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe . 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 - name: Get ExDoc ref
if: ${{ inputs.build_docs }} if: ${{ inputs.build_docs }}
shell: bash shell: bash
@@ -66,5 +62,3 @@ runs:
run: | run: |
git fetch --tags git fetch --tags
make Docs.zip make Docs.zip
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
+58 -16
View File
@@ -4,13 +4,13 @@ Elixir v1.18 is an impressive release with improvements across the two main effo
## Type system improvements ## 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 ```elixir
defmodule User do defmodule User do
defstruct [:age, :car_choice] 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 if car in car_choices do
{:ok, car} {:ok, car}
else else
@@ -24,9 +24,9 @@ defmodule User do
end 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 ```elixir
User.drive({:ok, %User{}}, car_choices) User.drive({:ok, %User{}}, car_choices)
@@ -156,14 +156,11 @@ More migrations may be added in future releases.
This release includes official support for JSON encoding and decoding. 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 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.
[ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/)
standards.
### Encoding ### Encoding
Encoding can be done via `JSON.encode!/1` and `JSON.encode_to_iodata!/1` functions. Encoding can be done via `JSON.encode!/1` and `JSON.encode_to_iodata!/1` functions. The default encoding rules are applied as follows:
The default encoding rules are applied as follows:
| **Elixir** | **JSON** | | **Elixir** | **JSON** |
|------------------------|----------| |------------------------|----------|
@@ -177,19 +174,18 @@ The default encoding rules are applied as follows:
| `%{atom() => _}` | Object | | `%{atom() => _}` | Object |
| `%{integer() => _}` | Object | | `%{integer() => _}` | Object |
You may also implement the `JSON.Encoder` protocol for custom data structures. 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: If you have a struct, you can derive the implementation of the `JSON.Encoder` by specifying which fields should be encoded to JSON:
```elixir ```elixir
@derive {JSON.Encoder, only: [....]} @derive {JSON.Encoder, only: [...]}
defstruct ... defstruct ...
``` ```
### Decoding ### Decoding
Decoding can be done via `JSON.decode/2` and `JSON.decode!/2` functions. Decoding can be done via `JSON.decode/2` and `JSON.decode!/2` functions. The default decoding rules are applied as follows:
The default decoding rules are applied as follows:
| **JSON** | **Elixir** | | **JSON** | **Elixir** |
|----------|------------------------| |----------|------------------------|
@@ -199,6 +195,16 @@ The default decoding rules are applied as follows:
| String | `binary()` | | String | `binary()` |
| Object | `%{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 ## 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). 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 +225,37 @@ You may also prefer to write using guards:
def foo(x, y, z) when x == y and y == z def foo(x, y, z) when x == y and y == z
## v1.18.0-dev ## 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 ### 1. Enhancements
@@ -234,6 +270,8 @@ You may also prefer to write using guards:
* [Config] Add `Config.read_config/1` * [Config] Add `Config.read_config/1`
* [Enumerable] Add `Enum.product_by/2` and `Enum.sum_by/2` * [Enumerable] Add `Enum.product_by/2` and `Enum.sum_by/2`
* [Exception] Add `MissingApplicationsError` exception to denote missing applications * [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] 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] Warn on bidirectional confusability in identifiers
* [Kernel] Verify the type of the binary generators * [Kernel] Verify the type of the binary generators
@@ -261,6 +299,7 @@ You may also prefer to write using guards:
#### IEx #### 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 `:dot_iex` support to `IEx.configure/1`
* [IEx] Add report for normal/shutdown exits in IEx * [IEx] Add report for normal/shutdown exits in IEx
@@ -284,6 +323,7 @@ You may also prefer to write using guards:
* [Code.Fragment] Properly handle keyword keys as their own entry * [Code.Fragment] Properly handle keyword keys as their own entry
* [Inspect.Algebra] Ensure `next_break_fits` respects `line_length` * [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] 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 * [Module] Include module attribute line and name when tracing its aliases
* [Stream] Do not halt streams twice in `Stream.transform/5` * [Stream] Do not halt streams twice in `Stream.transform/5`
* [URI] Fix a bug when a schemaless URI is given to `URI.merge/2` * [URI] Fix a bug when a schemaless URI is given to `URI.merge/2`
@@ -334,6 +374,8 @@ You may also prefer to write using guards:
#### Mix #### Mix
* [mix cmd] Deprecate `mix cmd --app APP` in favor of `mix do --app APP` * [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` * [Mix.Tasks.Compile] Deprecate `compilers/0` in favor of `Mix.Task.Compiler.compilers/0`
## v1.17 ## v1.17
+2 -2
View File
@@ -2,7 +2,7 @@ PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs" TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man MAN_PREFIX ?= $(SHARE_PREFIX)/man
CANONICAL := main/ # CANONICAL := main/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS) ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ERLC := erlc -I lib/elixir/include ERLC := erlc -I lib/elixir/include
ERL_MAKE := erl -make ERL_MAKE := erl -make
@@ -17,7 +17,7 @@ INSTALL_DIR = $(INSTALL) -m755 -d
INSTALL_DATA = $(INSTALL) -m644 INSTALL_DATA = $(INSTALL) -m644
INSTALL_PROGRAM = $(INSTALL) -m755 INSTALL_PROGRAM = $(INSTALL) -m755
GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null )) 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_PATH = lib/elixir/tmp/ebin_reproducible
SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
+1 -1
View File
@@ -30,7 +30,7 @@
### Back in main ### 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. Start new /CHANGELOG.md
+2 -3
View File
@@ -6,12 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support Elixir version | Support
:------------- | :----------------------------- :------------- | :-----------------------------
1.18 | Development 1.18 | Bug fixes and security patches
1.17 | Bug fixes and security patches 1.17 | Security patches only
1.16 | Security patches only 1.16 | Security patches only
1.15 | Security patches only 1.15 | Security patches only
1.14 | Security patches only 1.14 | Security patches only
1.13 | Security patches only
## Announcements ## Announcements
+1 -1
View File
@@ -1 +1 @@
1.18.0-dev 1.18.1
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
set -e set -e
ELIXIR_VERSION=1.18.0-dev ELIXIR_VERSION=1.18.1
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2 cat <<USAGE >&2
+1 -1
View File
@@ -1,6 +1,6 @@
@echo off @echo off
set ELIXIR_VERSION=1.18.0-dev set ELIXIR_VERSION=1.18.1
if ""%1""=="""" if ""%2""=="""" goto documentation if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env pwsh #!/usr/bin/env pwsh
$ELIXIR_VERSION = "1.18.0-dev" $ELIXIR_VERSION = "1.18.1"
$scriptPath = Split-Path -Parent $PSCommandPath $scriptPath = Split-Path -Parent $PSCommandPath
$erlExec = "erl" $erlExec = "erl"
+19 -20
View File
@@ -110,30 +110,12 @@ defmodule NaiveDateTime do
@spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t @spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t
def utc_now(calendar_or_time_unit \\ Calendar.ISO) 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 def utc_now(time_unit) when time_unit in [:microsecond, :millisecond, :second, :native] do
utc_now(time_unit, Calendar.ISO) utc_now(time_unit, Calendar.ISO)
end end
def utc_now(calendar) do def utc_now(calendar) do
calendar utc_now(:native, calendar)
|> DateTime.utc_now()
|> DateTime.to_naive()
end end
@doc """ @doc """
@@ -158,7 +140,20 @@ defmodule NaiveDateTime do
@spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t @spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t
def utc_now(time_unit, calendar) def utc_now(time_unit, calendar)
when time_unit in [:native, :microsecond, :millisecond, :second] do 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 end
@doc """ @doc """
@@ -1265,6 +1260,10 @@ defmodule NaiveDateTime do
{:ok, t} | {:error, :incompatible_calendars} {:ok, t} | {:error, :incompatible_calendars}
# Keep it multiline for proper function clause errors. # Keep it multiline for proper function clause errors.
def convert(%NaiveDateTime{calendar: calendar} = ndt, calendar) do
{:ok, ndt}
end
def convert( def convert(
%{ %{
calendar: calendar, calendar: calendar,
+3 -1
View File
@@ -1709,7 +1709,9 @@ defmodule Code do
def put_compiler_option(:warnings_as_errors, _value) do def put_compiler_option(:warnings_as_errors, _value) do
IO.warn( IO.warn(
":warnings_as_errors is deprecated as part of Code.put_compiler_option/2, " <> ":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 :ok
-3
View File
@@ -1226,10 +1226,7 @@ defmodule Code.Fragment do
defp maybe_missing_stab?([{:else, _} | _]), do: true defp maybe_missing_stab?([{:else, _} | _]), do: true
defp maybe_missing_stab?([{:catch, _} | _]), do: true defp maybe_missing_stab?([{:catch, _} | _]), do: true
defp maybe_missing_stab?([{:rescue, _} | _]), do: true defp maybe_missing_stab?([{:rescue, _} | _]), do: true
defp maybe_missing_stab?([{:stab_op, _, :->} | _]), do: false 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?([_ | tail]), do: maybe_missing_stab?(tail)
defp maybe_missing_stab?([]), do: false defp maybe_missing_stab?([]), do: false
end end
-9
View File
@@ -27,11 +27,6 @@ defmodule Code.Typespec do
end end
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 def spec_to_quoted(name, {:type, anno, :bounded_fun, [type, constrs]}) when is_atom(name) do
meta = meta(anno) meta = meta(anno)
{:type, _, :fun, [{:type, _, :product, args}, result]} = type {:type, _, :fun, [{:type, _, :product, args}, result]} = type
@@ -317,10 +312,6 @@ defmodule Code.Typespec do
[{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}] [{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}]
end 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 defp typespec_to_quoted({:type, anno, :range, [left, right]}) do
{:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]} {:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]}
end end
+3 -9
View File
@@ -577,15 +577,9 @@ defmodule Float do
Returns a charlist which corresponds to the shortest text representation Returns a charlist which corresponds to the shortest text representation
of the given float. of the given float.
The underlying algorithm changes depending on the Erlang/OTP version: It uses the algorithm presented in "Ryū: fast float-to-string conversion"
in Proceedings of the SIGPLAN '2018 Conference on Programming Language
* For OTP >= 24, it uses the algorithm presented in "Ryū: fast Design and Implementation.
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.
For a configurable representation, use `:erlang.float_to_list/2`. For a configurable representation, use `:erlang.float_to_list/2`.
+19 -4
View File
@@ -150,6 +150,12 @@ defimpl JSON.Encoder, for: Map do
end end
end end
defimpl JSON.Encoder, for: [Date, Time, NaiveDateTime, DateTime, Duration] do
def encode(value, _encoder) do
[?", @for.to_iso8601(value), ?"]
end
end
defmodule JSON.DecodeError do defmodule JSON.DecodeError do
@moduledoc """ @moduledoc """
The exception raised by `JSON.decode!/1`. The exception raised by `JSON.decode!/1`.
@@ -199,6 +205,8 @@ defmodule JSON do
@moduledoc since: "1.18.0" @moduledoc since: "1.18.0"
@type encoder :: (term(), encoder() -> iodata())
@type decode_error_reason :: @type decode_error_reason ::
{:unexpected_end, non_neg_integer()} {:unexpected_end, non_neg_integer()}
| {:invalid_byte, non_neg_integer(), byte()} | {:invalid_byte, non_neg_integer(), byte()}
@@ -257,7 +265,8 @@ defmodule JSON do
For streaming decoding, see Erlang's `:json` module. 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 def decode(binary, acc, decoders) when is_binary(binary) and is_list(decoders) do
decoders = Keyword.put_new(decoders, :null, nil) decoders = Keyword.put_new(decoders, :null, nil)
@@ -326,13 +335,19 @@ defmodule JSON do
The second argument is a function that is recursively The second argument is a function that is recursively
invoked to encode a term. 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 ## Examples
iex> JSON.encode!([123, "string", %{key: "value"}]) iex> JSON.encode!([123, "string", %{key: "value"}])
"[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 def encode!(term, encoder \\ &protocol_encode/2) do
IO.iodata_to_binary(encoder.(term, encoder)) IO.iodata_to_binary(encoder.(term, encoder))
end end
@@ -353,7 +368,7 @@ defmodule JSON do
"[123,\"string\",{\"key\":\"value\"}]" "[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 def encode_to_iodata!(term, encoder \\ &protocol_encode/2) do
encoder.(term, encoder) encoder.(term, encoder)
end end
@@ -365,7 +380,7 @@ defmodule JSON do
`encode!/2` and `encode_to_iodata!/2`. The default implementation `encode!/2` and `encode_to_iodata!/2`. The default implementation
is an optimized dispatch to the `JSON.Encoder` protocol. 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 def protocol_encode(value, encoder) when is_atom(value) do
case value do case value do
nil -> "null" nil -> "null"
+7 -7
View File
@@ -1972,7 +1972,7 @@ defmodule Kernel do
defp build_boolean_check(operator, check, true_clause, false_clause) do defp build_boolean_check(operator, check, true_clause, false_clause) do
annotate_case( annotate_case(
[optimize_boolean: true, type_check: :expr], [optimize_boolean: true],
quote do quote do
case unquote(check) do case unquote(check) do
false -> unquote(false_clause) false -> unquote(false_clause)
@@ -2006,7 +2006,7 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "!") assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case( annotate_case(
[optimize_boolean: true, type_check: :expr], [optimize_boolean: true],
quote do quote do
case unquote(value) do case unquote(value) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> false x when :"Elixir.Kernel".in(x, [false, nil]) -> false
@@ -2020,7 +2020,7 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "!") assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case( annotate_case(
[optimize_boolean: true, type_check: :expr], [optimize_boolean: true],
quote do quote do
case unquote(value) do case unquote(value) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> true x when :"Elixir.Kernel".in(x, [false, nil]) -> true
@@ -3306,7 +3306,7 @@ defmodule Kernel do
end end
defp nest_pop_in(:map, h, [{:access, key}]) do defp nest_pop_in(:map, h, [{:access, key}]) do
quote do quote generated: true do
case unquote(h) do case unquote(h) do
nil -> {nil, nil} nil -> {nil, nil}
h -> Access.pop(h, unquote(key)) h -> Access.pop(h, unquote(key))
@@ -3327,7 +3327,7 @@ defmodule Kernel do
end end
defp nest_pop_in(_, h, [{:access, key}]) do defp nest_pop_in(_, h, [{:access, key}]) do
quote do quote generated: true do
case unquote(h) do case unquote(h) do
nil -> :pop nil -> :pop
h -> Access.pop(h, unquote(key)) h -> Access.pop(h, unquote(key))
@@ -3910,7 +3910,7 @@ defmodule Kernel do
defp build_if(condition, do: do_clause, else: else_clause) do defp build_if(condition, do: do_clause, else: else_clause) do
annotate_case( annotate_case(
[optimize_boolean: true, type_check: :expr], [optimize_boolean: true],
quote do quote do
case unquote(condition) do case unquote(condition) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause) x when :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause)
@@ -4066,7 +4066,7 @@ defmodule Kernel do
end end
defp stepless_range(nil, first, last, _caller) do 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 end
defp stepless_range(:guard, first, last, caller) do defp stepless_range(:guard, first, last, caller) do
+3 -18
View File
@@ -257,28 +257,13 @@ defmodule Kernel.ParallelCompiler do
{status, modules_or_errors, info} = {status, modules_or_errors, info} =
try do try do
outcome = spawn_workers(schedulers, cache, files, output, options) spawn_workers(schedulers, cache, files, output, options)
{outcome, Keyword.get(options, :warnings_as_errors, false)}
else else
{{:ok, _, %{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info}, true} {:ok, outcome, info} ->
when r_warnings != [] or c_warnings != [] ->
message =
"Compilation failed due to warnings while using the --warnings-as-errors option"
IO.puts(:stderr, message)
errors = Enum.map(r_warnings ++ c_warnings, &Map.replace!(&1, :severity, :error))
{:error, errors, %{info | runtime_warnings: [], compile_warnings: []}}
{{:ok, outcome, info}, _} ->
beam_timestamp = Keyword.get(options, :beam_timestamp) beam_timestamp = Keyword.get(options, :beam_timestamp)
{:ok, write_module_binaries(outcome, output, beam_timestamp), info} {:ok, write_module_binaries(outcome, output, beam_timestamp), info}
{{:error, errors, info}, true} -> {:error, errors, info} ->
%{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 after
Module.ParallelChecker.stop(cache) Module.ParallelChecker.stop(cache)
+1 -8
View File
@@ -668,14 +668,7 @@ defmodule Kernel.Typespec do
when is_list(args) do when is_list(args) do
{args, state} = fn_args(meta, args, vars, caller, state) {args, state} = fn_args(meta, args, vars, caller, state)
{spec, state} = typespec(return, vars, caller, state) {spec, state} = typespec(return, vars, caller, state)
{{:type, location(meta), :fun, [args, spec]}, state}
fun_args =
case [args, spec] do
[{:type, _, :any}, {:type, _, :any, []}] -> []
pair -> pair
end
{{:type, location(meta), :fun, fun_args}, state}
end end
# Handle type operator # Handle type operator
+7 -5
View File
@@ -239,7 +239,7 @@ defmodule Module.ParallelChecker do
|> Module.Types.warnings(file, definitions, no_warn_undefined, cache) |> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
|> Kernel.++(behaviour_warnings) |> Kernel.++(behaviour_warnings)
|> group_warnings() |> group_warnings()
|> emit_warnings(log?) |> emit_warnings(file, log?)
Enum.each(after_verify, fn {verify_mod, verify_fun} -> Enum.each(after_verify, fn {verify_mod, verify_fun} ->
apply(verify_mod, verify_fun, [module]) apply(verify_mod, verify_fun, [module])
@@ -320,9 +320,9 @@ defmodule Module.ParallelChecker do
Enum.sort(ungrouped ++ grouped) Enum.sort(ungrouped ++ grouped)
end end
defp emit_warnings(warnings, log?) do defp emit_warnings(warnings, file, log?) do
Enum.flat_map(warnings, fn {locations, diagnostic} -> 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) log? and print_diagnostics(diagnostics)
diagnostics diagnostics
end) end)
@@ -336,10 +336,12 @@ defmodule Module.ParallelChecker do
:elixir_errors.print_diagnostics(diagnostics) :elixir_errors.print_diagnostics(diagnostics)
end 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, severity: :warning,
source: file, source: source,
file: file, file: file,
position: position_to_tuple(position), position: position_to_tuple(position),
stacktrace: [to_stacktrace(file, position, mfa)], stacktrace: [to_stacktrace(file, position, mfa)],
+5 -2
View File
@@ -182,9 +182,12 @@ defmodule Module.Types do
end end
defp warn_unused_clauses(defs, stack, context) do 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 -> context ->
{_fun_arity, kind, _meta, clauses} = List.keyfind(defs, fun_arity, 0)
{_kind, _inferred, mapping} = Map.fetch!(context.local_sigs, fun_arity) {_kind, _inferred, mapping} = Map.fetch!(context.local_sigs, fun_arity)
clauses_indexes = clauses_indexes =
+105 -40
View File
@@ -269,7 +269,7 @@ defmodule Module.Types.Apply do
case signature(name, arity) do case signature(name, arity) do
:none -> {dynamic(), context} :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
end end
@@ -287,10 +287,14 @@ defmodule Module.Types.Apply do
{value_type, context} {value_type, context}
:badtuple -> :badtuple ->
{error_type(), badremote_error(expr, [integer(), tuple], stack, context)} {error_type(),
badremote_error(:erlang, :element, expr, [integer(), tuple], stack, context)}
reason -> :badindex ->
{error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)} mfac = mfac(expr, :erlang, :element, 2)
{error_type(),
error({:badindex, mfac, expr, tuple, index - 1, context}, meta, stack, context)}
end end
end end
@@ -308,10 +312,16 @@ defmodule Module.Types.Apply do
{value_type, context} {value_type, context}
:badtuple -> :badtuple ->
{error_type(), badremote_error(expr, [integer(), tuple, value], stack, context)} args_types = [integer(), tuple, value]
reason -> {error_type(),
{error_type(), error({reason, expr, tuple, index - 2, context}, meta, stack, context)} 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
end end
@@ -322,10 +332,16 @@ defmodule Module.Types.Apply do
{value_type, context} {value_type, context}
:badtuple -> :badtuple ->
{error_type(), badremote_error(expr, [integer(), tuple], stack, context)} args_types = [integer(), tuple]
reason -> {error_type(),
{error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)} 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
end end
@@ -340,7 +356,7 @@ defmodule Module.Types.Apply do
{value_type, context} {value_type, context}
:badnonemptylist -> :badnonemptylist ->
{error_type(), badremote_error(expr, [list], stack, context)} {error_type(), badremote_error(:erlang, :hd, expr, [list], stack, context)}
end end
end end
@@ -350,7 +366,7 @@ defmodule Module.Types.Apply do
{value_type, context} {value_type, context}
:badnonemptylist -> :badnonemptylist ->
{error_type(), badremote_error(expr, [list], stack, context)} {error_type(), badremote_error(:erlang, :tl, expr, [list], stack, context)}
end end
end end
@@ -363,14 +379,14 @@ defmodule Module.Types.Apply do
match?({false, _}, map_fetch(left, :__struct__)) or match?({false, _}, map_fetch(left, :__struct__)) or
match?({false, _}, map_fetch(right, :__struct__)) -> 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) warn(__MODULE__, warning, elem(expr, 1), stack, context)
number_type?(left) and number_type?(right) -> number_type?(left) and number_type?(right) ->
context context
disjoint?(left, right) -> disjoint?(left, right) ->
warning = {:mismatched_comparison, expr, context} warning = {:mismatched_comparison, expr, name, left, right, context}
warn(__MODULE__, warning, elem(expr, 1), stack, context) warn(__MODULE__, warning, elem(expr, 1), stack, context)
true -> true ->
@@ -384,18 +400,26 @@ defmodule Module.Types.Apply do
end end
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 when name in [:==, :"/=", :"=:=", :"=/="] do
context = context =
cond do 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 context
name in [:==, :"/="] and number_type?(left) and number_type?(right) -> name in [:==, :"/="] and number_type?(left) and number_type?(right) ->
context context
disjoint?(left, right) -> disjoint?(left, right) ->
warning = {:mismatched_comparison, expr, context} warning = {:mismatched_comparison, expr, name, left, right, context}
warn(__MODULE__, warning, elem(expr, 1), stack, context) warn(__MODULE__, warning, elem(expr, 1), stack, context)
true -> true ->
@@ -405,30 +429,36 @@ defmodule Module.Types.Apply do
{return(boolean(), args_types, stack), context} {return(boolean(), args_types, stack), context}
end 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) arity = length(args_types)
case :elixir_rewrite.inline(mod, name, arity) do case :elixir_rewrite.inline(mod, fun, arity) do
{mod, name} -> {new_mod, new_fun} ->
remote(mod, name, args_types, expr, stack, context) expr = inline_meta(expr, mod, fun)
remote(new_mod, new_fun, args_types, expr, stack, context)
false -> false ->
{info, context} = signature(mod, name, arity, elem(expr, 1), stack, context) {info, context} = signature(mod, fun, arity, elem(expr, 1), stack, context)
apply_remote(info, args_types, expr, stack, context) apply_remote(mod, fun, info, args_types, expr, stack, context)
end end
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 case apply_signature(info, args_types, stack) do
{:ok, _indexes, type} -> {:ok, _indexes, type} ->
{type, context} {type, context}
{:error, domain, clauses} -> {: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)} {error_type(), error(error, elem(expr, 1), stack, context)}
end end
end end
defp inline_meta({node, meta, args}, mod, fun) do
{node, [inline: {mod, fun}] ++ meta, args}
end
@doc """ @doc """
Returns the type of a remote capture. Returns the type of a remote capture.
""" """
@@ -718,22 +748,28 @@ defmodule Module.Types.Apply do
error(__MODULE__, warning, meta, stack, context) error(__MODULE__, warning, meta, stack, context)
end end
defp badremote_error({{:., _, [mod, fun]}, meta, _} = expr, args_types, stack, context) do defp badremote_error(mod, fun, {_, meta, _} = expr, args_types, stack, context) do
{_type, domain, [{args, _} | _] = clauses} = signature(mod, fun, length(args_types)) arity = length(args_types)
error({:badremote, expr, args_types, domain || args, clauses, context}, meta, stack, context) 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 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) traces = collect_traces(expr, context)
{mod, fun, arity, _converter} = mfac
mfa = Exception.format_mfa(mod, fun, arity)
%{ %{
details: %{typing_traces: traces}, details: %{typing_traces: traces},
message: message:
IO.iodata_to_binary([ 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)} #{expr_to_string(expr) |> indent(4)}
@@ -778,10 +814,9 @@ defmodule Module.Types.Apply do
} }
end 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) traces = collect_traces(expr, context)
{{:., _, [mod, fun]}, _, args} = expr {mod, fun, arity, converter} = mfac
{mod, fun, args, converter} = :elixir_rewrite.erl_to_ex(mod, fun, args)
explanation = explanation =
empty_arg_reason(converter.(args_types)) || empty_arg_reason(converter.(args_types)) ||
@@ -790,12 +825,15 @@ defmodule Module.Types.Apply do
#{clauses_args_to_quoted_string(clauses, converter)} #{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}, details: %{typing_traces: traces},
message: message:
IO.iodata_to_binary([ 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)} #{expr_to_string(expr) |> indent(4)}
@@ -810,7 +848,7 @@ defmodule Module.Types.Apply do
} }
end 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) traces = collect_traces(expr, context)
%{ %{
@@ -821,6 +859,10 @@ defmodule Module.Types.Apply do
comparison between distinct types found: comparison between distinct types found:
#{expr_to_string(expr) |> indent(4)} #{expr_to_string(expr) |> indent(4)}
given types:
#{type_comparison_to_string(name, left, right) |> indent(4)}
""", """,
format_traces(traces), format_traces(traces),
""" """
@@ -833,7 +875,7 @@ defmodule Module.Types.Apply do
} }
end 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) traces = collect_traces(expr, context)
%{ %{
@@ -844,6 +886,10 @@ defmodule Module.Types.Apply do
comparison with structs found: comparison with structs found:
#{expr_to_string(expr) |> indent(4)} #{expr_to_string(expr) |> indent(4)}
given types:
#{type_comparison_to_string(name, left, right) |> indent(4)}
""", """,
format_traces(traces), format_traces(traces),
""" """
@@ -924,15 +970,34 @@ defmodule Module.Types.Apply do
defp pluralize(1, singular, _), do: "1 #{singular}" defp pluralize(1, singular, _), do: "1 #{singular}"
defp pluralize(i, _, plural), do: "#{i} #{plural}" defp pluralize(i, _, plural), do: "#{i} #{plural}"
defp format_mfa({{:., _, [mod, fun]}, _, args}) do defp mfac({_, [inline: {mod, fun}] ++ _, _}, _mod, _fun, arity) do
{mod, fun, args, _} = :elixir_rewrite.erl_to_ex(mod, fun, args) {mod, fun, arity, & &1}
Exception.format_mfa(mod, fun, length(args)) 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 end
## Algebra helpers ## Algebra helpers
alias Inspect.Algebra, as: IA 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 defp clauses_args_to_quoted_string([{args, _return}], converter) do
"\n " <> (clause_args_to_quoted_string(args, converter) |> indent(4)) "\n " <> (clause_args_to_quoted_string(args, converter) |> indent(4))
end end
+190 -69
View File
@@ -367,18 +367,30 @@ defmodule Module.Types.Descr do
@doc """ @doc """
Converts a descr to its quoted representation. 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 if term_type?(descr) do
{:term, [], []} {:term, [], []}
else 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} = {extra, descr} =
case descr do case descr do
# Merge empty list and list together if they both exist
%{list: list, bitmap: bitmap} when (bitmap &&& @bit_empty_list) != 0 -> %{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} [] -> {[{:empty_list, [], []}], descr}
unions -> {unions, descr} unions -> {unions, descr}
end end
@@ -387,26 +399,32 @@ defmodule Module.Types.Descr do
{[], descr} {[], descr}
end 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, [], []} [] -> {:none, [], []}
unions -> unions |> Enum.sort() |> Enum.reduce(&{:or, [], [&2, &1]}) unions -> Enum.reduce(unions, &{:or, [], [&2, &1]})
end end
end end
end end
defp to_quoted(:atom, val), do: atom_to_quoted(val) defp to_quoted(:atom, val, _opts), do: atom_to_quoted(val)
defp to_quoted(:bitmap, val), do: bitmap_to_quoted(val) defp to_quoted(:bitmap, val, _opts), do: bitmap_to_quoted(val)
defp to_quoted(:dynamic, descr), do: dynamic_to_quoted(descr) defp to_quoted(:dynamic, descr, opts), do: dynamic_to_quoted(descr, opts)
defp to_quoted(:map, dnf), do: map_to_quoted(dnf) defp to_quoted(:map, dnf, opts), do: map_to_quoted(dnf, opts)
defp to_quoted(:list, dnf), do: list_to_quoted(dnf, :non_empty_list) defp to_quoted(:list, dnf, opts), do: list_to_quoted(dnf, :non_empty_list, opts)
defp to_quoted(:tuple, dnf), do: tuple_to_quoted(dnf) defp to_quoted(:tuple, dnf, opts), do: tuple_to_quoted(dnf, opts)
@doc """ @doc """
Converts a descr to its quoted string representation. Converts a descr to its quoted string representation.
""" """
def to_quoted_string(descr) do def to_quoted_string(descr, opts \\ []) do
descr descr
|> to_quoted() |> to_quoted(opts)
|> Code.Formatter.to_algebra() |> Code.Formatter.to_algebra()
|> Inspect.Algebra.format(98) |> Inspect.Algebra.format(98)
|> IO.iodata_to_binary() |> IO.iodata_to_binary()
@@ -785,17 +803,16 @@ defmodule Module.Types.Descr do
defp atom_to_quoted({:union, a}) do defp atom_to_quoted({:union, a}) do
if :sets.is_subset(@boolset, a) do if :sets.is_subset(@boolset, a) do
:sets.subtract(a, @boolset) entries =
|> :sets.to_list() :sets.subtract(a, @boolset)
|> Enum.sort() |> :sets.to_list()
|> Enum.reduce({:boolean, [], []}, &{:or, [], [&2, literal_to_quoted(&1)]}) |> Enum.map(&literal_to_quoted/1)
[{:boolean, [], []} | entries]
else else
:sets.to_list(a) :sets.to_list(a)
|> Enum.sort()
|> Enum.map(&literal_to_quoted/1) |> Enum.map(&literal_to_quoted/1)
|> Enum.reduce(&{:or, [], [&2, &1]})
end end
|> List.wrap()
end end
defp atom_to_quoted({:negation, a}) do defp atom_to_quoted({:negation, a}) do
@@ -1035,16 +1052,16 @@ defmodule Module.Types.Descr do
end end
end end
defp list_to_quoted(dnf, name) do defp list_to_quoted(dnf, name, opts) do
dnf = list_normalize(dnf) dnf = list_normalize(dnf)
for {list_type, last_type, negs} <- dnf, reduce: [] do for {list_type, last_type, negs} <- dnf, reduce: [] do
acc -> acc ->
arguments = arguments =
if subtype?(last_type, @empty_list) do if subtype?(last_type, @empty_list) do
[to_quoted(list_type)] [to_quoted(list_type, opts)]
else else
[to_quoted(list_type), to_quoted(last_type)] [to_quoted(list_type, opts), to_quoted(last_type, opts)]
end end
if negs == [] do if negs == [] do
@@ -1054,9 +1071,9 @@ defmodule Module.Types.Descr do
|> Enum.map(fn {ty, lst} -> |> Enum.map(fn {ty, lst} ->
args = args =
if subtype?(lst, @empty_list) do if subtype?(lst, @empty_list) do
[to_quoted(ty)] [to_quoted(ty, opts)]
else else
[to_quoted(ty), to_quoted(lst)] [to_quoted(ty, opts), to_quoted(lst, opts)]
end end
{name, [], args} {name, [], args}
@@ -1064,11 +1081,7 @@ defmodule Module.Types.Descr do
|> Enum.reduce(&{:or, [], [&2, &1]}) |> Enum.reduce(&{:or, [], [&2, &1]})
|> Kernel.then( |> Kernel.then(
&[ &[
{:and, [], {:and, [], [{name, [], arguments}, {:not, [], [&1]}]}
[
{name, [], arguments},
{:not, [], [&1]}
]}
| acc | acc
] ]
) )
@@ -1170,7 +1183,7 @@ defmodule Module.Types.Descr do
end end
end end
defp dynamic_to_quoted(descr) do defp dynamic_to_quoted(descr, opts) do
cond do cond do
term_type?(descr) -> term_type?(descr) ->
[{:dynamic, [], []}] [{:dynamic, [], []}]
@@ -1179,7 +1192,7 @@ defmodule Module.Types.Descr do
[single] [single]
true -> true ->
case to_quoted(descr) do case to_quoted(descr, opts) do
{:none, _meta, []} = none -> [none] {:none, _meta, []} = none -> [none]
descr -> [{:dynamic, [], [descr]}] descr -> [{:dynamic, [], [descr]}]
end end
@@ -1691,7 +1704,7 @@ defmodule Module.Types.Descr do
if map_empty_negation?(tag, acc_fields, neg) do if map_empty_negation?(tag, acc_fields, neg) do
{acc_fields, acc_negs} {acc_fields, acc_negs}
else 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} -> {:one, diff_key} ->
{Map.update!(acc_fields, diff_key, &difference(&1, neg_fields[diff_key])), {Map.update!(acc_fields, diff_key, &difference(&1, neg_fields[diff_key])),
acc_negs} acc_negs}
@@ -1704,10 +1717,55 @@ defmodule Module.Types.Descr do
{tag, fields, negs} {tag, fields, negs}
end) 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 end
# If all fields are the same except one, we can optimize map difference. # 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) keys1 = Map.keys(fields1)
keys2 = Map.keys(fields2) keys2 = Map.keys(fields2)
@@ -1735,55 +1793,77 @@ defmodule Module.Types.Descr do
end)) end))
end end
defp map_to_quoted(dnf) do defp map_to_quoted(dnf, opts) do
dnf dnf
|> map_normalize() |> map_normalize()
|> Enum.map(&map_each_to_quoted/1) |> Enum.map(&map_each_to_quoted(&1, opts))
|> case do
[] -> []
dnf -> Enum.reduce(dnf, &{:or, [], [&2, &1]}) |> List.wrap()
end
end 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 case negative_maps do
[] -> [] ->
map_literal_to_quoted({tag, positive_map}) map_literal_to_quoted({tag, positive_map}, opts)
_ -> _ ->
negative_maps negative_maps
|> Enum.map(&map_literal_to_quoted/1) |> Enum.map(&map_literal_to_quoted(&1, opts))
|> Enum.reduce(&{:or, [], [&2, &1]}) |> Enum.reduce(&{:or, [], [&2, &1]})
|> Kernel.then( |> 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
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, [], []} {:empty_map, [], []}
end end
def map_literal_to_quoted({tag, fields}) do def map_literal_to_quoted({tag, fields}, opts) do
case tag do case tag do
:closed -> :closed ->
with %{__struct__: struct_descr} <- fields, with %{__struct__: struct_descr} <- fields,
{_, [struct]} <- atom_fetch(struct_descr) do {_, [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), literal_to_quoted(struct),
{:%{}, [], map_fields_to_quoted(tag, Map.delete(fields, :__struct__))} {:%{}, [], map_fields_to_quoted(tag, fields, opts)}
]} ]}
else else
_ -> {:%{}, [], map_fields_to_quoted(tag, fields)} _ -> {:%{}, [], map_fields_to_quoted(tag, fields, opts)}
end end
:open -> :open ->
{:%{}, [], [{:..., [], nil} | map_fields_to_quoted(tag, fields)]} {:%{}, [], [{:..., [], nil} | map_fields_to_quoted(tag, fields, opts)]}
end end
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)) sorted = Enum.sort(Map.to_list(map))
keyword? = Inspect.List.keyword?(sorted) keyword? = Inspect.List.keyword?(sorted)
@@ -1799,9 +1879,9 @@ defmodule Module.Types.Descr do
{optional?, type} = pop_optional_static(type) {optional?, type} = pop_optional_static(type)
cond do cond do
not optional? -> {key, to_quoted(type)} not optional? -> {key, to_quoted(type, opts)}
empty?(type) -> {key, {:not_set, [], []}} empty?(type) -> {key, {:not_set, [], []}}
true -> {key, {:if_set, [], [to_quoted(type)]}} true -> {key, {:if_set, [], [to_quoted(type, opts)]}}
end end
end end
end end
@@ -1922,37 +2002,78 @@ defmodule Module.Types.Descr do
# This is a cheap optimization that relies on structural equality. # This is a cheap optimization that relies on structural equality.
defp tuple_union(left, right), do: left ++ (right -- left) defp tuple_union(left, right), do: left ++ (right -- left)
defp tuple_to_quoted(dnf) do defp tuple_to_quoted(dnf, opts) do
dnf dnf
|> tuple_simplify() |> tuple_simplify()
|> Enum.map(&tuple_each_to_quoted/1) |> tuple_fusion()
|> case do |> Enum.map(&tuple_each_to_quoted(&1, opts))
[] -> []
dnf -> Enum.reduce(dnf, &{:or, [], [&2, &1]}) |> List.wrap()
end
end end
defp tuple_each_to_quoted({tag, positive_map, negative_maps}) do # Given a dnf of tuples, fuses the tuple unions when possible,
case negative_maps do # 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 negative_tuples
|> Enum.map(&tuple_literal_to_quoted/1) |> Enum.map(&tuple_literal_to_quoted(&1, opts))
|> Enum.reduce(&{:or, [], [&2, &1]}) |> Enum.reduce(&{:or, [], [&2, &1]})
|> Kernel.then( |> 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
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 case tag do
:closed -> {:{}, [], Enum.map(elements, &to_quoted/1)} :closed -> {:{}, [], Enum.map(elements, &to_quoted(&1, opts))}
:open -> {:{}, [], Enum.map(elements, &to_quoted/1) ++ [{:..., [], nil}]} :open -> {:{}, [], Enum.map(elements, &to_quoted(&1, opts)) ++ [{:..., [], nil}]}
end end
end end
+5 -3
View File
@@ -263,8 +263,9 @@ defmodule Module.Types.Expr do
{case_type, context} = of_expr(case_expr, stack, context) {case_type, context} = of_expr(case_expr, stack, context)
# If we are only type checking the expression and the expression is a literal, # 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. # let's mark it as generated, as it is most likely a macro code. However, if
if is_atom(case_expr) and {:type_check, :expr} in meta do # 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} for {:->, meta, args} <- clauses, do: {:->, [generated: true] ++ meta, args}
else else
clauses clauses
@@ -444,7 +445,8 @@ defmodule Module.Types.Expr do
# to avoid export dependencies. So we do it here. # to avoid export dependencies. So we do it here.
if Code.ensure_loaded?(exception) and function_exported?(exception, :__struct__, 0) do if Code.ensure_loaded?(exception) and function_exported?(exception, :__struct__, 0) do
{info, context} = Of.struct_info(exception, meta, stack, context) {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 else
# If the exception cannot be found or is invalid, fetch the signature to emit warnings. # If the exception cannot be found or is invalid, fetch the signature to emit warnings.
{_, context} = Apply.signature(exception, :__struct__, 0, meta, stack, context) {_, context} = Apply.signature(exception, :__struct__, 0, meta, stack, context)
+11 -7
View File
@@ -85,6 +85,9 @@ defmodule Module.Types.Helpers do
@doc """ @doc """
Collect traces from variables in expression. 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 def collect_traces(expr, %{vars: vars}) do
{_, versions} = {_, versions} =
@@ -135,16 +138,17 @@ defmodule Module.Types.Helpers do
formatter -> formatter.(expr) formatter -> formatter.(expr)
end end
# This information is exposed to language servers and
# therefore must remain backwards compatible.
%{ %{
file: file, file: file,
line: meta[:line], meta: meta,
column: meta[:column],
hints: formatter_hints ++ expr_hints(expr),
formatted_expr: formatted_expr, 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) end)
|> Enum.sort_by(&{&1.line, &1.column}) |> Enum.sort_by(&{&1.meta[:line], &1.meta[:column]})
|> Enum.dedup() |> Enum.dedup()
end end
@@ -161,7 +165,7 @@ defmodule Module.Types.Helpers do
location = location =
trace.file trace.file
|> Path.relative_to_cwd() |> 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(":", "") |> String.replace_suffix(":", "")
[ [
@@ -173,7 +177,7 @@ defmodule Module.Types.Helpers do
""", """,
indent(trace.formatted_expr, 4), indent(trace.formatted_expr, 4),
?\n, ?\n,
format_hints(trace.hints) trace.formatted_hints
] ]
end end
+1 -3
View File
@@ -495,13 +495,11 @@ defmodule Module.Types.Of do
unknown key .#{key} in expression: unknown key .#{key} in expression:
#{expr_to_string(expr) |> indent(4)} #{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
the given type does not have the given key: the given type does not have the given key:
#{to_quoted_string(type) |> indent(4)} #{to_quoted_string(type) |> indent(4)}
"""), """,
format_traces(traces) format_traces(traces)
]) ])
} }
+1 -1
View File
@@ -192,7 +192,7 @@ defmodule Range do
IO.warn_once( IO.warn_once(
{__MODULE__, :new}, {__MODULE__, :new},
fn -> 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, end,
3 3
) )
@@ -8,12 +8,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support Elixir version | Support
:------------- | :----------------------------- :------------- | :-----------------------------
1.18 | Development 1.18 | Bug fixes and security patches
1.17 | Bug fixes and security patches 1.17 | Security patches only
1.16 | Security patches only 1.16 | Security patches only
1.15 | Security patches only 1.15 | Security patches only
1.14 | 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). 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 Elixir version | Supported Erlang/OTP versions
:------------- | :------------------------------- :------------- | :-------------------------------
1.18 | 25 - 27
1.17 | 25 - 27 1.17 | 25 - 27
1.16 | 24 - 26 1.16 | 24 - 26
1.15 | 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) 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] | Single-quoted charlists (`'foo'`) | `~c"foo"` (v1.0)
[v1.17] | `left..right` in patterns and guards | `left..right//step` (v1.11) [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) [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.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.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.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
+15 -4
View File
@@ -11,7 +11,7 @@
parallel_match(Meta, Expr, S, #{context := match} = E) -> parallel_match(Meta, Expr, S, #{context := match} = E) ->
#elixir_ex{vars={_Read, Write}} = S, #elixir_ex{vars={_Read, Write}} = S,
Matches = unpack_match(Expr, Meta, []), Matches = unpack_match(Expr, Meta, [], E),
{[{_, EHead} | ETail], EWrites, SM, EM} = {[{_, EHead} | ETail], EWrites, SM, EM} =
lists:foldl(fun({EMeta, Match}, {AccMatches, AccWrites, SI, EI}) -> 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), VWrite = (Write /= false) andalso elixir_env:merge_vars(Write, PWrites),
{EMatch, SM#elixir_ex{vars={VRead, VWrite}, prematch={PRead, PCycles, PInfo}}, EM}. {EMatch, SM#elixir_ex{vars={VRead, VWrite}, prematch={PRead, PCycles, PInfo}}, EM}.
unpack_match({'=', Meta, [Left, Right]}, _Meta, Acc) -> unpack_match({'=', Meta, [{_, VarMeta, _} = Node, Node]}, _Meta, Acc, E) ->
unpack_match(Left, Meta, unpack_match(Right, Meta, Acc)); %% TODO: remove this clause on Elixir v1.23
unpack_match(Node, Meta, Acc) -> 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]. [{Meta, Node} | Acc].
store_cycles([Write | Writes], {Cycles, SkipList}, Acc) -> store_cycles([Write | Writes], {Cycles, SkipList}, Acc) ->
@@ -532,6 +536,13 @@ origin(Meta, Default) ->
false -> Default false -> Default
end. 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}) -> format_error({recursive, Vars, TypeExpr}) ->
Code = Code =
case TypeExpr of case TypeExpr of
+1
View File
@@ -203,6 +203,7 @@ bootstrap_files() ->
<<"path.ex">>, <<"path.ex">>,
<<"file.ex">>, <<"file.ex">>,
<<"map.ex">>, <<"map.ex">>,
<<"function.ex">>,
<<"range.ex">>, <<"range.ex">>,
<<"access.ex">>, <<"access.ex">>,
<<"io.ex">>, <<"io.ex">>,
+1 -1
View File
@@ -526,7 +526,7 @@ docs_chunk(Map, Set, Module, Anno, Def, Defmacro, Types, Callbacks, ChunkOpts) -
TypeDocs = get_type_docs(Set, Types), TypeDocs = get_type_docs(Set, Types),
ModuleMeta = ModuleDocMeta#{ ModuleMeta = ModuleDocMeta#{
source_path => File, source_path => elixir_utils:characters_to_list(File),
source_annos => [Anno], source_annos => [Anno],
behaviours => [Mod || {behaviour, Mod} <- Attributes] behaviours => [Mod || {behaviour, Mod} <- Attributes]
}, },
+8 -5
View File
@@ -92,7 +92,10 @@ emit_diagnostic(Severity, Position, File, Message, Stacktrace, Options) ->
Diagnostic = #{ Diagnostic = #{
severity => Severity, severity => Severity,
source => File, source => case get(elixir_compiler_file) of
undefined -> File;
CompilerFile -> CompilerFile
end,
file => File, file => File,
position => Position, position => Position,
message => unicode:characters_to_binary(Message), message => unicode:characters_to_binary(Message),
@@ -216,12 +219,12 @@ highlight_at_position(Column, Severity, Length) ->
highlight_below_line(Line, Severity) -> highlight_below_line(Line, Severity) ->
% Don't highlight leading whitespaces in line % 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 Highlight = case Severity of
warning -> highlight(lists:duplicate(Length - SpacesMatched, $~), warning); warning -> highlight(lists:duplicate(Length, $~), warning);
error -> highlight(lists:duplicate(Length - SpacesMatched, $^), error) error -> highlight(lists:duplicate(Length, $^), error)
end, end,
[n_spaces(SpacesMatched), Highlight]. [n_spaces(SpacesMatched), Highlight].
+4
View File
@@ -112,6 +112,10 @@ capture_expr(Meta, Expr, S, E, Escaped, Sequential) ->
case escape(Expr, E, Escaped) of case escape(Expr, E, Escaped) of
{_, []} when not Sequential -> {_, []} when not Sequential ->
invalid_capture(Meta, Expr, E); 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} -> {EExpr, EDict} ->
EVars = validate(Meta, EDict, 1, E), EVars = validate(Meta, EDict, 1, E),
Fn = {fn, Meta, [{'->', Meta, [EVars, EExpr]}]}, Fn = {fn, Meta, [{'->', Meta, [EVars, EExpr]}]},
+2 -1
View File
@@ -75,8 +75,9 @@ store_not_overridden(Module) ->
%% Private %% Private
store(Set, Module, Tuple, {_, Count, Def, Overridden}, Hidden) -> 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, {Defaults, _HasBody, _LastDefaults}}, Clauses} = Def,
Meta = [{from_super, Hidden} | BaseMeta],
{FinalKind, FinalName, FinalArity, FinalClauses} = {FinalKind, FinalName, FinalArity, FinalClauses} =
case Hidden of case Hidden of
+19 -7
View File
@@ -266,11 +266,7 @@ tokenize([$" | T], Line, Column, Scope, Tokens) ->
%% TODO: Remove me in Elixir v2.0 %% TODO: Remove me in Elixir v2.0
tokenize([$' | T], Line, Column, Scope, Tokens) -> tokenize([$' | T], Line, Column, Scope, Tokens) ->
Message = "single-quoted strings represent charlists. " handle_strings(T, Line, Column + 1, $', Scope, Tokens);
"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);
% Operator atoms % Operator atoms
@@ -786,7 +782,11 @@ handle_strings(T, Line, Column, H, Scope, Tokens) ->
"number do not require quotes", "number do not require quotes",
[hd(Parts)] [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 -> false ->
InterScope InterScope
@@ -814,7 +814,19 @@ handle_strings(T, Line, Column, H, Scope, Tokens) ->
error(Reason, Rest, NewScope, Tokens) error(Reason, Rest, NewScope, Tokens)
end; end;
{NewLine, NewColumn, Parts, Rest, NewScope} -> {NewLine, NewColumn, Parts, Rest, InterScope} ->
NewScope =
case H of
$' ->
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.",
prepend_warning(Line, Column-1, Message, InterScope);
_ ->
InterScope
end,
case unescape_tokens(Parts, Line, Column, NewScope) of case unescape_tokens(Parts, Line, Column, NewScope) of
{ok, Unescaped} -> {ok, Unescaped} ->
Token = {string_type(H), {Line, Column - 1, nil}, Unescaped}, Token = {string_type(H), {Line, Column - 1, nil}, Unescaped},
+24 -1
View File
@@ -1315,9 +1315,15 @@ defmodule CodeFragmentTest do
assert cc2q!("(fn x ->", trailing_fragment: ":ok end)") == assert cc2q!("(fn x ->", trailing_fragment: ":ok end)") ==
s2q!("(fn x -> __cursor__() 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)") 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") == s2q!("(__cursor__())")
assert cc2q!("(fn x") == s2q!("(__cursor__())") assert cc2q!("(fn x") == s2q!("(__cursor__())")
assert cc2q!("(fn x,") == s2q!("(__cursor__())") assert cc2q!("(fn x,") == s2q!("(__cursor__())")
@@ -1327,6 +1333,23 @@ defmodule CodeFragmentTest do
assert cc2q!("(fn x, y -> x + y end") == s2q!("(__cursor__())") assert cc2q!("(fn x, y -> x + y end") == s2q!("(__cursor__())")
end end
test "do -> end" do
assert cc2q!("if do\nx ->\n", trailing_fragment: "y\nz ->\nw\nend") ==
s2q!("if do\nx ->\n__cursor__()\nend")
assert cc2q!("if do\nx ->\ny", trailing_fragment: "\nz ->\nw\nend") ==
s2q!("if do\nx ->\n__cursor__()\nend")
assert cc2q!("if do\nx ->\ny\n", trailing_fragment: "\nz ->\nw\nend") ==
s2q!("if do\nx ->\ny\n__cursor__()\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")
end
test "removes tokens until opening" do test "removes tokens until opening" do
assert cc2q!("(123") == s2q!("(__cursor__())") assert cc2q!("(123") == s2q!("(__cursor__())")
assert cc2q!("[foo") == s2q!("[__cursor__()]") assert cc2q!("[foo") == s2q!("[__cursor__()]")
+8
View File
@@ -46,6 +46,14 @@ defmodule JSONTest do
assert JSON.encode!(%Token{value: :example}) == "[\"example\"]" assert JSON.encode!(%Token{value: :example}) == "[\"example\"]"
assert JSON.encode!(%Token{value: "hello\0world"}) == "[\"hello\\u0000world\"]" assert JSON.encode!(%Token{value: "hello\0world"}) == "[\"hello\\u0000world\"]"
end 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 end
describe "JSON.Encoder" do describe "JSON.Encoder" do
@@ -859,6 +859,35 @@ defmodule Kernel.DiagnosticsTest do
purge(Sample) purge(Sample)
end 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 test "simple warning (no file)" do
source = """ source = """
defmodule Sample do defmodule Sample do
+1 -1
View File
@@ -255,7 +255,7 @@ defmodule Kernel.DocsTest do
assert module_doc == "Module doc" assert module_doc == "Module doc"
file = __ENV__.file file = String.to_charlist(__ENV__.file)
source_annos = [:erl_anno.new({line + 3, 19})] source_annos = [:erl_anno.new({line + 3, 19})]
@@ -190,17 +190,6 @@ defmodule Kernel.ExpansionTest do
end end
test "errors on directly recursive definitions" do 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( assert_compile_error(
~r""" ~r"""
recursive variable definition in patterns: recursive variable definition in patterns:
@@ -1214,6 +1203,11 @@ defmodule Kernel.ExpansionTest do
[{:->, [{:line, 1}], [[{:capture, [line: 1], nil}], {:capture, [line: 1], nil}]}]} [{:->, [{:line, 1}], [[{:capture, [line: 1], nil}], {:capture, [line: 1], nil}]}]}
end 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 test "expands remotes" do
assert expand(quote(do: &List.flatten/2)) == assert expand(quote(do: &List.flatten/2)) ==
quote(do: &:"Elixir.List".flatten/2) quote(do: &:"Elixir.List".flatten/2)
@@ -95,6 +95,12 @@ defmodule Kernel.FnTest do
assert (&mod.flatten/1) == (&List.flatten/1) assert (&mod.flatten/1) == (&List.flatten/1)
end 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 test "local partial application" do
assert (&atb(&1, :utf8)).(:a) == "a" assert (&atb(&1, :utf8)).(:a) == "a"
assert (&atb(List.to_atom(&1), :utf8)).(~c"a") == "a" assert (&atb(List.to_atom(&1), :utf8)).(~c"a") == "a"
@@ -421,40 +421,6 @@ defmodule Kernel.ParallelCompilerTest do
end) end)
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 test "does not use incorrect line number when error originates in another file" do
File.mkdir_p!(tmp_path()) File.mkdir_p!(tmp_path())
@@ -587,33 +553,5 @@ defmodule Kernel.ParallelCompilerTest do
"cannot compile module WithBehaviourAndStruct (errors have been logged)" "cannot compile module WithBehaviourAndStruct (errors have been logged)"
end) =~ expected_msg end) =~ expected_msg
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
"""
)
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
end end
+15 -1
View File
@@ -190,7 +190,7 @@ defmodule Kernel.WarningTest do
) )
assert_warn_eval( 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]/ ~s/["foo": :bar]/
) )
@@ -264,6 +264,20 @@ defmodule Kernel.WarningTest do
purge(Sample) purge(Sample)
end 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 test "unused compiler variable" do
output = output =
capture_eval(""" capture_eval("""
+2
View File
@@ -1136,6 +1136,8 @@ defmodule KernelTest do
test "pop_in/1" do test "pop_in/1" do
users = %{"john" => %{age: 27}, "meg" => %{age: 23}} 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"][:age]) == {27, %{"john" => %{}, "meg" => %{age: 23}}}
assert pop_in(users["john"][:name]) == {nil, %{"john" => %{age: 27}, "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}}} assert pop_in(users["bob"][:age]) == {nil, %{"john" => %{age: 27}, "meg" => %{age: 23}}}
@@ -1184,7 +1184,7 @@ defmodule Module.Types.DescrTest do
test "boolean" do test "boolean" do
assert boolean() |> to_quoted_string() == "boolean()" 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 atom([true, :a]) |> to_quoted_string() == ":a or true"
assert difference(atom(), boolean()) |> to_quoted_string() == "atom() and not boolean()" assert difference(atom(), boolean()) |> to_quoted_string() == "atom() and not boolean()"
end end
@@ -1199,7 +1199,7 @@ defmodule Module.Types.DescrTest do
assert intersection(atom(), dynamic()) |> to_quoted_string() == "dynamic(atom())" assert intersection(atom(), dynamic()) |> to_quoted_string() == "dynamic(atom())"
assert union(atom([:foo, :bar]), dynamic()) |> to_quoted_string() == 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() == assert intersection(dynamic(), closed_map(a: integer())) |> to_quoted_string() ==
"dynamic(%{a: integer()})" "dynamic(%{a: integer()})"
@@ -1256,7 +1256,7 @@ defmodule Module.Types.DescrTest do
assert open_tuple([integer(), atom()]) |> to_quoted_string() == "{integer(), atom(), ...}" assert open_tuple([integer(), atom()]) |> to_quoted_string() == "{integer(), atom(), ...}"
assert union(tuple([integer(), atom()]), open_tuple([atom()])) |> to_quoted_string() == 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() == assert difference(tuple([integer(), atom()]), open_tuple([atom()])) |> to_quoted_string() ==
"{integer(), atom()}" "{integer(), atom()}"
@@ -1268,6 +1268,96 @@ defmodule Module.Types.DescrTest do
# assert difference(tuple([number(), term()]), tuple([integer(), atom()])) # assert difference(tuple([number(), term()]), tuple([integer(), atom()]))
# |> to_quoted_string() == # |> to_quoted_string() ==
# "{float(), term()} or {number(), term() and not atom()}" # "{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 end
test "map" do test "map" do
@@ -1311,6 +1401,50 @@ defmodule Module.Types.DescrTest do
assert difference(open_map(a: number(), b: atom()), open_map(a: integer())) assert difference(open_map(a: number(), b: atom()), open_map(a: integer()))
|> to_quoted_string() == "%{..., a: float(), b: atom()}" |> 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 # Test complex combinations
assert intersection(open_map(a: number(), b: atom()), open_map(a: integer(), c: boolean())) assert intersection(open_map(a: number(), b: atom()), open_map(a: integer(), c: boolean()))
|> union(difference(open_map(x: atom()), open_map(x: boolean()))) |> union(difference(open_map(x: atom()), open_map(x: boolean())))
@@ -243,6 +243,35 @@ defmodule Module.Types.ExprTest do
""" """
end 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 test "capture a function with non atoms" do
assert typeerror!([<<x::integer>>], &x.foo_bar/2) == assert typeerror!([<<x::integer>>], &x.foo_bar/2) ==
~l""" ~l"""
@@ -798,9 +827,9 @@ defmodule Module.Types.ExprTest do
x.foo_bar 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(), authority: term(),
fragment: term(), fragment: term(),
host: term(), host: term(),
@@ -810,6 +839,10 @@ defmodule Module.Types.ExprTest do
scheme: term(), scheme: term(),
userinfo: term() userinfo: term()
}) })
where "x" was given the type:
# type: dynamic(%URI{})
# from: types_test.ex:LINE-4:43 # from: types_test.ex:LINE-4:43
x = %URI{} x = %URI{}
""" """
@@ -827,7 +860,11 @@ defmodule Module.Types.ExprTest do
test "in dynamic mode" 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!([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 end
test "min/max" do test "min/max" do
@@ -845,6 +882,10 @@ defmodule Module.Types.ExprTest do
min(x, y) min(x, y)
given types:
min(dynamic(:foo), integer())
where "x" was given the type: where "x" was given the type:
# type: dynamic(:foo) # type: dynamic(:foo)
@@ -868,6 +909,10 @@ defmodule Module.Types.ExprTest do
x === y x === y
given types:
integer() === float()
where "x" was given the type: where "x" was given the type:
# type: integer() # type: integer()
@@ -893,6 +938,10 @@ defmodule Module.Types.ExprTest do
mod.<=(x, y) mod.<=(x, y)
given types:
dynamic(:foo) <= dynamic(%Point{})
where "mod" was given the type: where "mod" was given the type:
# type: dynamic(Kernel) # type: dynamic(Kernel)
@@ -907,7 +956,7 @@ defmodule Module.Types.ExprTest do
where "y" was given the type: where "y" was given the type:
# type: dynamic(%Point{x: term(), y: term(), z: term()}) # type: dynamic(%Point{})
# from: types_test.ex:LINE-2 # from: types_test.ex:LINE-2
y = %Point{} y = %Point{}
@@ -1015,6 +1064,15 @@ defmodule Module.Types.ExprTest do
end end
describe "case" do 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 test "returns unions of all clauses" do
assert typecheck!( assert typecheck!(
[x], [x],
@@ -1070,7 +1128,7 @@ defmodule Module.Types.ExprTest do
end end
describe "conditionals" do describe "conditionals" do
test "if does not report on literal atoms" do test "if does not report on literals" do
assert typecheck!( assert typecheck!(
if true do if true do
:ok :ok
@@ -1078,17 +1136,9 @@ defmodule Module.Types.ExprTest do
) == atom([:ok, nil]) ) == atom([:ok, nil])
end end
test "and does not report on literal atoms" do test "and does not report on literals" do
assert typecheck!(false and true) == boolean() assert typecheck!(false and true) == boolean()
end end
test "and reports on non-atom literals" do
assert typeerror!(1 and true) == ~l"""
the following conditional expression will always evaluate to integer():
1
"""
end
end end
describe "receive" do describe "receive" do
@@ -1224,16 +1274,18 @@ defmodule Module.Types.ExprTest do
e e
end end
) == ) ==
union( dynamic(
closed_map( union(
__struct__: atom([ArgumentError]), closed_map(
__exception__: atom([true]), __struct__: atom([ArgumentError]),
message: term() __exception__: atom([true]),
), message: term()
closed_map( ),
__struct__: atom([RuntimeError]), closed_map(
__exception__: atom([true]), __struct__: atom([RuntimeError]),
message: term() __exception__: atom([true]),
message: term()
)
) )
) )
end end
@@ -1382,6 +1434,16 @@ defmodule Module.Types.ExprTest do
assert typewarn!(:string.__info__(:functions)) == assert typewarn!(:string.__info__(:functions)) ==
{dynamic(), ":string.__info__/1 is undefined or private"} {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 end
test "behaviour_info/1" do test "behaviour_info/1" do
@@ -36,7 +36,7 @@ defmodule Module.Types.IntegrationTest do
""" """
} }
modules = compile(files) modules = compile_modules(files)
assert [ assert [
{{:c, 0}, %{}}, {{:c, 0}, %{}},
@@ -195,6 +195,85 @@ defmodule Module.Types.IntegrationTest do
assert_warnings(files, warnings) assert_warnings(files, warnings)
end 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 "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 end
describe "undefined warnings" do describe "undefined warnings" do
@@ -932,26 +1011,39 @@ defmodule Module.Types.IntegrationTest do
defp capture_compile_warnings(files) do defp capture_compile_warnings(files) do
in_tmp(fn -> in_tmp(fn ->
paths = generate_files(files) paths = generate_files(files)
capture_io(:stderr, fn -> compile_files(paths) end) capture_io(:stderr, fn -> compile_to_path(paths) end)
end) end)
end end
defp compile(files) do defp with_compile_warnings(files) do
in_tmp(fn -> in_tmp(fn ->
paths = generate_files(files) paths = generate_files(files)
compile_files(paths) with_io(:stderr, fn -> compile_to_path(paths) end) |> elem(0)
end) end)
end end
defp compile_files(paths) do defp compile_modules(files) do
{:ok, modules, _warnings} = Kernel.ParallelCompiler.compile_to_path(paths, ".") in_tmp(fn ->
paths = generate_files(files)
{modules, _warnings} = compile_to_path(paths)
Map.new(modules, fn module -> Map.new(modules, fn module ->
{^module, binary, _filename} = :code.get_object_code(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.delete(module)
:code.purge(module) :code.purge(module)
{module, binary} end
end)
{modules, warnings}
end end
defp generate_files(files) do defp generate_files(files) do
@@ -166,9 +166,13 @@ defmodule Module.Types.PatternTest do
x.foo_bar 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: where "x" was given the type:
# type: dynamic(%Point{x: term(), y: term(), z: term()}) # type: dynamic(%Point{})
# from: types_test.ex:LINE-1 # from: types_test.ex:LINE-1
x = %Point{} x = %Point{}
""" """
+1 -1
View File
@@ -35,7 +35,7 @@ defmodule RangeTest do
assert ExUnit.CaptureIO.capture_io(:stderr, fn -> assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
assert Range.new(3, 1) == 3..1//-1 assert Range.new(3, 1) == 3..1//-1
end) =~ "has a default step of -1" end) =~ "default to a step of -1"
end end
test "fields" do test "fields" do
+2 -4
View File
@@ -710,7 +710,8 @@ defmodule TypespecTest do
@type my_type :: (... -> any) @type my_type :: (... -> any)
end end
assert [type: {:my_type, {:type, _, :fun, []}, []}] = types(bytecode) assert [type: {:my_type, {:type, _, :fun, [{:type, _, :any}, {:type, _, :any, []}]}, []}] =
types(bytecode)
end end
test "@type with a fun with multiple arguments and return type" do test "@type with a fun with multiple arguments and return type" do
@@ -1520,9 +1521,6 @@ defmodule TypespecTest do
assert ast_string == assert ast_string ==
"@type literal_struct_all_fields_key_type() :: %TypespecTest.SomeStruct{key: integer()}" "@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, _, _} -> {:built_in_nonempty_list, _, _} ->
assert ast_string == "@type built_in_nonempty_list() :: [...]" assert ast_string == "@type built_in_nonempty_list() :: [...]"
+1 -1
View File
@@ -786,7 +786,7 @@ defmodule ExUnit.Assertions do
## Examples ## Examples
assert_raise ArithmeticError, "bad argument in arithmetic expression", fn -> assert_raise ArithmeticError, "bad argument in arithmetic expression", fn ->
1 + "test" 1 / 0
end end
assert_raise RuntimeError, ~r/^today's lucky number is 0\.\d+!$/, fn -> assert_raise RuntimeError, ~r/^today's lucky number is 0\.\d+!$/, fn ->
+6 -1
View File
@@ -315,6 +315,11 @@ defmodule ExUnit.Case do
end end
end end
@keys [:async, :group, :parameterize, :register]
@doc false
def __keys__(opts), do: Keyword.take(opts, @keys)
@doc false @doc false
def __register__(module, opts) do def __register__(module, opts) do
if not Keyword.keyword?(opts) do if not Keyword.keyword?(opts) do
@@ -324,7 +329,7 @@ defmodule ExUnit.Case do
end end
{register?, opts} = Keyword.pop(opts, :register, true) {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 if opts != [] do
IO.warn("unknown options given to ExUnit.Case: #{inspect(opts)}") IO.warn("unknown options given to ExUnit.Case: #{inspect(opts)}")
+1 -1
View File
@@ -83,7 +83,7 @@ defmodule ExUnit.CaseTemplate do
# We inject this code in the module that calls "use MyTemplate". # We inject this code in the module that calls "use MyTemplate".
def __proxy__(module, opts) do def __proxy__(module, opts) do
quote do quote do
use ExUnit.Case, unquote(opts) use ExUnit.Case, ExUnit.Case.__keys__(unquote(opts))
setup_all context do setup_all context do
unquote(module).__ex_unit__(:setup_all, context) unquote(module).__ex_unit__(:setup_all, context)
+2 -2
View File
@@ -125,7 +125,7 @@ defmodule ExUnit.Runner do
# Run all sync modules directly # Run all sync modules directly
for pair <- sync_modules do 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) running != %{} and wait_until_available(config, running)
end end
@@ -161,7 +161,7 @@ defmodule ExUnit.Runner do
running running
end 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 if max_failures_reached?(config) do
running running
else else
+55 -63
View File
@@ -57,9 +57,10 @@ defmodule ExUnit.Server do
state = %{ state = %{
loaded: System.monotonic_time(), loaded: System.monotonic_time(),
waiting: nil, waiting: nil,
async_groups: %{}, groups: %{},
async_groups: [],
async_modules: :queue.new(), async_modules: :queue.new(),
sync_modules: :queue.new() sync_modules: []
} }
{:ok, state} {:ok, state}
@@ -72,31 +73,31 @@ defmodule ExUnit.Server do
# Called once after all async modules have been sent and reverts the state. # Called once after all async modules have been sent and reverts the state.
def handle_call(:take_sync_modules, _from, state) do def handle_call(:take_sync_modules, _from, state) do
%{waiting: nil, loaded: :done, async_modules: async_modules} = state %{waiting: nil, loaded: :done, async_groups: []} = state
0 = :queue.len(async_modules) true = :queue.is_empty(state.async_modules)
{:reply, :queue.to_list(state.sync_modules), {:reply, state.sync_modules, %{state | sync_modules: [], loaded: System.monotonic_time()}}
%{state | sync_modules: :queue.new(), loaded: System.monotonic_time()}}
end end
# Called by the runner when --repeat-until-failure is used. # Called by the runner when --repeat-until-failure is used.
def handle_call({:restore_modules, async_modules, sync_modules}, _from, state) do def handle_call({:restore_modules, async_modules, sync_modules}, _from, state) do
{async_modules, async_groups} = {async_modules, async_groups, groups} =
Enum.map_reduce(async_modules, %{}, fn Enum.reduce(async_modules, {[], [], []}, fn
{nil, [module]}, {modules, groups} -> {nil, [module]}, {async_modules, async_groups, groups} ->
{[{:module, module} | modules], groups} {[module | async_modules], async_groups, groups}
{group, group_modules}, {modules, groups} -> {group, group_modules}, {async_modules, async_groups, groups} ->
{[{:group, group} | modules], Map.put(groups, group, group_modules)} {async_modules, [group | async_groups], [{group, group_modules} | groups]}
end) end)
{:reply, :ok, {:reply, :ok,
%{ %{
state state
| loaded: :done, | loaded: :done,
groups: Map.new(groups),
async_groups: async_groups, async_groups: async_groups,
async_modules: :queue.from_list(async_modules), async_modules: :queue.from_list(async_modules),
sync_modules: :queue.from_list(sync_modules) sync_modules: sync_modules
}} }}
end end
@@ -108,22 +109,24 @@ defmodule ExUnit.Server do
when is_integer(loaded) do when is_integer(loaded) do
state = state =
if uniq? do if uniq? do
async_groups = groups = Map.new(state.groups, fn {group, modules} -> {group, Enum.uniq(modules)} end)
Map.new(state.async_groups, fn {group, modules} -> async_groups = state.async_groups |> Enum.uniq() |> Enum.reverse()
{group, Enum.uniq(modules)}
end)
async_modules = :queue.to_list(state.async_modules) |> Enum.uniq() |> :queue.from_list() 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 state
| async_groups: async_groups, | groups: groups,
async_groups: async_groups,
async_modules: async_modules, async_modules: async_modules,
sync_modules: sync_modules sync_modules: sync_modules
} }
else else
state %{
state
| async_groups: Enum.reverse(state.async_groups),
sync_modules: Enum.reverse(state.sync_modules)
}
end end
diff = System.convert_time_unit(System.monotonic_time() - loaded, :native, :microsecond) 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) def handle_call({:add, false = _async, _group, names}, _from, %{loaded: loaded} = state)
when is_integer(loaded) do when is_integer(loaded) do
state = state = update_in(state.sync_modules, &Enum.reverse(names, &1))
update_in(state.sync_modules, &Enum.reduce(names, &1, fn name, q -> :queue.in(name, q) end))
{:reply, :ok, state} {:reply, :ok, state}
end end
@@ -143,7 +144,7 @@ defmodule ExUnit.Server do
state = state =
update_in( update_in(
state.async_modules, 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)} {: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) def handle_call({:add, true = _async, group, names}, _from, %{loaded: loaded} = state)
when is_integer(loaded) do when is_integer(loaded) do
{async_groups, async_modules} = {groups, async_groups} =
case state.async_groups do case state.groups do
%{^group => entries} = async_groups -> %{^group => entries} = groups ->
{%{async_groups | group => names ++ entries}, state.async_modules} {%{groups | group => Enum.reverse(names, entries)}, state.async_groups}
%{} = async_groups -> %{} = groups ->
{Map.put(async_groups, group, names), :queue.in({:group, group}, state.async_modules)} {Map.put(groups, group, names), [group | state.async_groups]}
end end
{:reply, :ok, {:reply, :ok, take_modules(%{state | groups: groups, async_groups: async_groups})}
take_modules(%{state | async_groups: async_groups, async_modules: async_modules})}
end end
def handle_call({:add, _async?, _group, _names}, _from, state) do def handle_call({:add, _async?, _group, _names}, _from, state) do
@@ -173,50 +173,42 @@ defmodule ExUnit.Server do
end end
defp take_modules(%{waiting: {from, count}} = state) do defp take_modules(%{waiting: {from, count}} = state) do
has_async_modules? = not :queue.is_empty(state.async_modules)
cond do 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) GenServer.reply(from, nil)
%{state | waiting: nil} %{state | waiting: nil}
not has_async_modules? ->
state
true -> true ->
{async_modules, remaining_modules} = take_until(count, state.async_modules) state
{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
}
end end
end end
# :queue.split fails if the provided count is larger than the queue size; # :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 # We also want to return the values as tuples of shape {group, [modules]}.
# return {list, queue} instead of {queue, queue}
defp take_until(n, queue), do: take_until(n, queue, []) defp take_until(n, queue), do: take_until(n, queue, [])
defp take_until(0, queue, acc), do: {Enum.reverse(acc), queue} defp take_until(0, queue, acc), do: {Enum.reverse(acc), queue}
defp take_until(n, queue, acc) do defp take_until(n, queue, acc) do
case :queue.out(queue) 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} {:empty, queue} -> {Enum.reverse(acc), queue}
end end
end end
@@ -36,7 +36,7 @@ defmodule ExUnit.NestedCase do
end end
defmodule ExUnit.CaseTemplateTest do defmodule ExUnit.CaseTemplateTest do
use ExUnit.SampleCase, async: true use ExUnit.SampleCase, async: true, another_option: 123
use ExUnit.NestedCase use ExUnit.NestedCase
two = 2 two = 2
+18
View File
@@ -997,6 +997,24 @@ defmodule ExUnitTest do
assert length(runs) == 6 assert length(runs) == 6
end 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 test "stops on failure" do
{:ok, pid} = Agent.start_link(fn -> 0 end) {:ok, pid} = Agent.start_link(fn -> 0 end)
Process.register(pid, :ex_unit_repeat_until_failure_count) Process.register(pid, :ex_unit_repeat_until_failure_count)
+16 -12
View File
@@ -391,16 +391,17 @@ defmodule IEx do
The supported options are: The supported options are:
* `:colors` * `:auto_reload`
* `:inspect`
* `:width`
* `:history_size`
* `:default_prompt`
* `:continuation_prompt`
* `:alive_prompt`
* `:alive_continuation_prompt` * `:alive_continuation_prompt`
* `:parser` * `:alive_prompt`
* `:colors`
* `:continuation_prompt`
* `:default_prompt`
* `:dot_iex` * `:dot_iex`
* `:history_size`
* `:inspect`
* `:parser`
* `:width`
They are discussed individually in the sections below. They are discussed individually in the sections below.
@@ -488,9 +489,6 @@ defmodule IEx do
* `:alive_continuation_prompt` - used when `Node.alive?/0` returns * `:alive_continuation_prompt` - used when `Node.alive?/0` returns
`true` and more input is expected `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: The following values in the prompt string will be replaced appropriately:
* `%counter` - the index of the history * `%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. 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. 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`). 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 @spec configure(keyword()) :: :ok
def configure(options) do def configure(options) do
+11 -10
View File
@@ -208,14 +208,18 @@ defmodule Mix.Compilers.Elixir do
end end
Mix.Task.Compiler.notify_modules_compiled(lazy_modules_diff) Mix.Task.Compiler.notify_modules_compiled(lazy_modules_diff)
unless_warnings_as_errors(opts, {:ok, all_warnings})
unless_previous_warnings_as_errors(previous_warnings, opts, {:ok, all_warnings})
{:error, errors, %{runtime_warnings: r_warnings, compile_warnings: c_warnings}, state} -> {: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. # In case of errors, we show all previous warnings and all new ones.
{_, _, sources, _, _, _} = state {_, _, sources, _, _, _} = state
errors = Enum.map(errors, &diagnostic/1) 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 == []) all_warnings = Keyword.get(opts, :all_warnings, errors == [])
{:error, previous_warnings(sources, all_warnings) ++ warnings ++ errors} {:error, previous_warnings(sources, all_warnings) ++ warnings ++ errors}
after after
@@ -254,8 +258,7 @@ defmodule Mix.Compilers.Elixir do
all_warnings = Keyword.get(opts, :all_warnings, true) all_warnings = Keyword.get(opts, :all_warnings, true)
previous_warnings = previous_warnings(sources, all_warnings) previous_warnings = previous_warnings(sources, all_warnings)
unless_warnings_as_errors(opts, {status, previous_warnings})
unless_previous_warnings_as_errors(previous_warnings, opts, {status, previous_warnings})
end end
end end
@@ -1012,8 +1015,8 @@ defmodule Mix.Compilers.Elixir do
File.rm(manifest <> ".checkpoint") File.rm(manifest <> ".checkpoint")
end end
defp unless_previous_warnings_as_errors(previous_warnings, opts, {status, all_warnings}) do defp unless_warnings_as_errors(opts, {status, all_warnings}) do
if previous_warnings != [] and opts[:warnings_as_errors] do if all_warnings != [] and opts[:warnings_as_errors] do
message = "Compilation failed due to warnings while using the --warnings-as-errors option" message = "Compilation failed due to warnings while using the --warnings-as-errors option"
IO.puts(:stderr, message) IO.puts(:stderr, message)
{:error, all_warnings} {:error, all_warnings}
@@ -1049,7 +1052,6 @@ defmodule Mix.Compilers.Elixir do
threshold = opts[:long_compilation_threshold] || 10 threshold = opts[:long_compilation_threshold] || 10
profile = opts[:profile] profile = opts[:profile]
verbose = opts[:verbose] || false verbose = opts[:verbose] || false
warnings_as_errors = opts[:warnings_as_errors] || false
pid = pid =
spawn_link(fn -> spawn_link(fn ->
@@ -1071,8 +1073,7 @@ defmodule Mix.Compilers.Elixir do
long_compilation_threshold: threshold, long_compilation_threshold: threshold,
profile: profile, profile: profile,
beam_timestamp: timestamp, beam_timestamp: timestamp,
return_diagnostics: true, return_diagnostics: true
warnings_as_errors: warnings_as_errors
] ]
response = Kernel.ParallelCompiler.compile_to_path(stale, dest, compile_opts) response = Kernel.ParallelCompiler.compile_to_path(stale, dest, compile_opts)
+3 -3
View File
@@ -100,12 +100,12 @@ defmodule Mix.Shell.IO do
""" """
def cmd(command, opts \\ []) do def cmd(command, opts \\ []) do
print_app? = Keyword.get(opts, :print_app, true) print_app? = Keyword.get(opts, :print_app, true)
windows? = match?({:win32, _}, :os.type())
Mix.Shell.cmd(command, opts, fn data -> Mix.Shell.cmd(command, opts, fn data ->
if print_app?, do: print_app() if print_app?, do: print_app()
# Due to encoding of shell command on Windows, # Due to encoding of shell command on Windows, write the data as is.
# let's write the data as is if windows?, do: IO.binwrite(data), else: IO.write(data)
IO.binwrite(data)
end) end)
end end
end end
+7
View File
@@ -189,6 +189,13 @@ defmodule Mix.Tasks.Compile.App do
Mix.Project.ensure_structure() Mix.Project.ensure_structure()
File.write!(target, IO.chardata_to_string(contents)) File.write!(target, IO.chardata_to_string(contents))
File.touch!(target, new_mtime) File.touch!(target, new_mtime)
# If we just created the .app file, it will have touched
# the directory mtime, so we need to reset it.
if current_properties == [] do
File.touch!(compile_path, new_mtime)
end
Mix.shell().info("Generated #{app} app") Mix.shell().info("Generated #{app} app")
{:ok, []} {:ok, []}
else else
+35 -16
View File
@@ -48,6 +48,12 @@ defmodule Mix.Tasks.Deps.Loadpaths do
Mix.Task.run("archive.check", args) Mix.Task.run("archive.check", args)
end end
config = Mix.Project.config()
if "--no-elixir-version-check" not in args do
check_elixir_version(config)
end
all = Mix.Dep.load_and_cache() all = Mix.Dep.load_and_cache()
all = all =
@@ -57,14 +63,8 @@ defmodule Mix.Tasks.Deps.Loadpaths do
all all
end end
config = Mix.Project.config()
if "--no-elixir-version-check" not in args do
check_elixir_version(config)
end
if "--no-deps-check" not in args do if "--no-deps-check" not in args do
deps_check(all, "--no-compile" in args) deps_check(config, all, "--no-compile" in args)
end end
Code.prepend_paths(Enum.flat_map(all, &Mix.Dep.load_paths/1), cache: true) Code.prepend_paths(Enum.flat_map(all, &Mix.Dep.load_paths/1), cache: true)
@@ -93,25 +93,38 @@ defmodule Mix.Tasks.Deps.Loadpaths do
end end
end end
defp deps_check(config, all, no_compile?) do
with {:compile, _to_compile} <- deps_check(all, no_compile?) do
# We need to compile, we first grab the lock, then, we check
# again and compile if still applicable
Mix.Project.with_build_lock(config, fn ->
all = reload_deps(all)
with {:compile, to_compile} <- deps_check(all, no_compile?) do
Mix.Tasks.Deps.Compile.compile(to_compile)
to_compile
|> reload_deps()
|> Enum.filter(&(not Mix.Dep.ok?(&1)))
|> show_not_ok!()
end
end)
end
end
defp deps_check(all, no_compile?) do defp deps_check(all, no_compile?) do
all = Enum.map(all, &check_lock/1) all = Enum.map(all, &check_lock/1)
{not_ok, compile} = partition(all, [], []) {not_ok, to_compile} = partition(all, [], [])
cond do cond do
not_ok != [] -> not_ok != [] ->
show_not_ok!(not_ok) show_not_ok!(not_ok)
compile == [] or no_compile? -> to_compile == [] or no_compile? ->
:ok :ok
true -> true ->
Mix.Tasks.Deps.Compile.compile(compile) {:compile, to_compile}
compile
|> Enum.map(& &1.app)
|> Mix.Dep.filter_by_name(Mix.Dep.load_and_cache())
|> Enum.filter(&(not Mix.Dep.ok?(&1)))
|> show_not_ok!()
end end
end end
@@ -136,6 +149,12 @@ defmodule Mix.Tasks.Deps.Loadpaths do
{Enum.reverse(not_ok), Enum.reverse(compile)} {Enum.reverse(not_ok), Enum.reverse(compile)}
end end
defp reload_deps(deps) do
deps
|> Enum.map(& &1.app)
|> Mix.Dep.filter_by_name(Mix.Dep.load_and_cache())
end
# Those are compiled by umbrella. # Those are compiled by umbrella.
defp from_umbrella?(dep) do defp from_umbrella?(dep) do
dep.opts[:from_umbrella] dep.opts[:from_umbrella]
+19 -6
View File
@@ -58,12 +58,25 @@ defmodule Mix.Tasks.Do do
{apps, args} = extract_apps_from_args(args) {apps, args} = extract_apps_from_args(args)
show_forgotten_apps_warning(apps) show_forgotten_apps_warning(apps)
Enum.each(gather_commands(args), fn [task | args] -> Enum.each(gather_commands(args), fn
if apps == [] do [task | args] ->
Mix.Task.run(task, args) if apps == [] do
else Mix.Task.run(task, args)
Mix.Task.run_in_apps(task, apps, args) else
end Mix.Task.run_in_apps(task, apps, args)
end
[] ->
Mix.raise("""
One of the commands passed to "mix do" is empty. Each command passed to "mix do" must \
have at least the task name. These are all invalid:
mix do
mix do my_task +
mix do + my_task
Run "mix help do" for more information.
""")
end) end)
end end
+1 -1
View File
@@ -342,7 +342,7 @@ defmodule Mix.Tasks.Escript.Build do
{zip_path, consolidated[Path.basename(path)] || path} {zip_path, consolidated[Path.basename(path)] || path}
end end
else else
[] files
end end
end end
@@ -66,8 +66,6 @@ defmodule Mix.Tasks.Compile.AppTest do
test "generates .app file when changes happen" do test "generates .app file when changes happen" do
in_fixture("no_mixfile", fn -> in_fixture("no_mixfile", fn ->
Mix.Project.push(MixTest.Case.Sample) Mix.Project.push(MixTest.Case.Sample)
# Pre-create the compilation path to avoid mtime races
File.mkdir_p(Mix.Project.compile_path())
Mix.Tasks.Compile.Elixir.run([]) Mix.Tasks.Compile.Elixir.run([])
assert Mix.Tasks.Compile.App.run([]) == {:ok, []} assert Mix.Tasks.Compile.App.run([]) == {:ok, []}
@@ -1045,12 +1045,15 @@ defmodule Mix.Tasks.Compile.ElixirTest do
# Compiles with missing external resources # Compiles with missing external resources
file = Path.absname("lib/a.eex") file = Path.absname("lib/a.eex")
source = Path.absname("lib/a.ex")
assert capture_io(:stderr, fn -> assert capture_io(:stderr, fn ->
assert {:ok, [%Mix.Task.Compiler.Diagnostic{file: ^file, position: 13}]} = assert {:ok,
[%Mix.Task.Compiler.Diagnostic{source: ^source, file: ^file, position: 13}]} =
Mix.Tasks.Compile.Elixir.run([]) Mix.Tasks.Compile.Elixir.run([])
assert {:noop, [%Mix.Task.Compiler.Diagnostic{file: ^file, position: 13}]} = assert {:noop,
[%Mix.Task.Compiler.Diagnostic{source: ^source, file: ^file, position: 13}]} =
Mix.Tasks.Compile.Elixir.run(["--all-warnings"]) Mix.Tasks.Compile.Elixir.run(["--all-warnings"])
end) =~ "oops" end) =~ "oops"
+8
View File
@@ -13,6 +13,14 @@ defmodule Mix.Tasks.DoTest do
end) end)
end end
test "raises if a task is empty" do
for args <- [~w(), ~w(+), ~w(help +), ~w(+ help)] do
assert_raise Mix.Error, ~r"^One of the commands passed to \"mix do\" is empty", fn ->
Mix.Tasks.Do.run(args)
end
end
end
test "gather_command returns a list of commands" do test "gather_command returns a list of commands" do
assert gather_commands(["help", "+", "compile"]) == assert gather_commands(["help", "+", "compile"]) ==
[["help"], ["compile"]] [["help"], ["compile"]]
+14
View File
@@ -50,6 +50,20 @@ defmodule Mix.Tasks.EscriptTest do
end) end)
end end
test "generate escript without protocol consolidation" do
in_fixture("escript_test", fn ->
push_project_with_config(Escript, consolidate_protocols: false)
Mix.Tasks.Escript.Build.run([])
assert_received {:mix_shell, :info, ["Generated escript escript_test with MIX_ENV=dev"]}
assert System.cmd("escript", ["escript_test"]) == {"TEST\n", 0}
assert count_abstract_code("escript_test") == 0
# Does not consolidate protocols
assert System.cmd("escript", ["escript_test", "--protocol", "Enumerable"]) == {"false\n", 0}
end)
end
test "generate escript with --no-compile option" do test "generate escript with --no-compile option" do
in_fixture("escript_test", fn -> in_fixture("escript_test", fn ->
push_project_with_config(Escript) push_project_with_config(Escript)