diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 978f36a2f9e..5b44545fe87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }} strategy: matrix: - otp_version: ["25.3", "26.2", "27.1"] + otp_version: ["25.3", "26.2", "27.3"] runs-on: windows-2022 steps: - name: Configure Git diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 23557c18b44..2efac4bbcd2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,7 +55,6 @@ jobs: git push origin $ref_name --force build: - needs: create_draft_release strategy: fail-fast: true matrix: @@ -80,26 +79,6 @@ jobs: otp: ${{ matrix.otp }} build_docs: ${{ matrix.build_docs }} - - name: "Attest release .exe provenance" - uses: actions/attest-build-provenance@v2 - id: attest-exe-provenance - with: - subject-path: "elixir-otp-${{ matrix.otp }}.exe" - - name: "Copy release .exe provenance" - run: cp "$ATTESTATION" elixir-otp-${{ matrix.otp }}.exe.sigstore - env: - ATTESTATION: "${{ steps.attest-exe-provenance.outputs.bundle-path }}" - - - name: "Attest release .zip provenance" - uses: actions/attest-build-provenance@v2 - id: attest-zip-provenance - with: - subject-path: "elixir-otp-${{ matrix.otp }}.zip" - - name: "Copy release .zip provenance" - run: cp "$ATTESTATION" elixir-otp-${{ matrix.otp }}.zip.sigstore - env: - ATTESTATION: "${{ steps.attest-zip-provenance.outputs.bundle-path }}" - - name: "Attest docs provenance" uses: actions/attest-build-provenance@v2 id: attest-docs-provenance @@ -112,11 +91,23 @@ jobs: env: ATTESTATION: "${{ steps.attest-docs-provenance.outputs.bundle-path }}" - - name: "Upload release artifacts" + - name: Create Docs Hashes + if: ${{ matrix.build_docs }} + run: | + shasum -a 1 Docs.zip > Docs.zip.sha1sum + shasum -a 256 Docs.zip > Docs.zip.sha256sum + + - name: "Upload linux release artifacts" uses: actions/upload-artifact@v4 with: - name: elixir-otp-${{ matrix.otp }} - path: elixir-otp-${{ matrix.otp }}* + name: build-linux-elixir-otp-${{ matrix.otp }} + path: elixir-otp-${{ matrix.otp }}.zip + + - name: "Upload windows release artifacts" + uses: actions/upload-artifact@v4 + with: + name: build-windows-elixir-otp-${{ matrix.otp }} + path: elixir-otp-${{ matrix.otp }}.exe - name: "Upload doc artifacts" uses: actions/upload-artifact@v4 @@ -125,20 +116,26 @@ jobs: name: Docs path: Docs.zip* - upload-release: - needs: build - runs-on: windows-2022 + sign: + needs: [build] + strategy: + fail-fast: true + matrix: + otp: [25, 26, 27] + flavor: [windows, linux] + + env: + RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }} + + runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-22.04' || 'windows-2022' }} steps: - uses: actions/download-artifact@v4 - - - run: | - mv elixir-otp-*/* . - mv Docs/* . - shell: bash + with: + name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }} - name: "Sign files with Trusted Signing" - if: github.repository == 'elixir-lang/elixir' + if: github.repository == 'elixir-lang/elixir' && matrix.flavor == 'windows' uses: azure/trusted-signing-action@v0.5.0 with: azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -153,6 +150,50 @@ jobs: timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 + - name: "Attest release provenance" + uses: actions/attest-build-provenance@v2 + id: attest-provenance + with: + subject-path: ${{ env.RELEASE_FILE }} + - name: "Copy release .zip provenance" + shell: bash + run: cp "$ATTESTATION" "${RELEASE_FILE}.sigstore" + env: + ATTESTATION: "${{ steps.attest-provenance.outputs.bundle-path }}" + + - name: Create Release Hashes + if: matrix.flavor == 'windows' + shell: pwsh + run: | + $sha1 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA1 + $sha1.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha1sum" + + $sha256 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA256 + $sha256.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha256sum" + + - name: Create Release Hashes + if: matrix.flavor == 'linux' + shell: bash + run: | + shasum -a 1 "$RELEASE_FILE" > "${RELEASE_FILE}.sha1sum" + shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum" + + - name: "Upload linux release artifacts" + uses: actions/upload-artifact@v4 + with: + name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }} + path: ${{ env.RELEASE_FILE }}* + + upload-release: + needs: [create_draft_release, build, sign] + runs-on: ubuntu-22.04 + + steps: + - uses: actions/download-artifact@v4 + with: + pattern: "{sign-*-elixir-otp-*,Docs}" + merge-multiple: true + - name: Upload Pre-built shell: bash env: @@ -179,7 +220,7 @@ jobs: Docs.zip.sigstore upload-builds-hex-pm: - needs: build + needs: [build, sign] runs-on: ubuntu-22.04 concurrency: builds-hex-pm env: @@ -193,6 +234,9 @@ jobs: OTP_GENERIC_VERSION: "25" steps: - uses: actions/download-artifact@v4 + with: + pattern: "{sign-*-elixir-otp-*,Docs}" + merge-multiple: true - name: Init purge keys file run: | @@ -202,7 +246,6 @@ jobs: run: | ref_name=${{ github.ref_name }} - mv elixir-otp-*/* . for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do dest=${zip/elixir/${ref_name}} surrogate_key=${dest/.zip$/} @@ -221,7 +264,6 @@ jobs: done - name: Upload Docs to S3 - working-directory: Docs run: | version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g') diff --git a/.github/workflows/release_pre_built/action.yml b/.github/workflows/release_pre_built/action.yml index c7a12e0d1bf..3d6efd332af 100644 --- a/.github/workflows/release_pre_built/action.yml +++ b/.github/workflows/release_pre_built/action.yml @@ -19,8 +19,6 @@ runs: run: | make Precompiled.zip mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip - shasum -a 1 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha1sum - shasum -a 256 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha256sum echo "$PWD/bin" >> $GITHUB_PATH - name: Install NSIS shell: bash @@ -34,8 +32,6 @@ runs: export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip (cd lib/elixir/scripts/windows_installer && ./build.sh) mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe . - shasum -a 1 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha1sum - shasum -a 256 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha256sum - name: Get ExDoc ref if: ${{ inputs.build_docs }} shell: bash @@ -66,5 +62,3 @@ runs: run: | git fetch --tags make Docs.zip - shasum -a 1 Docs.zip > Docs.zip.sha1sum - shasum -a 256 Docs.zip > Docs.zip.sha256sum diff --git a/CHANGELOG.md b/CHANGELOG.md index 06439826071..a3c83b55b39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,13 @@ Elixir v1.18 is an impressive release with improvements across the two main effo ## Type system improvements -The most exciting change in Elixir v1.18 is type checking of function calls, alongside gradual inference of patterns and return types. To understand how this will impact your programs, consider the following code: +The most exciting change in Elixir v1.18 is type checking of function calls, alongside gradual inference of patterns and return types. To understand how this will impact your programs, consider the following code in "lib/user.ex": ```elixir defmodule User do defstruct [:age, :car_choice] - def drive(%User{age: age, car_choice: car}, cars_choices) when age >= 18 do + def drive(%User{age: age, car_choice: car}, car_choices) when age >= 18 do if car in car_choices do {:ok, car} else @@ -24,9 +24,9 @@ defmodule User do end ``` -Elixir's type system will infer the `drive` function expects a `%User{}` struct as input and returns either `{:ok, dynamic()}`, `{:error, :no_choice}`, or `{:error, :not_allowed}`. +Elixir's type system will infer that the `drive/2` function expects a `%User{}` struct and returns either `{:ok, dynamic()}`, `{:error, :no_choice}`, or `{:error, :not_allowed}`. -Therefore, the following code should emit a violation, due to an invalid argument: +Therefore, the following code in a separate module (either in a separate or the same file), should emit a violation, due to an invalid argument: ```elixir User.drive({:ok, %User{}}, car_choices) @@ -156,40 +156,34 @@ More migrations may be added in future releases. This release includes official support for JSON encoding and decoding. -Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and -[ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/) -standards. +Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and [ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/) standards. ### Encoding -Encoding can be done via `JSON.encode!/1` and `JSON.encode_to_iodata!/1` functions. -The default encoding rules are applied as follows: - -| **Elixir** | **JSON** | -|------------------------|----------| -| `integer() \| float()` | Number | -| `true \| false ` | Boolean | -| `nil` | Null | -| `binary()` | String | -| `atom()` | String | -| `list()` | Array | -| `%{binary() => _}` | Object | -| `%{atom() => _}` | Object | -| `%{integer() => _}` | Object | - -You may also implement the `JSON.Encoder` protocol for custom data structures. -If you have a struct, you can derive the implementation of the `JSON.Encoder` -by specifying which fields should be encoded to JSON: +Encoding can be done via `JSON.encode!/1` and `JSON.encode_to_iodata!/1` functions. The default encoding rules are applied as follows: + +| **Elixir** | **JSON** | +|-----------------------------|----------| +| `integer() \| float()` | Number | +| `true \| false ` | Boolean | +| `nil` | Null | +| `binary()` | String | +| `atom()` | String | +| `list()` | Array | +| `%{String.Chars.t() => _}` | Object | + +You may also implement the `JSON.Encoder` protocol for custom data structures. Elixir already implements the protocol for all Calendar types. + +If you have a struct, you can derive the implementation of the `JSON.Encoder` by specifying which fields should be encoded to JSON: ```elixir - @derive {JSON.Encoder, only: [....]} + @derive {JSON.Encoder, only: [...]} defstruct ... ``` ### Decoding -Decoding can be done via `JSON.decode/2` and `JSON.decode!/2` functions. -The default decoding rules are applied as follows: +Decoding can be done via `JSON.decode/2` and `JSON.decode!/2` functions. The default decoding rules are applied as follows: | **JSON** | **Elixir** | |----------|------------------------| @@ -199,6 +193,16 @@ The default decoding rules are applied as follows: | String | `binary()` | | Object | `%{binary() => _}` | +## Language server listeners + +4 months ago, we welcomed [the Official Language Server team](https://elixir-lang.org/blog/2024/08/15/welcome-elixir-language-server-team/), with the goal of unifying the efforts behind code intelligence, tools, and editors in Elixir. Elixir v1.18 brings new features on this front by introducing locks and listeners to its compilation. Let's understand what it means. + +At the moment, all language server implementations have their own compilation environment. This means that your project and dependencies during development are compiled once, for your own use, and then again for the language server. This duplicate effort could cause the language server experience to lag, when it could be relying on the already compiled artifacts of your project. + +This release address by introducing a compiler lock, ensuring that only a single operating system process running Elixir compiles your project at a given moment, and by providing the ability for one operating system process to listen to the compilation results of others. In other words, different Elixir instances can now communicate over the same compilation build, instead of racing each other. + +These enhancements do not only improve editor tooling, but they also directly benefit projects like IEx and Phoenix. For example, you can invoke `IEx.configure(auto_reload: true)` and IEx will automatically reload modules changed elsewhere, either by a separate terminal or your IDE. + ## Potential incompatibilities This release no longer supports WERL (a graphical user interface on Windows used by Erlang 25 and earlier). For a better user experience on Windows terminals, use Erlang/OTP 26+ (this is also the last Elixir release to support Erlang/OTP 25). @@ -219,7 +223,101 @@ You may also prefer to write using guards: def foo(x, y, z) when x == y and y == z -## v1.18.0-dev +## v1.18.3 (2025-03-06) + +### 1. Enhancements + +#### Elixir + + * [JSON] Encode any JSON key to string + * [Kernel] Allow `<<_::3*8>>` in typespecs + +#### Mix + + * [mix loadpaths] Support `--no-listeners` option + +### 2. Bug fixes + +#### Elixir + + * [CLI] Fix `--no-color` not setting `:ansi_enabled` to false + * [Protocol] Return correct implementation for an invalid struct pointing to `nil` + * [Stream] Do not raise when `Stream.cycle/1` is explicitly halted + +#### ExUnit + + * [ExUnit.Diff] Fix regression when diffing nested improper lists + +#### IEx + + * [IEx.Autocomplete] Fix autocomplete crash when expanding struct with `__MODULE__` + * [IEx.Helpers] Do not purge on `recompile` if IEx is not running + +## v1.18.2 (2025-01-22) + +### 1. Enhancements + +#### Elixir + + * [CLI] Add `--color`/`--no-color` for enabling and disabling of ANSI colors + * [Code.Fragment] Provide more AST context when invoking `container_cursor_to_quoted` with trailing fragments + * [Regex] Ensure compatibility with Erlang/OTP 28+ new Regex engine + +#### Mix + + * [mix] Print compilation lock waiting message to stderr + * [mix] Add an environment variable to optionally disable compilation locking + +### 2. Bug fixes + +#### Elixir + + * [CLI] Temporarily remove PowerShell scripts for `elixir`, `elixirc`, and `mix` on Windows, as they leave the shell broken after quitting Erlang + +#### ExUnit + + * [ExUnit] Fix crash when diffing bitstring specifiers + +#### IEx + + * [IEx.Autocomplete] Fix crashing when autocompleting structs with runtime values + +#### Mix + + * [mix] Track compilation locks per user to avoid permission errors + * [mix deps.update] Ensure Git dependencies can be upgraded by doing so against the origin + +## v1.18.1 (2024-12-24) + +### 1. Enhancements + + * [Kernel] Do not emit type violation warnings when comparing or matching against literals + * [Kernel] Do not validate clauses of private overridable functions + +### 2. Bug fixes + +#### Elixir + + * [Code.Fragment] Ensure `Code.Fragment.container_cursor_to_quoted/2` with `:trailing_fragment` parses expressions that were supported in previous versions + * [Kernel] Do not crash when typing violation is detected on dynamic dispatch + * [Kernel] Properly annotate the source for warnings emitted by the compiler with the `@file` annotation + * [Kernel] Properly annotate the source for warnings emitted by the type system with the `@file` annotation + * [Kernel] Remove `:no_parens` metadata when using capture with arity on all cases + * [Kernel] Ensure diagnostic traces are kept backwards compatible + +#### ExUnit + + * [ExUnit.Case] Ensure async groups do not run concurrenly while the test suite is still loading + * [ExUnit.Case] Ensure `--repeat-until-failure` can be combined with groups + +#### Mix + + * [mix compile.elixir] Store compilation results if compilation fails due to `--warnings-as-errors` + * [mix deps.loadpaths] Add build lock + * [mix escript.build] Ensure build succeeds when protocol consolidation is disabled + * [Mix.Shell] Ensure encoding is properly respected on Windows and Unix systems + +## v1.18.0 (2024-12-19) ### 1. Enhancements @@ -234,6 +332,8 @@ You may also prefer to write using guards: * [Config] Add `Config.read_config/1` * [Enumerable] Add `Enum.product_by/2` and `Enum.sum_by/2` * [Exception] Add `MissingApplicationsError` exception to denote missing applications + * [JSON] Add a new `JSON` module with encoding and decoding functionality + * [JSON] Implement `JSON.Encoder` for all Calendar types * [Kernel] Update source code parsing to match [UTS #55](https://www.unicode.org/reports/tr55/) latest recommendations. In particular, mixed script is allowed in identifiers as long as they are separate by underscores (`_`), such as `http_сервер`. Previously allowed highly restrictive identifiers, which mixed Latin and other scripts, such as the japanese word for t-shirt, `Tシャツ`, now require the underscore as well * [Kernel] Warn on bidirectional confusability in identifiers * [Kernel] Verify the type of the binary generators @@ -261,6 +361,7 @@ You may also prefer to write using guards: #### IEx + * [IEx] Add `IEx.configure(auto_reload: true)` to automatically pick up modules recompiled from other operating system processes * [IEx] Add `:dot_iex` support to `IEx.configure/1` * [IEx] Add report for normal/shutdown exits in IEx @@ -284,6 +385,7 @@ You may also prefer to write using guards: * [Code.Fragment] Properly handle keyword keys as their own entry * [Inspect.Algebra] Ensure `next_break_fits` respects `line_length` * [Kernel] Validate AST on `unquote` and `unquote_splicing` to provide better error reports instead of failing too late inside the compiler + * [Kernel] Avoid crashes when emitting diagnostics on code using \t for indentation * [Module] Include module attribute line and name when tracing its aliases * [Stream] Do not halt streams twice in `Stream.transform/5` * [URI] Fix a bug when a schemaless URI is given to `URI.merge/2` @@ -334,6 +436,8 @@ You may also prefer to write using guards: #### Mix * [mix cmd] Deprecate `mix cmd --app APP` in favor of `mix do --app APP` + * [mix compile] `:warnings_as_errors` configuration in `:elixirc_options` is deprecated. Instead pass the `--warnings-as-errors` flag to `mix compile`. Alternatively, you might alias the task: `aliases: [compile: "compile --warnings-as-errors"]` + * [mix test] `:warnings_as_errors` configuration in `:test_elixirc_options` is deprecated. Instead pass the `--warnings-as-errors` flag to `mix test`. Alternatively, you might alias the task: `aliases: [test: "test --warnings-as-errors"]` * [Mix.Tasks.Compile] Deprecate `compilers/0` in favor of `Mix.Task.Compiler.compilers/0` ## v1.17 diff --git a/Makefile b/Makefile index b6f51b1ffbc..8d0270a5757 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ PREFIX ?= /usr/local TEST_FILES ?= "*_test.exs" SHARE_PREFIX ?= $(PREFIX)/share MAN_PREFIX ?= $(SHARE_PREFIX)/man -CANONICAL := main/ +# CANONICAL := main/ ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS) ERLC := erlc -I lib/elixir/include ERL_MAKE := erl -make @@ -17,7 +17,7 @@ INSTALL_DIR = $(INSTALL) -m755 -d INSTALL_DATA = $(INSTALL) -m644 INSTALL_PROGRAM = $(INSTALL) -m755 GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null )) -GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | tail -1) ) +GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | grep -v latest | tail -1)) SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH diff --git a/RELEASE.md b/RELEASE.md index dc769aec9f7..79ba58241ce 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -2,7 +2,7 @@ ## Shipping a new version -1. Update version in /VERSION, bin/elixir, bin/elixir.bat, and bin/elixir.ps1 +1. Update version in /VERSION, bin/elixir, and bin/elixir.bat 2. Ensure /CHANGELOG.md is updated, versioned and add the current date @@ -30,7 +30,7 @@ ### Back in main -1. Bump /VERSION file, bin/elixir and bin/elixir.bat +1. Bump /VERSION file, bin/elixir, bin/elixir.bat, and bin/elixir.ps1 2. Start new /CHANGELOG.md diff --git a/SECURITY.md b/SECURITY.md index d035b75db8f..f436cf62934 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,12 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a Elixir version | Support :------------- | :----------------------------- -1.18 | Development -1.17 | Bug fixes and security patches +1.18 | Bug fixes and security patches +1.17 | Security patches only 1.16 | Security patches only 1.15 | Security patches only 1.14 | Security patches only -1.13 | Security patches only ## Announcements diff --git a/VERSION b/VERSION index ee017091ff3..72582753e34 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.18.0-dev +1.18.3 \ No newline at end of file diff --git a/bin/elixir b/bin/elixir index 79b74deb9e2..79cb49484fe 100755 --- a/bin/elixir +++ b/bin/elixir @@ -1,7 +1,7 @@ #!/bin/sh set -e -ELIXIR_VERSION=1.18.0-dev +ELIXIR_VERSION=1.18.3 if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then cat <&2 @@ -18,6 +18,7 @@ Usage: $(basename "$0") [options] [.exs file] [data] -pz "PATH" Appends the given path to Erlang code path (*) -v, --version Prints Erlang/OTP and Elixir versions (standalone) + --color, --no-color Enables or disables ANSI coloring --erl "SWITCHES" Switches to be passed down to Erlang (*) --eval "COMMAND" Evaluates the given command, same as -e (*) --logger-otp-reports BOOL Enables or disables OTP reporting @@ -111,7 +112,7 @@ while [ $I -le $LENGTH ]; do C=1 MODE="iex" ;; - -v|--no-halt) + -v|--no-halt|--color|--no-color) C=1 ;; -e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg) diff --git a/bin/elixir.bat b/bin/elixir.bat index 448e22f4fd3..eeec4db500a 100644 --- a/bin/elixir.bat +++ b/bin/elixir.bat @@ -1,6 +1,6 @@ @echo off -set ELIXIR_VERSION=1.18.0-dev +set ELIXIR_VERSION=1.18.3 if ""%1""=="""" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation @@ -24,6 +24,7 @@ echo -pa "PATH" Prepends the given path to Erlang code path echo -pz "PATH" Appends the given path to Erlang code path (*) echo -v, --version Prints Erlang/OTP and Elixir versions (standalone) echo. +echo --color, --no-color Enables or disables ANSI coloring echo --erl "SWITCHES" Switches to be passed down to Erlang (*) echo --eval "COMMAND" Evaluates the given command, same as -e (*) echo --logger-otp-reports BOOL Enables or disables OTP reporting @@ -107,6 +108,8 @@ if ""==!par:-pz=! (shift && goto startloop) if ""==!par:-v=! (goto startloop) if ""==!par:--version=! (goto startloop) if ""==!par:--no-halt=! (goto startloop) +if ""==!par:--color=! (goto startloop) +if ""==!par:--no-color=! (goto startloop) if ""==!par:--remsh=! (shift && goto startloop) if ""==!par:--dot-iex=! (shift && goto startloop) if ""==!par:--dbg=! (shift && goto startloop) @@ -133,9 +136,9 @@ if not defined useIEx ( set beforeExtra=-noshell -elixir_root "%SCRIPT_PATH%..\lib" -pa "%SCRIPT_PATH%..\lib\elixir\ebin" %beforeExtra% if defined ELIXIR_CLI_DRY_RUN ( - echo "%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %* + echo "%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %* ) else ( - "%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %* + "%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %* ) exit /B %ERRORLEVEL% :end diff --git a/bin/elixir.ps1 b/bin/elixir.ps1 deleted file mode 100755 index 90048705549..00000000000 --- a/bin/elixir.ps1 +++ /dev/null @@ -1,304 +0,0 @@ -#!/usr/bin/env pwsh - -$ELIXIR_VERSION = "1.18.0-dev" - -$scriptPath = Split-Path -Parent $PSCommandPath -$erlExec = "erl" - -# The iex.ps1, elixirc.ps1 and mix.ps1 scripts may populate this var. -if ($null -eq $allArgs) { - $allArgs = $args -} - -function PrintElixirHelp { - $scriptName = Split-Path -Leaf $PSCommandPath - $help = @" -Usage: $scriptName [options] [.exs file] [data] - -## General options - - -e "COMMAND" Evaluates the given command (*) - -h, --help Prints this message (standalone) - -r "FILE" Requires the given files/patterns (*) - -S SCRIPT Finds and executes the given script in `$PATH - -pr "FILE" Requires the given files/patterns in parallel (*) - -pa "PATH" Prepends the given path to Erlang code path (*) - -pz "PATH" Appends the given path to Erlang code path (*) - -v, --version Prints Erlang/OTP and Elixir versions (standalone) - - --erl "SWITCHES" Switches to be passed down to Erlang (*) - --eval "COMMAND" Evaluates the given command, same as -e (*) - --logger-otp-reports BOOL Enables or disables OTP reporting - --logger-sasl-reports BOOL Enables or disables SASL reporting - --no-halt Does not halt the Erlang VM after execution - --short-version Prints Elixir version (standalone) - -Options given after the .exs file or -- are passed down to the executed code. -Options can be passed to the Erlang runtime using `$ELIXIR_ERL_OPTIONS or --erl. - -## Distribution options - -The following options are related to node distribution. - - --cookie COOKIE Sets a cookie for this distributed node - --hidden Makes a hidden node - --name NAME Makes and assigns a name to the distributed node - --rpc-eval NODE "COMMAND" Evaluates the given command on the given remote node (*) - --sname NAME Makes and assigns a short name to the distributed node - ---name and --sname may be set to undefined so one is automatically generated. - -## Release options - -The following options are generally used under releases. - - --boot "FILE" Uses the given FILE.boot to start the system - --boot-var VAR "VALUE" Makes `$VAR available as VALUE to FILE.boot (*) - --erl-config "FILE" Loads configuration in FILE.config written in Erlang (*) - --vm-args "FILE" Passes the contents in file as arguments to the VM - ---pipe-to is not supported via PowerShell. - -** Options marked with (*) can be given more than once. -** Standalone options can't be combined with other options. -"@ - - Write-Host $help -} - -if (($allArgs.Count -eq 1) -and ($allArgs[0] -eq "--short-version")) { - Write-Host "$ELIXIR_VERSION" - exit -} - -if (($allArgs.Count -eq 0) -or (($allArgs.Count -eq 1) -and ($allArgs[0] -in @("-h", "--help")))) { - PrintElixirHelp - exit 1 -} - -function NormalizeArg { - param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [string[]] $Items - ) - $Items -join "," -} - -function QuoteString { - param( - [Parameter(ValueFromPipeline = $true)] - [string] $Item - ) - - # We surround the string with double quotes, in order to preserve its contents as - # only one command arg. - # This is needed because PowerShell consider spaces as separator of arguments. - # The double quotes around will be removed when PowerShell process the argument. - # See: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#passing-quoted-strings-to-external-commands - if ($Item.Contains(" ")) { - '"' + $Item + '"' - } - else { - $Item - } -} - -$elixirParams = @() -$erlangParams = @() -$beforeExtras = @() -$allOtherParams = @() - -$runErlPipe = $null -$runErlLog = $null - -for ($i = 0; $i -lt $allArgs.Count; $i++) { - $private:arg = $allArgs[$i] - - switch -exact ($arg) { - { $_ -in @("-e", "-r", "-pr", "-pa", "-pz", "--eval", "--remsh", "--dot-iex", "--dbg") } { - $private:nextArg = NormalizeArg($allArgs[++$i]) - - $elixirParams += $arg - $elixirParams += $nextArg - - break - } - - { $_ -in @("-v", "--version") } { - # Standalone options goes only once in the Elixir params, when they are empty. - if (($elixirParams.Count -eq 0) -and ($allOtherParams.Count -eq 0)) { - $elixirParams += $arg - } - else { - $allOtherParams += $arg - } - break - } - - "--no-halt" { - $elixirParams += $arg - break - } - - "--cookie" { - $erlangParams += "-setcookie" - $erlangParams += $allArgs[++$i] - break - } - - "--hidden" { - $erlangParams += "-hidden" - break - } - - "--name" { - $erlangParams += "-name" - $erlangParams += $allArgs[++$i] - break - } - - "--sname" { - $erlangParams += "-sname" - $erlangParams += $allArgs[++$i] - break - } - - "--boot" { - $erlangParams += "-boot" - $erlangParams += $allArgs[++$i] - break - } - - "--erl-config" { - $erlangParams += "-config" - $erlangParams += $allArgs[++$i] - break - } - - "--vm-args" { - $erlangParams += "-args_file" - $erlangParams += $allArgs[++$i] - break - } - - "--logger-otp-reports" { - $private:tempVal = $allArgs[$i + 1] - if ($tempVal -in @("true", "false")) { - $erlangParams += @("-logger", "handle_otp_reports", $allArgs[++$i]) - } - break - } - - "--logger-sasl-reports" { - $private:tempVal = $allArgs[$i + 1] - if ($tempVal -in @("true", "false")) { - $erlangParams += @("-logger", "handle_sasl_reports", $allArgs[++$i]) - } - break - } - - "--erl" { - $private:erlFlags = $allArgs[++$i] -split " " - $beforeExtras += $erlFlags - break - } - - "+iex" { - $elixirParams += "+iex" - $useIex = $true - - break - } - - "+elixirc" { - $elixirParams += "+elixirc" - break - } - - "--rpc-eval" { - $private:key = $allArgs[++$i] - $private:value = $allArgs[++$i] - - if ($null -eq $key) { - Write-Error "--rpc-eval: NODE must be present" - exit 1 - } - - if ($null -eq $value) { - Write-Error "--rpc-eval: COMMAND for the '$key' node must be present" - exit 1 - } - - $elixirParams += "--rpc-eval" - $elixirParams += $key - $elixirParams += $value - break - } - - "--boot-var" { - $private:key = $allArgs[++$i] - $private:value = $allArgs[++$i] - - if ($null -eq $key) { - Write-Error "--boot-var: VAR must be present" - exit 1 - } - - if ($null -eq $value) { - Write-Error "--boot-var: Value for the '$key' var must be present" - exit 1 - } - - $elixirParams += "-boot_var" - $elixirParams += $key - $elixirParams += $value - break - } - - Default { - $private:normalized = NormalizeArg $arg - $allOtherParams += $normalized - break - } - } -} - -if ($null -eq $useIEx) { - $beforeExtras = @("-s", "elixir", "start_cli") + $beforeExtras -} - -$beforeExtras = @("-pa", "$(Join-Path $scriptPath -ChildPath "../lib/elixir/ebin")") + $beforeExtras -$beforeExtras = @("-noshell", "-elixir_root", "$(Join-Path $scriptPath -ChildPath "../lib")") + $beforeExtras - -$allParams = @() - -if ($null -ne $env:ELIXIR_ERL_OPTIONS) { - $private:erlFlags = $env:ELIXIR_ERL_OPTIONS -split " " - $allParams += $erlFlags -} - -$allParams += $erlangParams -$allParams += $beforeExtras -$allParams += "-extra" -$allParams += $elixirParams -$allParams += $allOtherParams - -$binSuffix = "" - -# The variable is available after PowerShell 7.2. Previous to that, PS only worked on Windows. -if ($isWindows -or ($null -eq $isWindows)) { - $binSuffix = ".exe" -} - -$binPath = "$erlExec$binSuffix" - -# We double the double-quotes because they are going to be escaped by arguments parsing. -$paramsPart = $allParams | ForEach-Object -Process { QuoteString($_ -replace "`"", "`"`"") } - -if ($env:ELIXIR_CLI_DRY_RUN) { - Write-Host "$binPath $paramsPart" -} -else { - $output = Start-Process -FilePath $binPath -ArgumentList $paramsPart -NoNewWindow -Wait -PassThru - exit $output.ExitCode -} diff --git a/bin/elixirc.ps1 b/bin/elixirc.ps1 deleted file mode 100755 index 29742f010d6..00000000000 --- a/bin/elixirc.ps1 +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env pwsh - -$scriptName = Split-Path -Leaf $PSCommandPath - -if (($args.Count -eq 0) -or ($args[0] -in @("-h", "--help"))) { - Write-Host @" -Usage: $scriptName [elixir switches] [compiler switches] [.ex files] - - -h, --help Prints this message and exits - -o The directory to output compiled files - -v, --version Prints Elixir version and exits (standalone) - - --ignore-module-conflict Does not emit warnings if a module was previously defined - --no-debug-info Does not attach debug info to compiled modules - --no-docs Does not attach documentation to compiled modules - --profile time Profile the time to compile modules - --verbose Prints compilation status - --warnings-as-errors Treats warnings as errors and returns non-zero exit status - -** Options given after -- are passed down to the executed code -** Options can be passed to the Erlang runtime using ELIXIR_ERL_OPTIONS -** Options can be passed to the Erlang compiler using ERL_COMPILER_OPTIONS -"@ - exit -} - -$scriptPath = Split-Path -Parent $PSCommandPath -$elixirMainScript = Join-Path -Path $scriptPath -ChildPath "elixir.ps1" - -$prependedArgs = @("+elixirc") - -$allArgs = $prependedArgs + $args - -# The dot is going to evaluate the script with the vars defined here. -. $elixirMainScript diff --git a/bin/iex.ps1 b/bin/iex.ps1 deleted file mode 100755 index 870909377fb..00000000000 --- a/bin/iex.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env pwsh - -$scriptName = Split-Path -Leaf $PSCommandPath - -if ($args[0] -in @("-h", "--help")) { - Write-Host @" -Usage: $scriptName [options] [.exs file] [data] - -The following options are exclusive to IEx: - - --dbg pry Sets the backend for Kernel.dbg/2 to IEx.pry/0 - --dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environment. - Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists. - If FILE is empty, then no file will be loaded. - --remsh NAME Connects to a node using a remote shell. - -It accepts all other options listed by "elixir --help". -"@ - exit -} - -$scriptPath = Split-Path -Parent $PSCommandPath -$elixirMainScript = Join-Path -Path $scriptPath -ChildPath "elixir.ps1" - -$prependedArgs = @("--no-halt", "--erl", "-user elixir", "+iex") - -$allArgs = $prependedArgs + $args - -# The dot is going to evaluate the script with the vars defined here. -. $elixirMainScript diff --git a/bin/mix.ps1 b/bin/mix.ps1 index a1d3bd29458..05b19a04746 100755 --- a/bin/mix.ps1 +++ b/bin/mix.ps1 @@ -1,13 +1,23 @@ -#!/usr/bin/env pwsh +# Store path to mix.bat as a FileInfo object +$mixBatPath = (Get-ChildItem (((Get-ChildItem $MyInvocation.MyCommand.Path).Directory.FullName) + '\mix.bat')) +$newArgs = @() -$scriptPath = Split-Path -Parent $PSCommandPath -$elixirMainScript = Join-Path -Path $scriptPath -ChildPath "elixir.ps1" +for ($i = 0; $i -lt $args.length; $i++) +{ + if ($args[$i] -is [array]) + { + # Commas created the array so we need to reintroduce those commas + for ($j = 0; $j -lt $args[$i].length - 1; $j++) + { + $newArgs += ($args[$i][$j] + ',') + } + $newArgs += $args[$i][-1] + } + else + { + $newArgs += $args[$i] + } +} -$mixFile = Join-Path -Path $scriptPath -ChildPath "mix" - -$prependedArgs = @($mixFile) - -$allArgs = $prependedArgs + $args - -# The dot is going to evaluate the script with the vars defined here. -. $elixirMainScript +# Corrected arguments are ready to pass to batch file +& $mixBatPath $newArgs \ No newline at end of file diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 4fb676c107d..43e71c9eaa3 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -110,30 +110,12 @@ defmodule NaiveDateTime do @spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t def utc_now(calendar_or_time_unit \\ Calendar.ISO) - def utc_now(Calendar.ISO) do - {:ok, {year, month, day}, {hour, minute, second}, microsecond} = - Calendar.ISO.from_unix(:os.system_time(), :native) - - %NaiveDateTime{ - year: year, - month: month, - day: day, - hour: hour, - minute: minute, - second: second, - microsecond: microsecond, - calendar: Calendar.ISO - } - end - def utc_now(time_unit) when time_unit in [:microsecond, :millisecond, :second, :native] do utc_now(time_unit, Calendar.ISO) end def utc_now(calendar) do - calendar - |> DateTime.utc_now() - |> DateTime.to_naive() + utc_now(:native, calendar) end @doc """ @@ -158,7 +140,20 @@ defmodule NaiveDateTime do @spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t def utc_now(time_unit, calendar) when time_unit in [:native, :microsecond, :millisecond, :second] do - DateTime.utc_now(time_unit, calendar) |> DateTime.to_naive() + {:ok, {year, month, day}, {hour, minute, second}, microsecond} = + Calendar.ISO.from_unix(System.os_time(time_unit), time_unit) + + %NaiveDateTime{ + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond, + calendar: Calendar.ISO + } + |> convert!(calendar) end @doc """ @@ -1265,6 +1260,10 @@ defmodule NaiveDateTime do {:ok, t} | {:error, :incompatible_calendars} # Keep it multiline for proper function clause errors. + def convert(%NaiveDateTime{calendar: calendar} = ndt, calendar) do + {:ok, ndt} + end + def convert( %{ calendar: calendar, diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 1a4a400f06b..be6e4beaaf2 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -1709,7 +1709,9 @@ defmodule Code do def put_compiler_option(:warnings_as_errors, _value) do IO.warn( ":warnings_as_errors is deprecated as part of Code.put_compiler_option/2, " <> - "pass it as option to Kernel.ParallelCompiler instead" + "instead you must pass it as a --warnings-as-errors flag. " <> + "If you need to set it as a default in a mix task, you can also set it under aliases: " <> + "[compile: \"compile --warnings-as-errors\"]" ) :ok diff --git a/lib/elixir/lib/code/fragment.ex b/lib/elixir/lib/code/fragment.ex index 75a9cf325a3..567f31348cc 100644 --- a/lib/elixir/lib/code/fragment.ex +++ b/lib/elixir/lib/code/fragment.ex @@ -1151,7 +1151,7 @@ defmodule Code.Fragment do {rev_tokens, rev_terminators} = with [close, open, {_, _, :__cursor__} = cursor | rev_tokens] <- rev_tokens, {_, [_ | after_fn]} <- Enum.split_while(rev_terminators, &(elem(&1, 0) != :fn)), - true <- maybe_missing_stab?(rev_tokens), + true <- maybe_missing_stab?(rev_tokens, false), [_ | rev_tokens] <- Enum.drop_while(rev_tokens, &(elem(&1, 0) != :fn)) do {[close, open, cursor | rev_tokens], after_fn} else @@ -1165,7 +1165,7 @@ defmodule Code.Fragment do tokens = with {before_start, [_ | _] = after_start} <- Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])), - true <- maybe_missing_stab?(rev_tokens), + true <- maybe_missing_stab?(rev_tokens, true), opts = Keyword.put(opts, :check_terminators, {:cursor, before_start}), {:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <- @@ -1173,6 +1173,14 @@ defmodule Code.Fragment do trailing_tokens = reverse_tokens(meta[:line], meta[:column], trailing_rev_tokens, after_start) + # If the cursor has its own line, then we do not trim new lines trailing tokens. + # Otherwise we want to drop any newline so we drop the next tokens after eol. + trailing_tokens = + case rev_tokens do + [_close, _open, {_, _, :__cursor__}, {:eol, _} | _] -> trailing_tokens + _ -> Enum.drop_while(trailing_tokens, &match?({:eol, _}, &1)) + end + Enum.reverse(rev_tokens, drop_tokens(trailing_tokens, 0)) else _ -> reverse_tokens(line, column, rev_tokens, rev_terminators) @@ -1196,12 +1204,16 @@ defmodule Code.Fragment do Enum.reverse(tokens, terminators) end + # Otherwise we drop all tokens, trying to build a minimal AST + # for cursor completion. defp drop_tokens([{:"}", _} | _] = tokens, 0), do: tokens defp drop_tokens([{:"]", _} | _] = tokens, 0), do: tokens defp drop_tokens([{:")", _} | _] = tokens, 0), do: tokens defp drop_tokens([{:">>", _} | _] = tokens, 0), do: tokens defp drop_tokens([{:end, _} | _] = tokens, 0), do: tokens defp drop_tokens([{:",", _} | _] = tokens, 0), do: tokens + defp drop_tokens([{:";", _} | _] = tokens, 0), do: tokens + defp drop_tokens([{:eol, _} | _] = tokens, 0), do: tokens defp drop_tokens([{:stab_op, _, :->} | _] = tokens, 0), do: tokens defp drop_tokens([{:"}", _} | tokens], counter), do: drop_tokens(tokens, counter - 1) @@ -1220,16 +1232,13 @@ defmodule Code.Fragment do defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter) defp drop_tokens([], 0), do: [] - defp maybe_missing_stab?([{:after, _} | _]), do: true - defp maybe_missing_stab?([{:do, _} | _]), do: true - defp maybe_missing_stab?([{:fn, _} | _]), do: true - defp maybe_missing_stab?([{:else, _} | _]), do: true - defp maybe_missing_stab?([{:catch, _} | _]), do: true - defp maybe_missing_stab?([{:rescue, _} | _]), do: true - - defp maybe_missing_stab?([{:stab_op, _, :->} | _]), do: false - defp maybe_missing_stab?([{:eol, _}, next | _]) when elem(next, 0) != :",", do: false - - defp maybe_missing_stab?([_ | tail]), do: maybe_missing_stab?(tail) - defp maybe_missing_stab?([]), do: false + defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true + defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true + defp maybe_missing_stab?([{:fn, _} | _], _stab_choice?), do: true + defp maybe_missing_stab?([{:else, _} | _], _stab_choice?), do: true + defp maybe_missing_stab?([{:catch, _} | _], _stab_choice?), do: true + defp maybe_missing_stab?([{:rescue, _} | _], _stab_choice?), do: true + defp maybe_missing_stab?([{:stab_op, _, :->} | _], stab_choice?), do: stab_choice? + defp maybe_missing_stab?([_ | tail], stab_choice?), do: maybe_missing_stab?(tail, stab_choice?) + defp maybe_missing_stab?([], _stab_choice?), do: false end diff --git a/lib/elixir/lib/code/typespec.ex b/lib/elixir/lib/code/typespec.ex index 017b2f05dcc..3efa7be86e0 100644 --- a/lib/elixir/lib/code/typespec.ex +++ b/lib/elixir/lib/code/typespec.ex @@ -27,11 +27,6 @@ defmodule Code.Typespec do end end - def spec_to_quoted(name, {:type, anno, :fun, []}) when is_atom(name) do - meta = meta(anno) - {:"::", meta, [{name, meta, []}, quote(do: term)]} - end - def spec_to_quoted(name, {:type, anno, :bounded_fun, [type, constrs]}) when is_atom(name) do meta = meta(anno) {:type, _, :fun, [{:type, _, :product, args}, result]} = type @@ -288,7 +283,6 @@ defmodule Code.Typespec do end defp typespec_to_quoted({:type, anno, :binary, [arg1, arg2]}) do - [arg1, arg2] = for arg <- [arg1, arg2], do: typespec_to_quoted(arg) line = meta(anno)[:line] case {typespec_to_quoted(arg1), typespec_to_quoted(arg2)} do @@ -317,10 +311,6 @@ defmodule Code.Typespec do [{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}] end - defp typespec_to_quoted({:type, anno, :fun, []}) do - typespec_to_quoted({:type, anno, :fun, [{:type, anno, :any}, {:type, anno, :any, []}]}) - end - defp typespec_to_quoted({:type, anno, :range, [left, right]}) do {:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]} end @@ -338,10 +328,14 @@ defmodule Code.Typespec do {erl_to_ex_var(var), meta(anno), nil} end - defp typespec_to_quoted({:op, anno, op, arg}) do + defp typespec_to_quoted({:op, anno, op, arg}) when op in [:+, :-] do {op, meta(anno), [typespec_to_quoted(arg)]} end + defp typespec_to_quoted({:op, anno, :*, arg1, arg2}) do + {:*, meta(anno), [typespec_to_quoted(arg1), typespec_to_quoted(arg2)]} + end + defp typespec_to_quoted({:remote_type, anno, [mod, name, args]}) do remote_type(anno, mod, name, args) end diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index d2181d42e7f..575c1b16c6c 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -3837,6 +3837,9 @@ defmodule Enum do @doc """ Enumerates the `enumerable`, removing all duplicate elements. + The first occurrence of each element is kept and all following + duplicates are removed. The overall order is preserved. + ## Examples iex> Enum.uniq([1, 2, 3, 3, 2, 1]) @@ -3862,7 +3865,8 @@ defmodule Enum do considered duplicates if the return value of `fun` is equal for both of them. - The first occurrence of each element is kept. + The first occurrence of each element is kept and all following + duplicates are removed. The overall order is preserved. ## Example diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index 63ec7a2407d..094e5f6a735 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -2530,7 +2530,7 @@ defmodule ErlangError do {:ok, reason, IO.iodata_to_binary([":\n\n" | Enum.map(args_errors, &arg_error/1)])} general = extra[:general] -> - {:ok, reason, ": " <> general} + {:ok, reason, ": " <> IO.chardata_to_string(general)} true -> :error diff --git a/lib/elixir/lib/float.ex b/lib/elixir/lib/float.ex index 5855a975aff..07313e45d31 100644 --- a/lib/elixir/lib/float.ex +++ b/lib/elixir/lib/float.ex @@ -577,15 +577,9 @@ defmodule Float do Returns a charlist which corresponds to the shortest text representation of the given float. - The underlying algorithm changes depending on the Erlang/OTP version: - - * For OTP >= 24, it uses the algorithm presented in "Ryū: fast - float-to-string conversion" in Proceedings of the SIGPLAN '2018 - Conference on Programming Language Design and Implementation. - - * For OTP < 24, it uses the algorithm presented in "Printing Floating-Point - Numbers Quickly and Accurately" in Proceedings of the SIGPLAN '1996 - Conference on Programming Language Design and Implementation. + It uses the algorithm presented in "Ryū: fast float-to-string conversion" + in Proceedings of the SIGPLAN '2018 Conference on Programming Language + Design and Implementation. For a configurable representation, use `:erlang.float_to_list/2`. diff --git a/lib/elixir/lib/json.ex b/lib/elixir/lib/json.ex index d81b4ddcc1c..0cd9285c44f 100644 --- a/lib/elixir/lib/json.ex +++ b/lib/elixir/lib/json.ex @@ -146,7 +146,35 @@ end defimpl JSON.Encoder, for: Map do def encode(value, encoder) do - :elixir_json.encode_map(value, encoder) + case :maps.next(:maps.iterator(value)) do + :none -> + "{}" + + {key, value, iterator} -> + [?{, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)] + end + end + + defp next(iterator, encoder) do + case :maps.next(iterator) do + :none -> + "}" + + {key, value, iterator} -> + [?,, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)] + end + end + + # Erlang supports only numbers, binaries, and atoms as keys, + # we support anything that implements the String.Chars protocol. + defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder) + defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder) + defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder) +end + +defimpl JSON.Encoder, for: [Date, Time, NaiveDateTime, DateTime, Duration] do + def encode(value, _encoder) do + [?", @for.to_iso8601(value), ?"] end end @@ -169,17 +197,15 @@ defmodule JSON do Elixir built-in data structures are encoded to JSON as follows: - | **Elixir** | **JSON** | - |------------------------|----------| - | `integer() \| float()` | Number | - | `true \| false ` | Boolean | - | `nil` | Null | - | `binary()` | String | - | `atom()` | String | - | `list()` | Array | - | `%{binary() => _}` | Object | - | `%{atom() => _}` | Object | - | `%{integer() => _}` | Object | + | **Elixir** | **JSON** | + |----------------------------|----------| + | `integer() \| float()` | Number | + | `true \| false ` | Boolean | + | `nil` | Null | + | `binary()` | String | + | `atom()` | String | + | `list()` | Array | + | `%{String.Chars.t() => _}` | Object | You may also implement the `JSON.Encoder` protocol for custom data structures. @@ -199,6 +225,8 @@ defmodule JSON do @moduledoc since: "1.18.0" + @type encoder :: (term(), encoder() -> iodata()) + @type decode_error_reason :: {:unexpected_end, non_neg_integer()} | {:invalid_byte, non_neg_integer(), byte()} @@ -257,7 +285,8 @@ defmodule JSON do For streaming decoding, see Erlang's `:json` module. """ - @spec decode(binary(), term(), keyword()) :: {term(), term(), binary()} | decode_error_reason() + @spec decode(binary(), term(), keyword()) :: + {term(), term(), binary()} | {:error, decode_error_reason()} def decode(binary, acc, decoders) when is_binary(binary) and is_list(decoders) do decoders = Keyword.put_new(decoders, :null, nil) @@ -326,13 +355,19 @@ defmodule JSON do The second argument is a function that is recursively invoked to encode a term. + > #### IO and performance {: .tip} + > + > If you need to encode data to be sent over the network + > or written to the filesystem, consider using the more + > efficient `encode_to_iodata!/2`. + ## Examples iex> JSON.encode!([123, "string", %{key: "value"}]) "[123,\"string\",{\"key\":\"value\"}]" """ - @spec encode!(a, (a -> iodata())) :: binary() when a: var + @spec encode!(term(), encoder()) :: binary() def encode!(term, encoder \\ &protocol_encode/2) do IO.iodata_to_binary(encoder.(term, encoder)) end @@ -353,7 +388,7 @@ defmodule JSON do "[123,\"string\",{\"key\":\"value\"}]" """ - @spec encode_to_iodata!(a, (a -> iodata())) :: iodata() when a: var + @spec encode_to_iodata!(term(), encoder()) :: iodata() def encode_to_iodata!(term, encoder \\ &protocol_encode/2) do encoder.(term, encoder) end @@ -365,7 +400,7 @@ defmodule JSON do `encode!/2` and `encode_to_iodata!/2`. The default implementation is an optimized dispatch to the `JSON.Encoder` protocol. """ - @spec protocol_encode(a, (a -> iodata())) :: iodata() when a: var + @spec protocol_encode(term(), encoder()) :: iodata() def protocol_encode(value, encoder) when is_atom(value) do case value do nil -> "null" @@ -388,7 +423,7 @@ defmodule JSON do do: :elixir_json.encode_list(value, encoder) def protocol_encode(%{} = value, encoder) when not is_map_key(value, :__struct__), - do: :elixir_json.encode_map(value, encoder) + do: JSON.Encoder.Map.encode(value, encoder) def protocol_encode(value, encoder), do: JSON.Encoder.encode(value, encoder) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 31a9a4c8c65..f8f6c9e6fac 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -3306,7 +3306,7 @@ defmodule Kernel do end defp nest_pop_in(:map, h, [{:access, key}]) do - quote do + quote generated: true do case unquote(h) do nil -> {nil, nil} h -> Access.pop(h, unquote(key)) @@ -3327,7 +3327,7 @@ defmodule Kernel do end defp nest_pop_in(_, h, [{:access, key}]) do - quote do + quote generated: true do case unquote(h) do nil -> :pop h -> Access.pop(h, unquote(key)) @@ -3716,26 +3716,25 @@ defmodule Kernel do case function? do true -> - value = - case Module.__get_attribute__(env.module, name, line, false) do - {_, doc} when doc_attr? -> doc - other -> other - end + case Module.__get_attribute__(env.module, name, line, false) do + {_, doc} when doc_attr? -> + do_at_escape(name, doc) + + %{__struct__: Regex, source: source, opts: opts} = regex -> + case :erlang.system_info(:otp_release) < [?2, ?8] do + true -> do_at_escape(name, regex) + false -> quote(do: Regex.compile!(unquote(source), unquote(opts))) + end - try do - :elixir_quote.escape(value, :none, false) - rescue - ex in [ArgumentError] -> - raise ArgumentError, - "cannot inject attribute @#{name} into function/macro because " <> - Exception.message(ex) + value -> + do_at_escape(name, value) end false when doc_attr? -> quote do case Module.__get_attribute__(__MODULE__, unquote(name), unquote(line), false) do {_, doc} -> doc - other -> other + value -> value end end @@ -3770,6 +3769,17 @@ defmodule Kernel do raise ArgumentError, "expected 0 or 1 argument for @#{name}, got: #{length(args)}" end + defp do_at_escape(name, value) do + try do + :elixir_quote.escape(value, :none, false) + rescue + ex in [ArgumentError] -> + raise ArgumentError, + "cannot inject attribute @#{name} into function/macro because " <> + Exception.message(ex) + end + end + # Those are always compile-time dependencies, so we can skip the trace. defp collect_traces(:before_compile, arg, _env), do: {arg, []} defp collect_traces(:after_compile, arg, _env), do: {arg, []} @@ -4066,7 +4076,7 @@ defmodule Kernel do end defp stepless_range(nil, first, last, _caller) do - quote(do: Elixir.Range.new(unquote(first), unquote(last))) + quote(do: Function.identity(Elixir.Range.new(unquote(first), unquote(last)))) end defp stepless_range(:guard, first, last, caller) do @@ -4584,6 +4594,9 @@ defmodule Kernel do defp in_var(true, {atom, _, context} = var, fun) when is_atom(atom) and is_atom(context), do: fun.(var) + defp in_var(true, var, fun) when is_atom(var) or is_binary(var) or is_number(var), + do: fun.(var) + defp in_var(true, ast, fun) do quote do var = unquote(ast) @@ -5460,33 +5473,21 @@ defmodule Kernel do use `@type`. """ defmacro defstruct(fields) do - header = - quote bind_quoted: [fields: fields, bootstrapped?: bootstrapped?(Enum)] do - {struct, derive, escaped_struct, kv, body} = - Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?, __ENV__) - - case derive do - [] -> :ok - _ -> Protocol.__derive__(derive, __MODULE__, __ENV__) - end - end + quote bind_quoted: [fields: fields, bootstrapped?: bootstrapped?(Enum)] do + {struct, derive, escaped_struct, kv, body} = + Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?, __ENV__) - # We attach the line: 0 to struct functions because we don't want - # the generated callbacks to count towards code coverage and metrics, - # especially since they are often expanded at compile-time. - functions = - quote line: 0, unquote: false do - def __struct__(), do: unquote(escaped_struct) - def __struct__(unquote(kv)), do: unquote(body) + case derive do + [] -> :ok + _ -> Protocol.__derive__(derive, __MODULE__, __ENV__) end - footer = - quote do - Kernel.Utils.announce_struct(__MODULE__) - struct - end + def __struct__(), do: unquote(escaped_struct) + def __struct__(unquote(kv)), do: unquote(body) - {:__block__, [], [header, functions, footer]} + Kernel.Utils.announce_struct(__MODULE__) + struct + end end @doc ~S""" @@ -6446,29 +6447,38 @@ defmodule Kernel do """ defmacro sigil_r(term, modifiers) - defmacro sigil_r({:<<>>, _meta, [string]}, options) when is_binary(string) do - binary = :elixir_interpolation.unescape_string(string, ®ex_unescape_map/1) - regex = Regex.compile!(binary, :binary.list_to_bin(options)) - Macro.escape(regex) + defmacro sigil_r({:<<>>, _meta, [binary]}, options) when is_binary(binary) do + binary = :elixir_interpolation.unescape_string(binary, ®ex_unescape_map/1) + compile_regex(binary, options) end defmacro sigil_r({:<<>>, meta, pieces}, options) do - binary = {:<<>>, meta, unescape_tokens(pieces, ®ex_unescape_map/1)} - quote(do: Regex.compile!(unquote(binary), unquote(:binary.list_to_bin(options)))) + tuple = {:<<>>, meta, unescape_tokens(pieces, ®ex_unescape_map/1)} + compile_regex(tuple, options) end defp regex_unescape_map(:newline), do: true defp regex_unescape_map(_), do: false @doc false - defmacro sigil_R({:<<>>, _meta, [string]}, options) when is_binary(string) do + defmacro sigil_R({:<<>>, _meta, [binary]}, options) when is_binary(binary) do IO.warn( "~R/.../ is deprecated, use ~r/.../ instead", Macro.Env.stacktrace(__CALLER__) ) - regex = Regex.compile!(string, :binary.list_to_bin(options)) - Macro.escape(regex) + compile_regex(binary, options) + end + + defp compile_regex(binary_or_tuple, options) do + # TODO: Remove this when we require Erlang/OTP 28+ + case is_binary(binary_or_tuple) and :erlang.system_info(:otp_release) < [?2, ?8] do + true -> + Macro.escape(Regex.compile!(binary_or_tuple, :binary.list_to_bin(options))) + + false -> + quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(:binary.list_to_bin(options)))) + end end @doc ~S""" diff --git a/lib/elixir/lib/kernel/cli.ex b/lib/elixir/lib/kernel/cli.ex index c4b302b236b..59d91e1d0bf 100644 --- a/lib/elixir/lib/kernel/cli.ex +++ b/lib/elixir/lib/kernel/cli.ex @@ -295,6 +295,16 @@ defmodule Kernel.CLI do parse_argv(t, %{config | commands: [{:parallel_require, h} | config.commands]}) end + defp parse_argv([~c"--color" | t], config) do + Application.put_env(:elixir, :ansi_enabled, true) + parse_argv(t, config) + end + + defp parse_argv([~c"--no-color" | t], config) do + Application.put_env(:elixir, :ansi_enabled, false) + parse_argv(t, config) + end + ## Compiler defp parse_argv([~c"-o", h | t], %{mode: :elixirc} = config) do diff --git a/lib/elixir/lib/kernel/error_handler.ex b/lib/elixir/lib/kernel/error_handler.ex index 8fe1a2058a9..8152a2f009e 100644 --- a/lib/elixir/lib/kernel/error_handler.ex +++ b/lib/elixir/lib/kernel/error_handler.ex @@ -37,7 +37,15 @@ defmodule Kernel.ErrorHandler do :erlang.garbage_collect(self()) receive do - {^ref, value} -> value + {^ref, {:loading, pid}} -> + ref = :erlang.monitor(:process, pid) + + receive do + {:DOWN, ^ref, _, _, _} -> :found + end + + {^ref, value} -> + value end end end diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index db5a969a7cb..98f9088f0dc 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -181,7 +181,7 @@ defmodule Kernel.ParallelCompiler do {:ok, [atom], [warning] | info()} | {:error, [error] | [Code.diagnostic(:error)], [warning] | info()} def compile_to_path(files, path, options \\ []) when is_binary(path) and is_list(options) do - spawn_workers(files, {:compile, path}, options) + spawn_workers(files, {:compile, path}, Keyword.put(options, :dest, path)) end @doc """ @@ -257,29 +257,7 @@ defmodule Kernel.ParallelCompiler do {status, modules_or_errors, info} = try do - outcome = spawn_workers(schedulers, cache, files, output, options) - {outcome, Keyword.get(options, :warnings_as_errors, false)} - else - {{:ok, _, %{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info}, true} - when r_warnings != [] or c_warnings != [] -> - message = - "Compilation failed due to warnings while using the --warnings-as-errors option" - - IO.puts(:stderr, message) - errors = Enum.map(r_warnings ++ c_warnings, &Map.replace!(&1, :severity, :error)) - {:error, errors, %{info | runtime_warnings: [], compile_warnings: []}} - - {{:ok, outcome, info}, _} -> - beam_timestamp = Keyword.get(options, :beam_timestamp) - {:ok, write_module_binaries(outcome, output, beam_timestamp), info} - - {{:error, errors, info}, true} -> - %{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info - info = %{info | runtime_warnings: [], compile_warnings: []} - {:error, c_warnings ++ r_warnings ++ errors, info} - - {{:error, errors, info}, _} -> - {:error, errors, info} + spawn_workers(schedulers, cache, files, output, options) after Module.ParallelChecker.stop(cache) end @@ -303,6 +281,7 @@ defmodule Kernel.ParallelCompiler do {outcome, state} = spawn_workers(files, %{}, %{}, [], %{}, [], [], %{ + beam_timestamp: Keyword.get(options, :beam_timestamp), dest: Keyword.get(options, :dest), each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, [], []} end), each_file: Keyword.get(options, :each_file, fn _, _ -> :ok end) |> each_file(), @@ -341,8 +320,11 @@ defmodule Kernel.ParallelCompiler do end defp write_module_binaries(result, {:compile, path}, timestamp) do + File.mkdir_p!(path) + Code.prepend_path(path) + Enum.flat_map(result, fn - {{:module, module}, binary} when is_binary(binary) -> + {{:module, module}, {binary, _}} when is_binary(binary) -> full_path = Path.join(path, Atom.to_string(module) <> ".beam") File.write!(full_path, binary) if timestamp, do: File.touch!(full_path, timestamp) @@ -354,20 +336,21 @@ defmodule Kernel.ParallelCompiler do end defp write_module_binaries(result, _output, _timestamp) do - for {{:module, module}, binary} when is_binary(binary) <- result, do: module + for {{:module, module}, {binary, _}} when is_binary(binary) <- result, do: module end ## Verification defp verify_modules(result, compile_warnings, dependent_modules, state) do + modules = write_module_binaries(result, state.output, state.beam_timestamp) runtime_warnings = maybe_check_modules(result, dependent_modules, state) info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings} - {{:ok, result, info}, state} + {{:ok, modules, info}, state} end defp maybe_check_modules(result, runtime_modules, state) do compiled_modules = - for {{:module, module}, binary} when is_binary(binary) <- result, + for {{:module, module}, {binary, _}} when is_binary(binary) <- result, do: module profile( @@ -440,8 +423,8 @@ defmodule Kernel.ParallelCompiler do try do case output do - {:compile, path} -> compile_file(file, path, parent) - :compile -> compile_file(file, dest, parent) + {:compile, _} -> compile_file(file, dest, false, parent) + :compile -> compile_file(file, dest, true, parent) :require -> require_file(file, parent) end catch @@ -547,9 +530,9 @@ defmodule Kernel.ParallelCompiler do wait_for_messages([], spawned, waiting, files, result, warnings, errors, state) end - defp compile_file(file, path, parent) do + defp compile_file(file, path, force_load?, parent) do :erlang.process_flag(:error_handler, Kernel.ErrorHandler) - :erlang.put(:elixir_compiler_dest, path) + :erlang.put(:elixir_compiler_dest, {path, force_load?}) :elixir_compiler.file(file, &each_file(&1, &2, parent)) end @@ -583,7 +566,7 @@ defmodule Kernel.ParallelCompiler do end defp count_modules(result) do - Enum.count(result, &match?({{:module, _}, binary} when is_binary(binary), &1)) + Enum.count(result, &match?({{:module, _}, {binary, _}} when is_binary(binary), &1)) end defp each_cycle_return({kind, modules, warnings}), do: {kind, modules, warnings} @@ -650,19 +633,39 @@ defmodule Kernel.ParallelCompiler do state ) - {:module_available, child, ref, file, module, binary} -> + {{:module_loaded, module}, _ref, _type, _pid, _reason} -> + result = + Map.update!(result, {:module, module}, fn {binary, _loader} -> {binary, true} end) + + spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state) + + {:module_available, child, ref, file, module, binary, loaded?} -> state.each_module.(file, module, binary) + {available, load_status} = + case Map.get(result, {:module, module}) do + # We prefer to load in the client, if possible, + # to avoid locking the compilation server. + [_ | _] = pids when loaded? -> + {Enum.map(pids, &{&1, :found}), loaded?} + + [_ | _] = pids -> + pid = load_module(module, binary, state.dest) + {Enum.map(pids, &{&1, {:loading, pid}}), pid} + + _ -> + {[], loaded?} + end + # Release the module loader which is waiting for an ack send(child, {ref, :ack}) - {available, result} = update_result(result, :module, module, binary) spawn_workers( available ++ queue, spawned, waiting, files, - result, + Map.put(result, {:module, module}, {binary, load_status}), warnings, errors, state @@ -681,7 +684,9 @@ defmodule Kernel.ParallelCompiler do {waiting, files, result} = if not is_list(available_or_pending) or on in defining do - send(child_pid, {ref, :found}) + # If what we are waiting on was defined but not loaded, we do it now. + {reply, result} = load_pending(kind, on, result, state) + send(child_pid, {ref, reply}) {waiting, files, result} else waiting = Map.put(waiting, child_pid, {kind, ref, file_pid, on, defining, deadlock}) @@ -775,6 +780,52 @@ defmodule Kernel.ParallelCompiler do {{:error, Enum.reverse(errors, fun.()), info}, state} end + defp load_pending(kind, module, result, state) do + case result do + %{{:module, ^module} => {binary, load_status}} + when kind in [:module, :struct] and is_binary(binary) -> + case load_status do + true -> + {:found, result} + + false -> + pid = load_module(module, binary, state.dest) + result = Map.put(result, {:module, module}, {binary, pid}) + {{:loading, pid}, result} + + pid when is_pid(pid) -> + {{:loading, pid}, result} + end + + _ -> + {:found, result} + end + end + + defp load_module(module, binary, dest) do + {pid, _ref} = + :erlang.spawn_opt( + fn -> + beam_location = + case dest do + nil -> + [] + + dest -> + :filename.join( + :elixir_utils.characters_to_list(dest), + Atom.to_charlist(module) ++ ~c".beam" + ) + end + + :code.load_binary(module, beam_location, binary) + end, + monitor: [tag: {:module_loaded, module}] + ) + + pid + end + defp update_result(result, kind, module, value) do available = case Map.get(result, {kind, module}) do diff --git a/lib/elixir/lib/kernel/typespec.ex b/lib/elixir/lib/kernel/typespec.ex index 942cd23455f..149a302e95f 100644 --- a/lib/elixir/lib/kernel/typespec.ex +++ b/lib/elixir/lib/kernel/typespec.ex @@ -493,6 +493,24 @@ defmodule Kernel.Typespec do state} end + defp typespec( + {:<<>>, meta, [{:"::", _, [{:_, _, ctx1}, {:*, prod_meta, [size, unit]}]}]}, + _, + _, + state + ) + when is_atom(ctx1) and is_integer(size) and size >= 0 and unit in 1..256 do + location = location(meta) + prod_location = location(prod_meta) + + {{:type, location, :binary, + [ + {:op, prod_location, :*, {:integer, prod_location, size}, + {:integer, prod_location, unit}}, + {:integer, location, 0} + ]}, state} + end + defp typespec({:<<>>, meta, [{:"::", size_meta, [{:_, _, ctx}, size]}]}, _, _, state) when is_atom(ctx) and is_integer(size) and size >= 0 do location = location(meta) @@ -668,14 +686,7 @@ defmodule Kernel.Typespec do when is_list(args) do {args, state} = fn_args(meta, args, vars, caller, state) {spec, state} = typespec(return, vars, caller, state) - - fun_args = - case [args, spec] do - [{:type, _, :any}, {:type, _, :any, []}] -> [] - pair -> pair - end - - {{:type, location(meta), :fun, fun_args}, state} + {{:type, location(meta), :fun, [args, spec]}, state} end # Handle type operator @@ -945,7 +956,7 @@ defmodule Kernel.Typespec do ## Helpers - # This is a backport of Macro.expand/2 because we want to expand + # This is a modified backport of Macro.expand/2 because we want to expand # aliases but we don't them to become compile-time references. defp expand_remote({:__aliases__, meta, list} = alias, env) do case :elixir_aliases.expand_or_concat(meta, list, env, true) do @@ -953,9 +964,12 @@ defmodule Kernel.Typespec do receiver [head | tail] -> - case Macro.expand_once(head, env) do - head when is_atom(head) -> :elixir_aliases.concat([head | tail]) - _ -> alias + case Macro.expand(head, env) do + head when is_atom(head) -> + :elixir_aliases.concat([head | tail]) + + _ -> + compile_error(env, "unexpected expression in typespec: #{Macro.to_string(alias)}") end end end diff --git a/lib/elixir/lib/kernel/utils.ex b/lib/elixir/lib/kernel/utils.ex index 251be8c19de..b2c2ffc6aed 100644 --- a/lib/elixir/lib/kernel/utils.ex +++ b/lib/elixir/lib/kernel/utils.ex @@ -174,14 +174,14 @@ defmodule Kernel.Utils do true -> case enforce_keys do [] -> - quote do + quote line: 0, generated: true do Enum.reduce(kv, unquote(escaped_struct), fn {key, val}, map -> %{map | key => val} end) end _ -> - quote do + quote line: 0, generated: true do {map, keys} = Enum.reduce(kv, {unquote(escaped_struct), unquote(enforce_keys)}, fn {key, val}, {map, keys} -> @@ -201,7 +201,7 @@ defmodule Kernel.Utils do end false -> - quote do + quote line: 0, generated: true do :lists.foldl( fn {key, val}, acc -> %{acc | key => val} end, unquote(escaped_struct), diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index 117cc4bee9a..734c6dad60b 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -146,8 +146,6 @@ defmodule Macro do * `:delimiter` - contains the opening delimiter for sigils, strings, and charlists as a string (such as `"{"`, `"/"`, `"'"`, and the like) - * `:format` - set to `:keyword` when an atom is defined as a keyword - * `:do` - contains metadata about the `do` location in a function call with `do`-`end` blocks (when `:token_metadata` is true) @@ -159,10 +157,18 @@ defmodule Macro do expressions inside "blocks of code", which are either direct children of a `__block__` or the right side of `->`. The last expression of the block does not have metadata if it is not followed by an end of line - character (either a newline or `;`) + character (either a newline or `;`). This entry may appear multiple times + in the same metadata if the expression is surround by parens + + * `:format` - set to `:keyword` when an atom is defined as a keyword. + It may also be set to `:atom` to distinguish `nil`, `false`, and `true` * `:indentation` - indentation of a sigil heredoc + * `:parens` - denotes a node was surrounded by parens for grouping. + This entry may appear multiple times in the same metadata if + multiple pairs are used for grouping + The following metadata keys are private: * `:alias` - Used for alias hygiene. diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index b31eeb3203f..7eeb4f1bcb6 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -586,9 +586,11 @@ defmodule Module do name/arity pairs. Inlining is applied locally, calls from another module are not affected by this option - * `@compile {:autoload, false}` - disables automatic loading of - modules after compilation. Instead, the module will be loaded after - it is dispatched to + * `@compile {:autoload, true}` - configures if modules are automatically + loaded after definition. It defaults to `false` when compiling modules + to `.beam` files in disk (as the modules are then lazily loaded from + disk). If modules are not compiled to disk, then they are always loaded, + regardless of this flag * `@compile {:no_warn_undefined, Mod}` or `@compile {:no_warn_undefined, {Mod, fun, arity}}` - does not warn if @@ -946,7 +948,8 @@ defmodule Module do @doc """ Concatenates two aliases and returns a new alias. - It handles binaries and atoms. + It handles binaries and atoms. If one of the aliases + is nil, it is discarded. ## Examples @@ -956,6 +959,9 @@ defmodule Module do iex> Module.concat(Foo, "Bar") Foo.Bar + iex> Module.concat(Foo, nil) + Foo + """ @spec concat(binary | atom, binary | atom) :: atom def concat(left, right) diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index 42a0d61fadc..a22076997cb 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -239,7 +239,7 @@ defmodule Module.ParallelChecker do |> Module.Types.warnings(file, definitions, no_warn_undefined, cache) |> Kernel.++(behaviour_warnings) |> group_warnings() - |> emit_warnings(log?) + |> emit_warnings(file, log?) Enum.each(after_verify, fn {verify_mod, verify_fun} -> apply(verify_mod, verify_fun, [module]) @@ -320,9 +320,9 @@ defmodule Module.ParallelChecker do Enum.sort(ungrouped ++ grouped) end - defp emit_warnings(warnings, log?) do + defp emit_warnings(warnings, file, log?) do Enum.flat_map(warnings, fn {locations, diagnostic} -> - diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, &1)) + diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1)) log? and print_diagnostics(diagnostics) diagnostics end) @@ -336,10 +336,12 @@ defmodule Module.ParallelChecker do :elixir_errors.print_diagnostics(diagnostics) end - defp to_diagnostic(diagnostic, {file, position, mfa}) when is_list(position) do + defp to_diagnostic(diagnostic, source, {file, position, mfa}) when is_list(position) do + file = Path.absname(file) + %{ severity: :warning, - source: file, + source: source, file: file, position: position_to_tuple(position), stacktrace: [to_stacktrace(file, position, mfa)], @@ -437,10 +439,16 @@ defmodule Module.ParallelChecker do defp cache_chunk(table, module, exports) do Enum.each(exports, fn {{fun, arity}, info} -> - # TODO: Match on signature directly in Elixir v1.22+ + sig = + case info do + %{sig: {:strong, _, _} = sig} -> sig + %{sig: {:infer, _} = sig} -> sig + _ -> :none + end + :ets.insert( table, - {{module, {fun, arity}}, Map.get(info, :deprecated), Map.get(info, :sig, :none)} + {{module, {fun, arity}}, Map.get(info, :deprecated), sig} ) end) diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index efead6b04ba..aa704982096 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -182,9 +182,12 @@ defmodule Module.Types do end defp warn_unused_clauses(defs, stack, context) do - for {fun_arity, pending} <- context.local_used, pending != [], reduce: context do + for {fun_arity, pending} <- context.local_used, + pending != [], + {_fun_arity, kind, meta, clauses} = List.keyfind(defs, fun_arity, 0), + not Keyword.get(meta, :from_super, false), + reduce: context do context -> - {_fun_arity, kind, _meta, clauses} = List.keyfind(defs, fun_arity, 0) {_kind, _inferred, mapping} = Map.fetch!(context.local_sigs, fun_arity) clauses_indexes = diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index b8a19022e39..c01a9d12da1 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -269,7 +269,7 @@ defmodule Module.Types.Apply do case signature(name, arity) do :none -> {dynamic(), context} - info -> apply_remote(info, args_types, expr, stack, context) + info -> apply_remote(nil, name, info, args_types, expr, stack, context) end end @@ -287,10 +287,14 @@ defmodule Module.Types.Apply do {value_type, context} :badtuple -> - {error_type(), badremote_error(expr, [integer(), tuple], stack, context)} + {error_type(), + badremote_error(:erlang, :element, expr, [integer(), tuple], stack, context)} - reason -> - {error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)} + :badindex -> + mfac = mfac(expr, :erlang, :element, 2) + + {error_type(), + error({:badindex, mfac, expr, tuple, index - 1, context}, meta, stack, context)} end end @@ -308,10 +312,16 @@ defmodule Module.Types.Apply do {value_type, context} :badtuple -> - {error_type(), badremote_error(expr, [integer(), tuple, value], stack, context)} + args_types = [integer(), tuple, value] + + {error_type(), + badremote_error(:erlang, :insert_element, expr, args_types, stack, context)} + + :badindex -> + mfac = mfac(expr, :erlang, :insert_element, 3) - reason -> - {error_type(), error({reason, expr, tuple, index - 2, context}, meta, stack, context)} + {error_type(), + error({:badindex, mfac, expr, tuple, index - 2, context}, meta, stack, context)} end end @@ -322,10 +332,16 @@ defmodule Module.Types.Apply do {value_type, context} :badtuple -> - {error_type(), badremote_error(expr, [integer(), tuple], stack, context)} + args_types = [integer(), tuple] - reason -> - {error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)} + {error_type(), + badremote_error(:erlang, :delete_element, expr, args_types, stack, context)} + + :badindex -> + mfac = mfac(expr, :erlang, :delete_element, 2) + + {error_type(), + error({:badindex, mfac, expr, tuple, index - 1, context}, meta, stack, context)} end end @@ -340,7 +356,7 @@ defmodule Module.Types.Apply do {value_type, context} :badnonemptylist -> - {error_type(), badremote_error(expr, [list], stack, context)} + {error_type(), badremote_error(:erlang, :hd, expr, [list], stack, context)} end end @@ -350,7 +366,7 @@ defmodule Module.Types.Apply do {value_type, context} :badnonemptylist -> - {error_type(), badremote_error(expr, [list], stack, context)} + {error_type(), badremote_error(:erlang, :tl, expr, [list], stack, context)} end end @@ -363,14 +379,14 @@ defmodule Module.Types.Apply do match?({false, _}, map_fetch(left, :__struct__)) or match?({false, _}, map_fetch(right, :__struct__)) -> - warning = {:struct_comparison, expr, context} + warning = {:struct_comparison, expr, name, left, right, context} warn(__MODULE__, warning, elem(expr, 1), stack, context) number_type?(left) and number_type?(right) -> context disjoint?(left, right) -> - warning = {:mismatched_comparison, expr, context} + warning = {:mismatched_comparison, expr, name, left, right, context} warn(__MODULE__, warning, elem(expr, 1), stack, context) true -> @@ -384,18 +400,26 @@ defmodule Module.Types.Apply do end end - def remote(:erlang, name, [left, right] = args_types, expr, stack, context) + def remote( + :erlang, + name, + [left, right] = args_types, + {_, _, args} = expr, + stack, + context + ) when name in [:==, :"/=", :"=:=", :"=/="] do context = cond do - stack.mode == :infer -> + # We ignore quoted literals as they most likely come from generated code. + stack.mode == :infer or Macro.quoted_literal?(args) -> context name in [:==, :"/="] and number_type?(left) and number_type?(right) -> context disjoint?(left, right) -> - warning = {:mismatched_comparison, expr, context} + warning = {:mismatched_comparison, expr, name, left, right, context} warn(__MODULE__, warning, elem(expr, 1), stack, context) true -> @@ -405,30 +429,36 @@ defmodule Module.Types.Apply do {return(boolean(), args_types, stack), context} end - def remote(mod, name, args_types, expr, stack, context) do + def remote(mod, fun, args_types, expr, stack, context) do arity = length(args_types) - case :elixir_rewrite.inline(mod, name, arity) do - {mod, name} -> - remote(mod, name, args_types, expr, stack, context) + case :elixir_rewrite.inline(mod, fun, arity) do + {new_mod, new_fun} -> + expr = inline_meta(expr, mod, fun) + remote(new_mod, new_fun, args_types, expr, stack, context) false -> - {info, context} = signature(mod, name, arity, elem(expr, 1), stack, context) - apply_remote(info, args_types, expr, stack, context) + {info, context} = signature(mod, fun, arity, elem(expr, 1), stack, context) + apply_remote(mod, fun, info, args_types, expr, stack, context) end end - defp apply_remote(info, args_types, expr, stack, context) do + defp apply_remote(mod, fun, info, args_types, expr, stack, context) do case apply_signature(info, args_types, stack) do {:ok, _indexes, type} -> {type, context} {:error, domain, clauses} -> - error = {:badremote, expr, args_types, domain, clauses, context} + mfac = mfac(expr, mod, fun, length(args_types)) + error = {:badremote, mfac, expr, args_types, domain, clauses, context} {error_type(), error(error, elem(expr, 1), stack, context)} end end + defp inline_meta({node, meta, args}, mod, fun) do + {node, [inline: {mod, fun}] ++ meta, args} + end + @doc """ Returns the type of a remote capture. """ @@ -718,22 +748,28 @@ defmodule Module.Types.Apply do error(__MODULE__, warning, meta, stack, context) end - defp badremote_error({{:., _, [mod, fun]}, meta, _} = expr, args_types, stack, context) do - {_type, domain, [{args, _} | _] = clauses} = signature(mod, fun, length(args_types)) - error({:badremote, expr, args_types, domain || args, clauses, context}, meta, stack, context) + defp badremote_error(mod, fun, {_, meta, _} = expr, args_types, stack, context) do + arity = length(args_types) + mfac = mfac(expr, mod, fun, arity) + {_type, domain, [{args, _} | _] = clauses} = signature(mod, fun, arity) + domain = domain || args + tuple = {:badremote, mfac, expr, args_types, domain, clauses, context} + error(tuple, meta, stack, context) end - ## Diagnosstics + ## Diagnostics - def format_diagnostic({:badindex, expr, type, index, context}) do + def format_diagnostic({:badindex, mfac, expr, type, index, context}) do traces = collect_traces(expr, context) + {mod, fun, arity, _converter} = mfac + mfa = Exception.format_mfa(mod, fun, arity) %{ details: %{typing_traces: traces}, message: IO.iodata_to_binary([ """ - expected a tuple with at least #{pluralize(index + 1, "element", "elements")} in #{format_mfa(expr)}: + expected a tuple with at least #{pluralize(index + 1, "element", "elements")} in #{mfa}: #{expr_to_string(expr) |> indent(4)} @@ -749,7 +785,7 @@ defmodule Module.Types.Apply do def format_diagnostic({:badlocal, expr, args_types, domain, clauses, context}) do traces = collect_traces(expr, context) converter = &Function.identity/1 - {fun, _, _} = expr + {fun, meta, _} = expr explanation = empty_arg_reason(args_types) || @@ -758,16 +794,29 @@ defmodule Module.Types.Apply do #{clauses_args_to_quoted_string(clauses, converter)} """ - %{ - details: %{typing_traces: traces}, - message: - IO.iodata_to_binary([ + banner = + case fun == :super && meta[:default] && meta[:super] do + {_kind, fun} -> + """ + incompatible types given as default arguments to #{fun}/#{length(args_types)}: + """ + + _ -> """ incompatible types given to #{fun}/#{length(args_types)}: #{expr_to_string(expr) |> indent(4)} given types: + """ + end + + %{ + details: %{typing_traces: traces}, + message: + IO.iodata_to_binary([ + banner, + """ #{args_to_quoted_string(args_types, domain, converter) |> indent(4)} @@ -778,10 +827,9 @@ defmodule Module.Types.Apply do } end - def format_diagnostic({:badremote, expr, args_types, domain, clauses, context}) do + def format_diagnostic({:badremote, mfac, expr, args_types, domain, clauses, context}) do traces = collect_traces(expr, context) - {{:., _, [mod, fun]}, _, args} = expr - {mod, fun, args, converter} = :elixir_rewrite.erl_to_ex(mod, fun, args) + {mod, fun, arity, converter} = mfac explanation = empty_arg_reason(converter.(args_types)) || @@ -790,12 +838,15 @@ defmodule Module.Types.Apply do #{clauses_args_to_quoted_string(clauses, converter)} """ + mfa_or_fa = + if mod, do: Exception.format_mfa(mod, fun, arity), else: "#{fun}/#{arity}" + %{ details: %{typing_traces: traces}, message: IO.iodata_to_binary([ """ - incompatible types given to #{Exception.format_mfa(mod, fun, length(args))}: + incompatible types given to #{mfa_or_fa}: #{expr_to_string(expr) |> indent(4)} @@ -810,7 +861,7 @@ defmodule Module.Types.Apply do } end - def format_diagnostic({:mismatched_comparison, expr, context}) do + def format_diagnostic({:mismatched_comparison, expr, name, left, right, context}) do traces = collect_traces(expr, context) %{ @@ -821,6 +872,10 @@ defmodule Module.Types.Apply do comparison between distinct types found: #{expr_to_string(expr) |> indent(4)} + + given types: + + #{type_comparison_to_string(name, left, right) |> indent(4)} """, format_traces(traces), """ @@ -833,7 +888,7 @@ defmodule Module.Types.Apply do } end - def format_diagnostic({:struct_comparison, expr, context}) do + def format_diagnostic({:struct_comparison, expr, name, left, right, context}) do traces = collect_traces(expr, context) %{ @@ -844,6 +899,10 @@ defmodule Module.Types.Apply do comparison with structs found: #{expr_to_string(expr) |> indent(4)} + + given types: + + #{type_comparison_to_string(name, left, right) |> indent(4)} """, format_traces(traces), """ @@ -924,15 +983,34 @@ defmodule Module.Types.Apply do defp pluralize(1, singular, _), do: "1 #{singular}" defp pluralize(i, _, plural), do: "#{i} #{plural}" - defp format_mfa({{:., _, [mod, fun]}, _, args}) do - {mod, fun, args, _} = :elixir_rewrite.erl_to_ex(mod, fun, args) - Exception.format_mfa(mod, fun, length(args)) + defp mfac({_, [inline: {mod, fun}] ++ _, _}, _mod, _fun, arity) do + {mod, fun, arity, & &1} + end + + defp mfac({{:., _, [mod, fun]}, _, args}, _mod, _fun, _arity) + when is_atom(mod) and is_atom(fun) do + {mod, fun, args, converter} = :elixir_rewrite.erl_to_ex(mod, fun, args) + {mod, fun, length(args), converter} + end + + defp mfac(_, mod, fun, arity) + when is_atom(mod) and is_atom(fun) and is_integer(arity) do + {mod, fun, arity, & &1} end ## Algebra helpers alias Inspect.Algebra, as: IA + defp type_comparison_to_string(fun, left, right) do + {Kernel, fun, [left, right], _} = :elixir_rewrite.erl_to_ex(:erlang, fun, [left, right]) + + {fun, [], [to_quoted(left, collapse_structs: true), to_quoted(right, collapse_structs: true)]} + |> Code.Formatter.to_algebra() + |> Inspect.Algebra.format(98) + |> IO.iodata_to_binary() + end + defp clauses_args_to_quoted_string([{args, _return}], converter) do "\n " <> (clause_args_to_quoted_string(args, converter) |> indent(4)) end diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 5220f93e735..3cc5b20c551 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -367,18 +367,30 @@ defmodule Module.Types.Descr do @doc """ Converts a descr to its quoted representation. + + ## Options + + * `:collapse_structs` - do not show struct fields that match + their default type """ - def to_quoted(descr) do + def to_quoted(descr, opts \\ []) do if term_type?(descr) do {:term, [], []} else + # Dynamic always come first for visibility + {dynamic, descr} = + case :maps.take(:dynamic, descr) do + :error -> {[], descr} + {dynamic, descr} -> {to_quoted(:dynamic, dynamic, opts), descr} + end + + # Merge empty list and list together if they both exist {extra, descr} = case descr do - # Merge empty list and list together if they both exist %{list: list, bitmap: bitmap} when (bitmap &&& @bit_empty_list) != 0 -> - descr = descr |> Map.delete(:list) |> Map.update!(:bitmap, &(&1 - @bit_empty_list)) + descr = descr |> Map.delete(:list) |> Map.replace!(:bitmap, bitmap - @bit_empty_list) - case list_to_quoted(list, :list) do + case list_to_quoted(list, :list, opts) do [] -> {[{:empty_list, [], []}], descr} unions -> {unions, descr} end @@ -387,26 +399,32 @@ defmodule Module.Types.Descr do {[], descr} end - case extra ++ Enum.flat_map(descr, fn {key, value} -> to_quoted(key, value) end) do + unions = + dynamic ++ + Enum.sort( + extra ++ Enum.flat_map(descr, fn {key, value} -> to_quoted(key, value, opts) end) + ) + + case unions do [] -> {:none, [], []} - unions -> unions |> Enum.sort() |> Enum.reduce(&{:or, [], [&2, &1]}) + unions -> Enum.reduce(unions, &{:or, [], [&2, &1]}) end end end - defp to_quoted(:atom, val), do: atom_to_quoted(val) - defp to_quoted(:bitmap, val), do: bitmap_to_quoted(val) - defp to_quoted(:dynamic, descr), do: dynamic_to_quoted(descr) - defp to_quoted(:map, dnf), do: map_to_quoted(dnf) - defp to_quoted(:list, dnf), do: list_to_quoted(dnf, :non_empty_list) - defp to_quoted(:tuple, dnf), do: tuple_to_quoted(dnf) + defp to_quoted(:atom, val, _opts), do: atom_to_quoted(val) + defp to_quoted(:bitmap, val, _opts), do: bitmap_to_quoted(val) + defp to_quoted(:dynamic, descr, opts), do: dynamic_to_quoted(descr, opts) + defp to_quoted(:map, dnf, opts), do: map_to_quoted(dnf, opts) + defp to_quoted(:list, dnf, opts), do: list_to_quoted(dnf, :non_empty_list, opts) + defp to_quoted(:tuple, dnf, opts), do: tuple_to_quoted(dnf, opts) @doc """ Converts a descr to its quoted string representation. """ - def to_quoted_string(descr) do + def to_quoted_string(descr, opts \\ []) do descr - |> to_quoted() + |> to_quoted(opts) |> Code.Formatter.to_algebra() |> Inspect.Algebra.format(98) |> IO.iodata_to_binary() @@ -785,17 +803,16 @@ defmodule Module.Types.Descr do defp atom_to_quoted({:union, a}) do if :sets.is_subset(@boolset, a) do - :sets.subtract(a, @boolset) - |> :sets.to_list() - |> Enum.sort() - |> Enum.reduce({:boolean, [], []}, &{:or, [], [&2, literal_to_quoted(&1)]}) + entries = + :sets.subtract(a, @boolset) + |> :sets.to_list() + |> Enum.map(&literal_to_quoted/1) + + [{:boolean, [], []} | entries] else :sets.to_list(a) - |> Enum.sort() |> Enum.map(&literal_to_quoted/1) - |> Enum.reduce(&{:or, [], [&2, &1]}) end - |> List.wrap() end defp atom_to_quoted({:negation, a}) do @@ -1035,16 +1052,16 @@ defmodule Module.Types.Descr do end end - defp list_to_quoted(dnf, name) do + defp list_to_quoted(dnf, name, opts) do dnf = list_normalize(dnf) for {list_type, last_type, negs} <- dnf, reduce: [] do acc -> arguments = if subtype?(last_type, @empty_list) do - [to_quoted(list_type)] + [to_quoted(list_type, opts)] else - [to_quoted(list_type), to_quoted(last_type)] + [to_quoted(list_type, opts), to_quoted(last_type, opts)] end if negs == [] do @@ -1054,9 +1071,9 @@ defmodule Module.Types.Descr do |> Enum.map(fn {ty, lst} -> args = if subtype?(lst, @empty_list) do - [to_quoted(ty)] + [to_quoted(ty, opts)] else - [to_quoted(ty), to_quoted(lst)] + [to_quoted(ty, opts), to_quoted(lst, opts)] end {name, [], args} @@ -1064,11 +1081,7 @@ defmodule Module.Types.Descr do |> Enum.reduce(&{:or, [], [&2, &1]}) |> Kernel.then( &[ - {:and, [], - [ - {name, [], arguments}, - {:not, [], [&1]} - ]} + {:and, [], [{name, [], arguments}, {:not, [], [&1]}]} | acc ] ) @@ -1170,7 +1183,7 @@ defmodule Module.Types.Descr do end end - defp dynamic_to_quoted(descr) do + defp dynamic_to_quoted(descr, opts) do cond do term_type?(descr) -> [{:dynamic, [], []}] @@ -1179,7 +1192,7 @@ defmodule Module.Types.Descr do [single] true -> - case to_quoted(descr) do + case to_quoted(descr, opts) do {:none, _meta, []} = none -> [none] descr -> [{:dynamic, [], [descr]}] end @@ -1251,8 +1264,115 @@ defmodule Module.Types.Descr do defp map_only?(descr), do: empty?(Map.delete(descr, :map)) - # Union is list concatenation - defp map_union(dnf1, dnf2), do: dnf1 ++ (dnf2 -- dnf1) + defp map_union(dnf1, dnf2) do + # Union is just concatenation, but we rely on some optimization strategies to + # avoid the list to grow when possible + + # first pass trying to identify patterns where two maps can be fused as one + with [map1] <- dnf1, + [map2] <- dnf2, + optimized when optimized != nil <- maybe_optimize_map_union(map1, map2) do + [optimized] + else + # otherwise we just concatenate and remove structural duplicates + _ -> dnf1 ++ (dnf2 -- dnf1) + end + end + + defp maybe_optimize_map_union({tag1, pos1, []} = map1, {tag2, pos2, []} = map2) do + case map_union_optimization_strategy(tag1, pos1, tag2, pos2) do + :all_equal -> + map1 + + :any_map -> + {:open, %{}, []} + + {:one_key_difference, key, v1, v2} -> + new_pos = Map.put(pos1, key, union(v1, v2)) + {tag1, new_pos, []} + + :left_subtype_of_right -> + map2 + + :right_subtype_of_left -> + map1 + + nil -> + nil + end + end + + defp maybe_optimize_map_union(_, _), do: nil + + defp map_union_optimization_strategy(tag1, pos1, tag2, pos2) + defp map_union_optimization_strategy(tag, pos, tag, pos), do: :all_equal + defp map_union_optimization_strategy(:open, empty, _, _) when empty == %{}, do: :any_map + defp map_union_optimization_strategy(_, _, :open, empty) when empty == %{}, do: :any_map + + defp map_union_optimization_strategy(tag, pos1, tag, pos2) + when map_size(pos1) == map_size(pos2) do + :maps.iterator(pos1) + |> :maps.next() + |> do_map_union_optimization_strategy(pos2, :all_equal) + end + + defp map_union_optimization_strategy(:open, pos1, _, pos2) + when map_size(pos1) <= map_size(pos2) do + :maps.iterator(pos1) + |> :maps.next() + |> do_map_union_optimization_strategy(pos2, :right_subtype_of_left) + end + + defp map_union_optimization_strategy(_, pos1, :open, pos2) + when map_size(pos1) >= map_size(pos2) do + :maps.iterator(pos2) + |> :maps.next() + |> do_map_union_optimization_strategy(pos1, :right_subtype_of_left) + |> case do + :right_subtype_of_left -> :left_subtype_of_right + nil -> nil + end + end + + defp map_union_optimization_strategy(_, _, _, _), do: nil + + defp do_map_union_optimization_strategy(:none, _, status), do: status + + defp do_map_union_optimization_strategy({key, v1, iterator}, pos2, status) do + with %{^key => v2} <- pos2, + next_status when next_status != nil <- map_union_next_strategy(key, v1, v2, status) do + do_map_union_optimization_strategy(:maps.next(iterator), pos2, next_status) + else + _ -> nil + end + end + + defp map_union_next_strategy(key, v1, v2, status) + + # structurally equal values do not impact the ongoing strategy + defp map_union_next_strategy(_key, same, same, status), do: status + + defp map_union_next_strategy(key, v1, v2, :all_equal) do + if key != :__struct__, do: {:one_key_difference, key, v1, v2} + end + + defp map_union_next_strategy(_key, v1, v2, {:one_key_difference, _, d1, d2}) do + # we have at least two key differences now, we switch strategy + # if both are subtypes in one direction, keep checking + cond do + subtype?(d1, d2) and subtype?(v1, v2) -> :left_subtype_of_right + subtype?(d2, d1) and subtype?(v2, v1) -> :right_subtype_of_left + true -> nil + end + end + + defp map_union_next_strategy(_key, v1, v2, :left_subtype_of_right) do + if subtype?(v1, v2), do: :left_subtype_of_right + end + + defp map_union_next_strategy(_key, v1, v2, :right_subtype_of_left) do + if subtype?(v2, v1), do: :right_subtype_of_left + end # Given two unions of maps, intersects each pair of maps. defp map_intersection(dnf1, dnf2) do @@ -1262,7 +1382,16 @@ defmodule Module.Types.Descr do acc -> try do {tag, fields} = map_literal_intersection(tag1, pos1, tag2, pos2) - [{tag, fields, negs1 ++ negs2} | acc] + entry = {tag, fields, negs1 ++ negs2} + + # Imagine a, b, c, where a is closed and b and c are open with + # no keys in common. The result in both cases will be a and we + # want to avoid adding duplicates, especially as intersection + # is a cartesian product. + case :lists.member(entry, acc) do + true -> acc + false -> [entry | acc] + end catch :empty -> acc end @@ -1316,11 +1445,10 @@ defmodule Module.Types.Descr do :maps.next(iterator) |> map_literal_intersection_loop(acc) _ -> - # If the key is marked as not_set in the open map, we can ignore it. - if type1 == @not_set do - :maps.next(iterator) |> map_literal_intersection_loop(acc) - else - throw(:empty) + # If the key is optional in the open map, we can ignore it + case type1 do + %{optional: 1} -> :maps.next(iterator) |> map_literal_intersection_loop(acc) + _ -> throw(:empty) end end end @@ -1691,7 +1819,7 @@ defmodule Module.Types.Descr do if map_empty_negation?(tag, acc_fields, neg) do {acc_fields, acc_negs} else - case all_but_one?(tag, acc_fields, neg_tag, neg_fields) do + case map_all_but_one?(tag, acc_fields, neg_tag, neg_fields) do {:one, diff_key} -> {Map.update!(acc_fields, diff_key, &difference(&1, neg_fields[diff_key])), acc_negs} @@ -1704,10 +1832,45 @@ defmodule Module.Types.Descr do {tag, fields, negs} end) + |> map_fusion() + end + + # Given a dnf, fuse maps when possible + # e.g. %{a: integer(), b: atom()} or %{a: float(), b: atom()} into %{a: number(), b: atom()} + defp map_fusion(dnf) do + # Steps: + # 1. Group maps by tags and keys + # 2. Try fusions for each group until no fusion is found + # 3. Merge the groups back into a dnf + {without_negs, with_negs} = Enum.split_with(dnf, fn {_tag, _fields, negs} -> negs == [] end) + + without_negs = + without_negs + |> Enum.group_by(fn {tag, fields, _} -> {tag, Map.keys(fields)} end) + |> Enum.flat_map(fn {_, maps} -> map_non_negated_fuse(maps) end) + + without_negs ++ with_negs + end + + defp map_non_negated_fuse(maps) do + Enum.reduce(maps, [], fn map, acc -> + fuse_with_first_fusible(map, acc) + end) + end + + defp fuse_with_first_fusible(map, []), do: [map] + + defp fuse_with_first_fusible(map, [candidate | rest]) do + if fused = maybe_optimize_map_union(map, candidate) do + # we found a fusible candidate, we're done + [fused | rest] + else + [candidate | fuse_with_first_fusible(map, rest)] + end end # If all fields are the same except one, we can optimize map difference. - defp all_but_one?(tag1, fields1, tag2, fields2) do + defp map_all_but_one?(tag1, fields1, tag2, fields2) do keys1 = Map.keys(fields1) keys2 = Map.keys(fields2) @@ -1735,55 +1898,77 @@ defmodule Module.Types.Descr do end)) end - defp map_to_quoted(dnf) do + defp map_to_quoted(dnf, opts) do dnf |> map_normalize() - |> Enum.map(&map_each_to_quoted/1) - |> case do - [] -> [] - dnf -> Enum.reduce(dnf, &{:or, [], [&2, &1]}) |> List.wrap() - end + |> Enum.map(&map_each_to_quoted(&1, opts)) end - defp map_each_to_quoted({tag, positive_map, negative_maps}) do + defp map_each_to_quoted({tag, positive_map, negative_maps}, opts) do case negative_maps do [] -> - map_literal_to_quoted({tag, positive_map}) + map_literal_to_quoted({tag, positive_map}, opts) _ -> negative_maps - |> Enum.map(&map_literal_to_quoted/1) + |> Enum.map(&map_literal_to_quoted(&1, opts)) |> Enum.reduce(&{:or, [], [&2, &1]}) |> Kernel.then( - &{:and, [], [map_literal_to_quoted({tag, positive_map}), {:not, [], [&1]}]} + &{:and, [], [map_literal_to_quoted({tag, positive_map}, opts), {:not, [], [&1]}]} ) end end - def map_literal_to_quoted({:closed, fields}) when map_size(fields) == 0 do + def map_literal_to_quoted({:closed, fields}, _opts) when map_size(fields) == 0 do {:empty_map, [], []} end - def map_literal_to_quoted({tag, fields}) do + def map_literal_to_quoted({tag, fields}, opts) do case tag do :closed -> with %{__struct__: struct_descr} <- fields, {_, [struct]} <- atom_fetch(struct_descr) do + fields = Map.delete(fields, :__struct__) + + fields = + with true <- Keyword.get(opts, :collapse_structs, false), + [_ | _] = info <- maybe_struct(struct), + true <- Enum.all?(info, &is_map_key(fields, &1.field)) do + Enum.reduce(info, fields, fn %{field: field}, acc -> + # TODO: This should consider the struct default value + if Map.fetch!(acc, field) == term() do + Map.delete(acc, field) + else + acc + end + end) + else + _ -> fields + end + {:%, [], [ literal_to_quoted(struct), - {:%{}, [], map_fields_to_quoted(tag, Map.delete(fields, :__struct__))} + {:%{}, [], map_fields_to_quoted(tag, fields, opts)} ]} else - _ -> {:%{}, [], map_fields_to_quoted(tag, fields)} + _ -> {:%{}, [], map_fields_to_quoted(tag, fields, opts)} end :open -> - {:%{}, [], [{:..., [], nil} | map_fields_to_quoted(tag, fields)]} + {:%{}, [], [{:..., [], nil} | map_fields_to_quoted(tag, fields, opts)]} + end + end + + defp maybe_struct(struct) do + try do + struct.__info__(:struct) + rescue + _ -> nil end end - defp map_fields_to_quoted(tag, map) do + defp map_fields_to_quoted(tag, map, opts) do sorted = Enum.sort(Map.to_list(map)) keyword? = Inspect.List.keyword?(sorted) @@ -1799,9 +1984,9 @@ defmodule Module.Types.Descr do {optional?, type} = pop_optional_static(type) cond do - not optional? -> {key, to_quoted(type)} + not optional? -> {key, to_quoted(type, opts)} empty?(type) -> {key, {:not_set, [], []}} - true -> {key, {:if_set, [], [to_quoted(type)]}} + true -> {key, {:if_set, [], [to_quoted(type, opts)]}} end end end @@ -1852,8 +2037,16 @@ defmodule Module.Types.Descr do reduce: [] do acc -> case tuple_literal_intersection(tag1, elements1, tag2, elements2) do - {tag, fields} -> [{tag, fields, negs1 ++ negs2} | acc] - :empty -> acc + {tag, elements} -> + entry = {tag, elements, negs1 ++ negs2} + + case :lists.member(entry, acc) do + true -> acc + false -> [entry | acc] + end + + :empty -> + acc end end |> case do @@ -1922,37 +2115,78 @@ defmodule Module.Types.Descr do # This is a cheap optimization that relies on structural equality. defp tuple_union(left, right), do: left ++ (right -- left) - defp tuple_to_quoted(dnf) do + defp tuple_to_quoted(dnf, opts) do dnf |> tuple_simplify() - |> Enum.map(&tuple_each_to_quoted/1) - |> case do - [] -> [] - dnf -> Enum.reduce(dnf, &{:or, [], [&2, &1]}) |> List.wrap() - end + |> tuple_fusion() + |> Enum.map(&tuple_each_to_quoted(&1, opts)) + end + + # Given a dnf of tuples, fuses the tuple unions when possible, + # e.g. {integer(), atom()} or {float(), atom()} into {number(), atom()} + # The negations of two fused tuples are just concatenated. + defp tuple_fusion(dnf) do + # Steps: + # 1. Consider tuples without negations apart from those with + # 2. Group tuples by size and tag + # 3. Try fusions for each group until no fusion is found + # 4. Merge the groups back into a dnf + {without_negs, with_negs} = Enum.split_with(dnf, fn {_tag, _elems, negs} -> negs == [] end) + + without_negs = + without_negs + |> Enum.group_by(fn {tag, elems, _} -> {tag, length(elems)} end) + |> Enum.flat_map(fn {_, tuples} -> tuple_non_negated_fuse(tuples) end) + + without_negs ++ with_negs + end + + defp tuple_non_negated_fuse(tuples) do + Enum.reduce(tuples, [], fn tuple, acc -> + case Enum.split_while(acc, &non_fusible_tuples?(tuple, &1)) do + {_, []} -> + [tuple | acc] + + {others, [match | rest]} -> + fused = tuple_non_negated_fuse_pair(tuple, match) + others ++ [fused | rest] + end + end) end - defp tuple_each_to_quoted({tag, positive_map, negative_maps}) do - case negative_maps do + # Two tuples are fusible if they have no negations and differ in at most one element. + defp non_fusible_tuples?({_, elems1, []}, {_, elems2, []}) do + Enum.zip(elems1, elems2) |> Enum.count_until(fn {a, b} -> a != b end, 2) > 1 + end + + defp tuple_non_negated_fuse_pair({tag, elems1, []}, {_, elems2, []}) do + fused_elements = + Enum.zip_with(elems1, elems2, fn a, b -> if a == b, do: a, else: union(a, b) end) + + {tag, fused_elements, []} + end + + defp tuple_each_to_quoted({tag, positive_tuple, negative_tuples}, opts) do + case negative_tuples do [] -> - tuple_literal_to_quoted({tag, positive_map}) + tuple_literal_to_quoted({tag, positive_tuple}, opts) _ -> - negative_maps - |> Enum.map(&tuple_literal_to_quoted/1) + negative_tuples + |> Enum.map(&tuple_literal_to_quoted(&1, opts)) |> Enum.reduce(&{:or, [], [&2, &1]}) |> Kernel.then( - &{:and, [], [tuple_literal_to_quoted({tag, positive_map}), {:not, [], [&1]}]} + &{:and, [], [tuple_literal_to_quoted({tag, positive_tuple}, opts), {:not, [], [&1]}]} ) end end - defp tuple_literal_to_quoted({:closed, []}), do: {:{}, [], []} + defp tuple_literal_to_quoted({:closed, []}, _opts), do: {:{}, [], []} - defp tuple_literal_to_quoted({tag, elements}) do + defp tuple_literal_to_quoted({tag, elements}, opts) do case tag do - :closed -> {:{}, [], Enum.map(elements, &to_quoted/1)} - :open -> {:{}, [], Enum.map(elements, &to_quoted/1) ++ [{:..., [], nil}]} + :closed -> {:{}, [], Enum.map(elements, &to_quoted(&1, opts))} + :open -> {:{}, [], Enum.map(elements, &to_quoted(&1, opts)) ++ [{:..., [], nil}]} end end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 7aab48d6fc3..ff7f5646509 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -263,8 +263,9 @@ defmodule Module.Types.Expr do {case_type, context} = of_expr(case_expr, stack, context) # If we are only type checking the expression and the expression is a literal, - # let's mark it as generated, as it is most likely a macro code. - if is_atom(case_expr) and {:type_check, :expr} in meta do + # let's mark it as generated, as it is most likely a macro code. However, if + # no clause is matched, we should still check for that. + if Macro.quoted_literal?(case_expr) do for {:->, meta, args} <- clauses, do: {:->, [generated: true] ++ meta, args} else clauses @@ -444,7 +445,8 @@ defmodule Module.Types.Expr do # to avoid export dependencies. So we do it here. if Code.ensure_loaded?(exception) and function_exported?(exception, :__struct__, 0) do {info, context} = Of.struct_info(exception, meta, stack, context) - {Of.struct_type(exception, info, args), context} + # TODO: For properly defined structs, this should not be dynamic + {dynamic(Of.struct_type(exception, info, args)), context} else # If the exception cannot be found or is invalid, fetch the signature to emit warnings. {_, context} = Apply.signature(exception, :__struct__, 0, meta, stack, context) @@ -516,7 +518,7 @@ defmodule Module.Types.Expr do defp with_clause({:<-, _meta, [left, right]} = expr, stack, context) do {pattern, guards} = extract_head([left]) - {_type, context} = Pattern.of_match(pattern, guards, dynamic(), :with, expr, stack, context) + {_type, context} = Pattern.of_match(pattern, guards, dynamic(), expr, :with, stack, context) {_, context} = of_expr(right, stack, context) context end diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index fa1774adea4..a76260e97c9 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -85,6 +85,9 @@ defmodule Module.Types.Helpers do @doc """ Collect traces from variables in expression. + + This information is exposed to language servers and + therefore must remain backwards compatible. """ def collect_traces(expr, %{vars: vars}) do {_, versions} = @@ -135,16 +138,17 @@ defmodule Module.Types.Helpers do formatter -> formatter.(expr) end + # This information is exposed to language servers and + # therefore must remain backwards compatible. %{ file: file, - line: meta[:line], - column: meta[:column], - hints: formatter_hints ++ expr_hints(expr), + meta: meta, formatted_expr: formatted_expr, - formatted_type: Module.Types.Descr.to_quoted_string(type) + formatted_hints: format_hints(formatter_hints ++ expr_hints(expr)), + formatted_type: Module.Types.Descr.to_quoted_string(type, collapse_structs: true) } end) - |> Enum.sort_by(&{&1.line, &1.column}) + |> Enum.sort_by(&{&1.meta[:line], &1.meta[:column]}) |> Enum.dedup() end @@ -161,7 +165,7 @@ defmodule Module.Types.Helpers do location = trace.file |> Path.relative_to_cwd() - |> Exception.format_file_line(trace.line, trace.column) + |> Exception.format_file_line(trace.meta[:line], trace.meta[:column]) |> String.replace_suffix(":", "") [ @@ -173,7 +177,7 @@ defmodule Module.Types.Helpers do """, indent(trace.formatted_expr, 4), ?\n, - format_hints(trace.hints) + trace.formatted_hints ] end diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index d04b53151fb..a5bdff26995 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -495,13 +495,11 @@ defmodule Module.Types.Of do unknown key .#{key} in expression: #{expr_to_string(expr) |> indent(4)} - """, - empty_if(dot_var?(expr), """ the given type does not have the given key: #{to_quoted_string(type) |> indent(4)} - """), + """, format_traces(traces) ]) } diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 77f51a16d7c..9f4a76f678a 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -335,6 +335,10 @@ defmodule Module.Types.Pattern do {type, context} end + def of_match_var({:<<>>, _meta, args}, _expected, _expr, stack, context) do + {binary(), Of.binary(args, :match, stack, context)} + end + def of_match_var(ast, expected, expr, stack, context) do of_match(ast, expected, expr, :default, stack, context) end diff --git a/lib/elixir/lib/path.ex b/lib/elixir/lib/path.ex index 7dca45f018c..535cac5ea6c 100644 --- a/lib/elixir/lib/path.ex +++ b/lib/elixir/lib/path.ex @@ -884,7 +884,7 @@ defmodule Path do * A `..` component would make it so that the path would traverse up above the root of `relative_to`. - * A symbolic link in the path points to something above the root of `cwd`. + * A symbolic link in the path points to something above the root of `relative_to`. ## Examples @@ -906,10 +906,10 @@ defmodule Path do """ @doc since: "1.14.0" @spec safe_relative(t, t) :: {:ok, binary} | :error - def safe_relative(path, cwd \\ File.cwd!()) do + def safe_relative(path, relative_to \\ File.cwd!()) do path = IO.chardata_to_string(path) - case :filelib.safe_relative_path(path, cwd) do + case :filelib.safe_relative_path(path, relative_to) do :unsafe -> :error relative_path -> {:ok, IO.chardata_to_string(relative_path)} end diff --git a/lib/elixir/lib/process.ex b/lib/elixir/lib/process.ex index 43299a1b680..ef1c8c46a7f 100644 --- a/lib/elixir/lib/process.ex +++ b/lib/elixir/lib/process.ex @@ -219,6 +219,16 @@ defmodule Process do Inlined by the compiler. + > #### Differences to `Kernel.exit/1` {: .info } + > + > The functions `Kernel.exit/1` and `Process.exit/2` are + > named similarly but provide very different functionalities. The + > `Kernel:exit/1` function should be used when the intent is to stop the current + > process while `Process.exit/2` should be used when the intent is to send an + > exit signal to another process. Note also that `Kernel.exit/1` can be caught + > with `try/1` while `Process.exit/2` can only be handled by trapping exits and + > when the signal is different than `:kill`. + ## Examples Process.exit(pid, :kill) diff --git a/lib/elixir/lib/protocol.ex b/lib/elixir/lib/protocol.ex index 247b2b59e27..526131f5d9b 100644 --- a/lib/elixir/lib/protocol.ex +++ b/lib/elixir/lib/protocol.ex @@ -347,7 +347,7 @@ defmodule Protocol do end defp assert_impl!(protocol, base, extra) do - impl = Module.concat(protocol, base) + impl = Protocol.__concat__(protocol, base) try do Code.ensure_compiled!(impl) @@ -678,7 +678,7 @@ defmodule Protocol do end defp load_impl(protocol, for) do - Module.concat(protocol, for) + Protocol.__concat__(protocol, for) end # Finally compile the module and emit its bytecode. @@ -831,7 +831,7 @@ defmodule Protocol do # Define the implementation for built-ins :lists.foreach( fn {guard, mod} -> - target = Module.concat(__MODULE__, mod) + target = Protocol.__concat__(__MODULE__, mod) Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do case Code.ensure_compiled(unquote(target)) do @@ -875,7 +875,7 @@ defmodule Protocol do # Internal handler for Structs Kernel.defp struct_impl_for(struct) do - case Code.ensure_compiled(Module.concat(__MODULE__, struct)) do + case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do {:module, module} -> module {:error, _} -> unquote(any_impl_for) end @@ -948,7 +948,7 @@ defmodule Protocol do quote do protocol = unquote(protocol) for = unquote(for) - name = Module.concat(protocol, for) + name = Protocol.__concat__(protocol, for) Protocol.assert_protocol!(protocol) Protocol.__ensure_defimpl__(protocol, for, __ENV__) @@ -994,7 +994,7 @@ defmodule Protocol do else # TODO: Deprecate this on Elixir v1.22+ assert_impl!(protocol, Any, extra) - {Module.concat(protocol, Any), [for, Macro.struct!(for, env), opts]} + {Protocol.__concat__(protocol, Any), [for, Macro.struct!(for, env), opts]} end # Clean up variables from eval context @@ -1006,7 +1006,7 @@ defmodule Protocol do else __ensure_defimpl__(protocol, for, env) assert_impl!(protocol, Any, extra) - impl = Module.concat(protocol, Any) + impl = Protocol.__concat__(protocol, Any) funs = for {fun, arity} <- protocol.__protocol__(:functions) do @@ -1031,7 +1031,11 @@ defmodule Protocol do def __impl__(:for), do: unquote(for) end - Module.create(Module.concat(protocol, for), [quoted | funs], Macro.Env.location(env)) + Module.create( + Protocol.__concat__(protocol, for), + [quoted | funs], + Macro.Env.location(env) + ) end end) end @@ -1070,4 +1074,17 @@ defmodule Protocol do is_reference: Reference ] end + + @doc false + def __concat__(left, right) do + String.to_atom( + ensure_prefix(Atom.to_string(left)) <> "." <> remove_prefix(Atom.to_string(right)) + ) + end + + defp ensure_prefix("Elixir." <> _ = left), do: left + defp ensure_prefix(left), do: "Elixir." <> left + + defp remove_prefix("Elixir." <> right), do: right + defp remove_prefix(right), do: right end diff --git a/lib/elixir/lib/range.ex b/lib/elixir/lib/range.ex index 7265b28966b..b876ffb21d2 100644 --- a/lib/elixir/lib/range.ex +++ b/lib/elixir/lib/range.ex @@ -192,7 +192,7 @@ defmodule Range do IO.warn_once( {__MODULE__, :new}, fn -> - "Range.new/2 has a default step of -1, please call Range.new/3 explicitly passing the step of -1 instead" + "Range.new/2 and first..last default to a step of -1 when last < first. Use Range.new(first, last, -1) or first..last//-1, or pass 1 if that was your intention" end, 3 ) diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex index b6cfbc0e23c..6e363325c27 100644 --- a/lib/elixir/lib/regex.ex +++ b/lib/elixir/lib/regex.ex @@ -209,17 +209,14 @@ defmodule Regex do ## Examples - iex> Regex.compile("foo") - {:ok, ~r/foo/} + Regex.compile("foo") + #=> {:ok, ~r/foo/} - iex> Regex.compile("*foo") - {:error, {~c"nothing to repeat", 0}} + Regex.compile("foo", "i") + #=>{:ok, ~r/foo/i} - iex> Regex.compile("foo", "i") - {:ok, ~r/foo/i} - - iex> Regex.compile("foo", [:caseless]) - {:ok, Regex.compile!("foo", [:caseless])} + Regex.compile("*foo") + #=> {:error, {~c"nothing to repeat", 0}} """ @spec compile(binary, binary | [term]) :: {:ok, t} | {:error, term} diff --git a/lib/elixir/lib/stream.ex b/lib/elixir/lib/stream.ex index 495edcf12bd..a69e7f5d3b9 100644 --- a/lib/elixir/lib/stream.ex +++ b/lib/elixir/lib/stream.ex @@ -1444,7 +1444,7 @@ defmodule Stream do defp check_cycle_first_element(reduce) do fn acc -> case reduce.(acc) do - {state, []} when state in [:done, :halted] -> + {state, []} when state in [:done, :halted] and elem(acc, 0) != :halt -> raise ArgumentError, "cannot cycle over an empty enumerable" other -> diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index e8b3a61c8ca..12007e58fb3 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -488,6 +488,15 @@ defmodule String do iex> String.split(String.normalize("é", :nfc), "e") ["é"] + When using both the `:trim` and the `:parts` option, the empty values + are removed as the parts are computed (if any). No trimming happens + after all parts are computed: + + iex> String.split(" a b c ", " ", trim: true, parts: 2) + ["a", " b c "] + iex> String.split(" a b c ", " ", trim: true, parts: 3) + ["a", "b", " c "] + """ @spec split(t, pattern | Regex.t(), keyword) :: [t] def split(string, pattern, options \\ []) diff --git a/lib/elixir/pages/getting-started/keywords-and-maps.md b/lib/elixir/pages/getting-started/keywords-and-maps.md index 734597eac5f..51cb2ad7120 100644 --- a/lib/elixir/pages/getting-started/keywords-and-maps.md +++ b/lib/elixir/pages/getting-started/keywords-and-maps.md @@ -11,36 +11,40 @@ Keyword lists are a data-structure used to pass options to functions. Let's see Imagine you want to split a string of numbers. Initially, we can invoke `String.split/2` passing two strings as arguments: ```elixir -iex> String.split("1 2 3", " ") -["1", "2", "3"] +iex> String.split("1 2 3 4", " ") +["1", "2", "3", "4"] ``` -However, what happens if there is an additional space between the numbers: +What if you only want to split at most 2 times? The `String.split/3` function allows the `parts` option to be set to the maximum number of entries in the result: ```elixir -iex> String.split("1 2 3", " ") -["1", "", "2", "", "3"] +iex> String.split("1 2 3 4", " ", [parts: 3]) +["1", "2", "3 4"] ``` -As you can see, there are now empty strings in our results. Luckily, the `String.split/3` function allows the `trim` option to be set to true: +As you can see, we got 3 parts, the last one containing the remaining of the input without splitting it. + +Now imagine that some of the inputs you must split on contains additional spaces between the numbers: ```elixir -iex> String.split("1 2 3", " ", [trim: true]) -["1", "2", "3"] +iex> String.split("1 2 3 4", " ", [parts: 3]) +["1", "", "2 3 4"] ``` -We can also use options to limit the splitting algorithm to a maximum number of parts, as shown next: +As you can see, the additional spaces lead to empty entries in the output. Luckily, we can also set the `trim` option to `true` to remove them: ```elixir -iex> String.split("1 2 3", " ", [trim: true, parts: 2]) -["1", "2 3"] +iex> String.split("1 2 3 4", " ", [parts: 3, trim: true]) +["1", "2", " 3 4"] ``` -`[trim: true]` and `[trim: true, parts: 2]` are keyword lists. When a keyword list is the last argument of a function, we can skip the brackets and write: +Once again we got 3 parts, with the last one containing the leftovers. + +`[parts: 3]` and `[parts: 3, trim: true]` are keyword lists. When a keyword list is the last argument of a function, we can skip the brackets and write: ```elixir -iex> String.split("1 2 3", " ", trim: true, parts: 2) -["1", "2 3"] +iex> String.split("1 2 3 4", " ", parts: 3, trim: true) +["1", "2", " 3 4"] ``` As shown in the example above, keyword lists are mostly used as optional arguments to functions. @@ -48,7 +52,7 @@ As shown in the example above, keyword lists are mostly used as optional argumen As the name implies, keyword lists are simply lists. In particular, they are lists consisting of 2-item tuples where the first element (the key) is an atom and the second element can be any value. Both representations are the same: ```elixir -iex> [{:trim, true}, {:parts, 2}] == [trim: true, parts: 2] +iex> [{:parts, 3}, {:trim, true}] == [parts: 3, trim: true] true ``` diff --git a/lib/elixir/pages/images/kv-observer.png b/lib/elixir/pages/images/kv-observer.png new file mode 100644 index 00000000000..7527d7e5c80 Binary files /dev/null and b/lib/elixir/pages/images/kv-observer.png differ diff --git a/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md b/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md index abb883f3a87..79d3a2972ec 100644 --- a/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md +++ b/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md @@ -157,7 +157,7 @@ say me say me ``` -Yes, it works! However, does it *scale*? +Yes, it works! However, can it handle more than one client? Try to connect two telnet clients at the same time. When you do so, you will notice that the second client doesn't echo: @@ -303,4 +303,8 @@ Luckily, this can be done by using `Supervisor.child_spec/2`, which allows us to Now we have an always running acceptor that starts temporary task processes under an always running task supervisor. +## Wrapping up + +In this chapter, we implemented a basic TCP acceptor while exploring concurrency and fault-tolerance. Our acceptor can manage concurrent connections, but it is still not ready for production. Production-ready TCP servers run a pool of acceptors, each with their own supervisor. Elixir's `PartitionSupervisor` might be used to partition and scale the acceptor, but it is out of scope for this guide. In practice, you will use existing packages tailored for this use-case, such as [Ranch](https://github.com/ninenines/ranch) (in Erlang) or [Thousand Island](https://github.com/mtrudel/thousand_island) (in Elixir). + In the next chapter, we will start parsing the client requests and sending responses, finishing our server. diff --git a/lib/elixir/pages/references/compatibility-and-deprecations.md b/lib/elixir/pages/references/compatibility-and-deprecations.md index 4b53ae59c01..81a9a57b515 100644 --- a/lib/elixir/pages/references/compatibility-and-deprecations.md +++ b/lib/elixir/pages/references/compatibility-and-deprecations.md @@ -8,12 +8,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a Elixir version | Support :------------- | :----------------------------- -1.18 | Development -1.17 | Bug fixes and security patches +1.18 | Bug fixes and security patches +1.17 | Security patches only 1.16 | Security patches only 1.15 | Security patches only 1.14 | Security patches only -1.13 | Security patches only New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). All security releases [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date). @@ -43,6 +42,7 @@ Erlang/OTP versioning is independent from the versioning of Elixir. Erlang relea Elixir version | Supported Erlang/OTP versions :------------- | :------------------------------- +1.18 | 25 - 27 1.17 | 25 - 27 1.16 | 24 - 26 1.15 | 24 - 26 @@ -82,6 +82,14 @@ The first column is the version the feature was hard deprecated. The second colu Version | Deprecated feature | Replaced by (available since) :-------| :-------------------------------------------------- | :--------------------------------------------------------------- +[v1.18] | `<%#` in EEx | `<%!--` (v1.14) or `<% #` (v1.0) +[v1.18] | `handle_text/2` callback in EEx | `handle_text/3` (v1.14) +[v1.18] | Returning 2-arity fun from `Enumerable.slice/1` | Returning 3-arity (v1.14) +[v1.18] | Ranges with negative steps in `Range.new/2` | Explicit steps in ranges (v1.11) +[v1.18] | `Tuple.append/2` | `Tuple.insert_at/3` (v1.0) +[v1.18] | `mix cmd --app APP` | `mix do --app APP` (v1.14) +[v1.18] | `List.zip/1` | `Enum.zip/1` (v1.0) +[v1.18] | `Module.eval_quoted/3` | `Code.eval_quoted/3` (v1.0) [v1.17] | Single-quoted charlists (`'foo'`) | `~c"foo"` (v1.0) [v1.17] | `left..right` in patterns and guards | `left..right//step` (v1.11) [v1.17] | `ExUnit.Case.register_test/4` | `register_test/6` (v1.10) @@ -214,4 +222,5 @@ Version | Deprecated feature | Replaced by (ava [v1.14]: https://github.com/elixir-lang/elixir/blob/v1.14/CHANGELOG.md#4-hard-deprecations [v1.15]: https://github.com/elixir-lang/elixir/blob/v1.15/CHANGELOG.md#4-hard-deprecations [v1.16]: https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md#4-hard-deprecations -[v1.17]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations +[v1.17]: https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md#4-hard-deprecations +[v1.18]: https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md#4-hard-deprecations diff --git a/lib/elixir/pages/references/typespecs.md b/lib/elixir/pages/references/typespecs.md index 95eb40253f2..49d39f30e24 100644 --- a/lib/elixir/pages/references/typespecs.md +++ b/lib/elixir/pages/references/typespecs.md @@ -1,7 +1,12 @@ # Typespecs reference -Elixir comes with a notation for declaring types and specifications. This document is a -reference into their uses and syntax. +> #### Typespecs are not set-theoretic types {: .warning} +> +> Elixir is in the process of implementing its +> [own type system](./gradual-set-theoretic-types.md) based on set-theoretic types. +> Typespecs, which are described in the following document, are a distinct notation +> for declaring types and specifications based on Erlang. +> Typespecs may be phased out as the set-theoretic type effort moves forward. Elixir is a dynamically typed language, and as such, type specifications are never used by the compiler to optimize or modify code. Still, using type specifications is useful because: @@ -48,6 +53,13 @@ The syntax Elixir provides for type specifications is similar to [the one in Erl The notation to represent the union of types is the pipe `|`. For example, the typespec `type :: atom() | pid() | tuple()` creates a type `type` that can be either an `atom`, a `pid`, or a `tuple`. This is usually called a [sum type](https://en.wikipedia.org/wiki/Tagged_union) in other languages +> #### Differences with set-theoretic types {: .warning} +> +> While they do share some similarities, the types below do not map one-to-one +> to the new types from the set theoretic type system. +> For example, there is no plan to support subsets of the `integer()` type such +> as positive, ranges or literals. + ### Basic types type :: diff --git a/lib/elixir/scripts/elixir_docs.exs b/lib/elixir/scripts/elixir_docs.exs index 11086e20a38..0fe4c3f3f8e 100644 --- a/lib/elixir/scripts/elixir_docs.exs +++ b/lib/elixir/scripts/elixir_docs.exs @@ -191,12 +191,19 @@ canonical = System.fetch_env!("CANONICAL") before_closing_body_tag: fn :html -> """ + - """ _ -> diff --git a/lib/elixir/src/elixir_clauses.erl b/lib/elixir/src/elixir_clauses.erl index 6efe5577b02..85198f694ad 100644 --- a/lib/elixir/src/elixir_clauses.erl +++ b/lib/elixir/src/elixir_clauses.erl @@ -11,7 +11,7 @@ parallel_match(Meta, Expr, S, #{context := match} = E) -> #elixir_ex{vars={_Read, Write}} = S, - Matches = unpack_match(Expr, Meta, []), + Matches = unpack_match(Expr, Meta, [], E), {[{_, EHead} | ETail], EWrites, SM, EM} = lists:foldl(fun({EMeta, Match}, {AccMatches, AccWrites, SI, EI}) -> @@ -31,9 +31,13 @@ parallel_match(Meta, Expr, S, #{context := match} = E) -> VWrite = (Write /= false) andalso elixir_env:merge_vars(Write, PWrites), {EMatch, SM#elixir_ex{vars={VRead, VWrite}, prematch={PRead, PCycles, PInfo}}, EM}. -unpack_match({'=', Meta, [Left, Right]}, _Meta, Acc) -> - unpack_match(Left, Meta, unpack_match(Right, Meta, Acc)); -unpack_match(Node, Meta, Acc) -> +unpack_match({'=', Meta, [{_, VarMeta, _} = Node, Node]}, _Meta, Acc, E) -> + %% TODO: remove this clause on Elixir v1.23 + file_warn(VarMeta, ?key(E, file), ?MODULE, {duplicate_match, Node}), + unpack_match(Node, Meta, Acc, E); +unpack_match({'=', Meta, [Left, Right]}, _Meta, Acc, E) -> + unpack_match(Left, Meta, unpack_match(Right, Meta, Acc, E), E); +unpack_match(Node, Meta, Acc, _E) -> [{Meta, Node} | Acc]. store_cycles([Write | Writes], {Cycles, SkipList}, Acc) -> @@ -532,6 +536,13 @@ origin(Meta, Default) -> false -> Default end. +format_error({duplicate_match, Expr}) -> + String = 'Elixir.Macro':to_string(Expr), + io_lib:format( + "this pattern is matched against itself inside a match: ~ts = ~ts", + [String, String] + ); + format_error({recursive, Vars, TypeExpr}) -> Code = case TypeExpr of diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index 8ba322b88fd..d9164efcd59 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -203,6 +203,7 @@ bootstrap_files() -> <<"path.ex">>, <<"file.ex">>, <<"map.ex">>, + <<"function.ex">>, <<"range.ex">>, <<"access.ex">>, <<"io.ex">>, diff --git a/lib/elixir/src/elixir_def.erl b/lib/elixir/src/elixir_def.erl index af9874b2985..da348e7eeb3 100644 --- a/lib/elixir/src/elixir_def.erl +++ b/lib/elixir/src/elixir_def.erl @@ -314,7 +314,7 @@ unpack_defaults(Kind, Meta, Name, Args, S, E) -> unpack_expanded(Kind, Meta, Name, [{'\\\\', DefaultMeta, [Expr, _]} | T] = List, VersionOffset, Acc, Clauses) -> Base = match_defaults(Acc, length(Acc) + VersionOffset, []), {Args, Invoke} = extract_defaults(List, length(Base) + VersionOffset, [], []), - Clause = {Meta, Base ++ Args, [], {super, [{super, {Kind, Name}} | DefaultMeta], Base ++ Invoke}}, + Clause = {Meta, Base ++ Args, [], {super, [{super, {Kind, Name}}, {default, true} | DefaultMeta], Base ++ Invoke}}, unpack_expanded(Kind, Meta, Name, T, VersionOffset, [Expr | Acc], [Clause | Clauses]); unpack_expanded(Kind, Meta, Name, [H | T], VersionOffset, Acc, Clauses) -> unpack_expanded(Kind, Meta, Name, T, VersionOffset, [H | Acc], Clauses); diff --git a/lib/elixir/src/elixir_dispatch.erl b/lib/elixir/src/elixir_dispatch.erl index 121b1683642..779880a41e8 100644 --- a/lib/elixir/src/elixir_dispatch.erl +++ b/lib/elixir/src/elixir_dispatch.erl @@ -204,7 +204,9 @@ do_expand_import(Result, Meta, Name, Arity, Module, E, Trace) -> {import, Receiver} -> case expand_require(true, Meta, Receiver, Name, Arity, E, Trace) of {macro, _, _} = Response -> Response; - error -> {function, Receiver, Name} + error -> + Trace andalso elixir_env:trace({remote_function, Meta, Receiver, Name, Arity}, E), + {function, Receiver, Name} end; false when Module == ?kernel -> case elixir_rewrite:inline(Module, Name, Arity) of diff --git a/lib/elixir/src/elixir_erl.erl b/lib/elixir/src/elixir_erl.erl index 58800a2a966..342835dd360 100644 --- a/lib/elixir/src/elixir_erl.erl +++ b/lib/elixir/src/elixir_erl.erl @@ -92,6 +92,9 @@ elixir_to_erl(Tree, Ann) when is_binary(Tree) -> %% considers a string in a binary to be encoded in latin1, so the bytes %% are not changed in any fashion. {bin, Ann, [{bin_element, Ann, {string, Ann, binary_to_list(Tree)}, default, default}]}; +elixir_to_erl(Tree, Ann) when is_bitstring(Tree) -> + Segments = [elixir_to_erl_bitstring_segment(X, Ann) || X <- bitstring_to_list(Tree)], + {bin, Ann, Segments}; elixir_to_erl(Tree, Ann) when is_function(Tree) -> case (erlang:fun_info(Tree, type) == {type, external}) andalso (erlang:fun_info(Tree, env) == {env, []}) of @@ -111,6 +114,13 @@ elixir_to_erl(Tree, Ann) -> elixir_to_erl_cons([H | T], Ann) -> {cons, Ann, elixir_to_erl(H, Ann), elixir_to_erl_cons(T, Ann)}; elixir_to_erl_cons(T, Ann) -> elixir_to_erl(T, Ann). +elixir_to_erl_bitstring_segment(Int, Ann) when is_integer(Int) -> + {bin_element, Ann, {integer, Ann, Int}, default, [integer]}; +elixir_to_erl_bitstring_segment(Rest, Ann) when is_bitstring(Rest) -> + Size = bit_size(Rest), + <> = Rest, + {bin_element, Ann, {integer, Ann, Int}, {integer, Ann, Size}, [integer]}. + %% Returns a scope for translation. scope(_Meta, ExpandCaptures) -> @@ -165,7 +175,12 @@ dynamic_form(#{module := Module, relative_file := RelativeFile, {Def, Defmacro, Macros, Exports, Functions} = split_definition(Definitions, Unreachable, Line, [], [], [], [], {[], []}), - FilteredOpts = lists:filter(fun({no_warn_undefined, _}) -> false; (_) -> true end, Opts), + FilteredOpts = lists:filter(fun( + {no_warn_undefined, _}) -> false; + (debug_info) -> false; + (_) -> true + end, Opts), + Location = {elixir_utils:characters_to_list(RelativeFile), Line}, Prefix = [{attribute, Line, file, Location}, @@ -526,7 +541,7 @@ docs_chunk(Map, Set, Module, Anno, Def, Defmacro, Types, Callbacks, ChunkOpts) - TypeDocs = get_type_docs(Set, Types), ModuleMeta = ModuleDocMeta#{ - source_path => File, + source_path => elixir_utils:characters_to_list(File), source_annos => [Anno], behaviours => [Mod || {behaviour, Mod} <- Attributes] }, diff --git a/lib/elixir/src/elixir_erl_compiler.erl b/lib/elixir/src/elixir_erl_compiler.erl index 90848fb3b48..7650f9dcb24 100644 --- a/lib/elixir/src/elixir_erl_compiler.erl +++ b/lib/elixir/src/elixir_erl_compiler.erl @@ -4,6 +4,7 @@ spawn(Fun) -> CompilerInfo = get(elixir_compiler_info), + {error_handler, ErrorHandler} = erlang:process_info(self(), error_handler), CodeDiagnostics = case get(elixir_code_diagnostics) of @@ -13,6 +14,7 @@ spawn(Fun) -> {_, Ref} = spawn_monitor(fun() -> + erlang:process_flag(error_handler, ErrorHandler), put(elixir_compiler_info, CompilerInfo), put(elixir_code_diagnostics, CodeDiagnostics), diff --git a/lib/elixir/src/elixir_erl_try.erl b/lib/elixir/src/elixir_erl_try.erl index 3543203462f..2a3d32aaf0e 100644 --- a/lib/elixir/src/elixir_erl_try.erl +++ b/lib/elixir/src/elixir_erl_try.erl @@ -20,7 +20,15 @@ reduce_clauses([], Acc, OldStack, SAcc, _S) -> {lists:reverse(Acc), SAcc#elixir_erl{stacktrace=OldStack}}. each_clause({'catch', Meta, Raw, Expr}, S) -> - {Match, Guards} = elixir_utils:extract_splat_guards(Raw), + {Args, Guards} = elixir_utils:extract_splat_guards(Raw), + + Match = + %% Elixir v1.17 and earlier emitted single argument + %% and may still be processed via debug_info + case Args of + [X] -> [throw, X]; + [X, Y] -> [X, Y] + end, {{clause, Line, [TKind, TMatches], TGuards, TBody}, TS} = elixir_erl_clauses:clause(?ann(Meta), fun elixir_erl_pass:translate_args/3, Match, Expr, Guards, S), diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index 6dd3197fc63..c8943cbe6af 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -92,7 +92,10 @@ emit_diagnostic(Severity, Position, File, Message, Stacktrace, Options) -> Diagnostic = #{ severity => Severity, - source => File, + source => case get(elixir_compiler_file) of + undefined -> File; + CompilerFile -> CompilerFile + end, file => File, position => Position, message => unicode:characters_to_binary(Message), @@ -216,12 +219,12 @@ highlight_at_position(Column, Severity, Length) -> highlight_below_line(Line, Severity) -> % Don't highlight leading whitespaces in line - {_, SpacesMatched} = trim_line(Line, 0), + {Rest, SpacesMatched} = trim_line(Line, 0), - Length = string:length(Line), + Length = string:length(Rest), Highlight = case Severity of - warning -> highlight(lists:duplicate(Length - SpacesMatched, $~), warning); - error -> highlight(lists:duplicate(Length - SpacesMatched, $^), error) + warning -> highlight(lists:duplicate(Length, $~), warning); + error -> highlight(lists:duplicate(Length, $^), error) end, [n_spaces(SpacesMatched), Highlight]. diff --git a/lib/elixir/src/elixir_fn.erl b/lib/elixir/src/elixir_fn.erl index 668b201909d..fafdbd8207a 100644 --- a/lib/elixir/src/elixir_fn.erl +++ b/lib/elixir/src/elixir_fn.erl @@ -112,6 +112,10 @@ capture_expr(Meta, Expr, S, E, Escaped, Sequential) -> case escape(Expr, E, Escaped) of {_, []} when not Sequential -> invalid_capture(Meta, Expr, E); + {{{'.', _, [_, _]} = Dot, _, Args}, []} -> + Meta2 = lists:keydelete(no_parens, 1, Meta), + Fn = {fn, Meta2, [{'->', Meta2, [[], {Dot, Meta2, Args}]}]}, + {expand, Fn, S, E}; {EExpr, EDict} -> EVars = validate(Meta, EDict, 1, E), Fn = {fn, Meta, [{'->', Meta, [EVars, EExpr]}]}, diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index 4725cfd2a72..ef14c919108 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -155,6 +155,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> put_compiler_modules([Module | CompilerModules]), {Result, ModuleE, CallbackE} = eval_form(Line, Module, DataBag, Block, Vars, Prune, E), CheckerInfo = checker_info(), + {BeamLocation, Forceload} = beam_location(ModuleAsCharlist), {Binary, PersistedAttributes, Autoload} = elixir_erl_compiler:spawn(fun() -> @@ -214,17 +215,17 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> compile_error_if_tainted(DataSet, E), Binary = elixir_erl:compile(ModuleMap), - Autoload = proplists:get_value(autoload, CompileOpts, true), + Autoload = Forceload or proplists:get_value(autoload, CompileOpts, false), spawn_parallel_checker(CheckerInfo, Module, ModuleMap), {Binary, PersistedAttributes, Autoload} end), - Autoload andalso code:load_binary(Module, beam_location(ModuleAsCharlist), Binary), + Autoload andalso code:load_binary(Module, BeamLocation, Binary), + make_module_available(Module, Binary, Autoload), put_compiler_modules(CompilerModules), eval_callbacks(Line, DataBag, after_compile, [CallbackE, Binary], CallbackE), elixir_env:trace({on_module, Binary, none}, ModuleE), warn_unused_attributes(DataSet, DataBag, PersistedAttributes, E), - make_module_available(Module, Binary), (element(2, CheckerInfo) == nil) andalso [VerifyMod:VerifyFun(Module) || {VerifyMod, VerifyFun} <- bag_lookup_element(DataBag, {accumulate, after_verify}, 2)], @@ -543,10 +544,11 @@ bag_lookup_element(Table, Name, Pos) -> beam_location(ModuleAsCharlist) -> case get(elixir_compiler_dest) of - Dest when is_binary(Dest) -> - filename:join(elixir_utils:characters_to_list(Dest), ModuleAsCharlist ++ ".beam"); + {Dest, Forceload} when is_binary(Dest) -> + {filename:join(elixir_utils:characters_to_list(Dest), ModuleAsCharlist ++ ".beam"), + Forceload}; _ -> - "" + {"", true} end. %% Integration with elixir_compiler that makes the module available @@ -567,7 +569,7 @@ spawn_parallel_checker(CheckerInfo, Module, ModuleMap) -> end, 'Elixir.Module.ParallelChecker':spawn(CheckerInfo, Module, ModuleMap, Log). -make_module_available(Module, Binary) -> +make_module_available(Module, Binary, Loaded) -> case get(elixir_module_binaries) of Current when is_list(Current) -> put(elixir_module_binaries, [{Module, Binary} | Current]); @@ -580,7 +582,7 @@ make_module_available(Module, Binary) -> ok; {PID, _} -> Ref = make_ref(), - PID ! {module_available, self(), Ref, get(elixir_compiler_file), Module, Binary}, + PID ! {module_available, self(), Ref, get(elixir_compiler_file), Module, Binary, Loaded}, receive {Ref, ack} -> ok end end. diff --git a/lib/elixir/src/elixir_overridable.erl b/lib/elixir/src/elixir_overridable.erl index c08b7ec37a2..6defa05f8b6 100644 --- a/lib/elixir/src/elixir_overridable.erl +++ b/lib/elixir/src/elixir_overridable.erl @@ -75,8 +75,9 @@ store_not_overridden(Module) -> %% Private store(Set, Module, Tuple, {_, Count, Def, Overridden}, Hidden) -> - {{{def, {Name, Arity}}, Kind, Meta, File, _Check, + {{{def, {Name, Arity}}, Kind, BaseMeta, File, _Check, {Defaults, _HasBody, _LastDefaults}}, Clauses} = Def, + Meta = [{from_super, Hidden} | BaseMeta], {FinalKind, FinalName, FinalArity, FinalClauses} = case Hidden of diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index 0290803970f..33f08c0ff28 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -266,11 +266,7 @@ tokenize([$" | T], Line, Column, Scope, Tokens) -> %% TODO: Remove me in Elixir v2.0 tokenize([$' | T], Line, Column, Scope, Tokens) -> - Message = "single-quoted strings represent charlists. " - "Use ~c\"\" if you indeed want a charlist or use \"\" instead.\n" - "You may run \"mix format --migrate\" to fix this warning automatically.", - NewScope = prepend_warning(Line, Column, Message, Scope), - handle_strings(T, Line, Column + 1, $', NewScope, Tokens); + handle_strings(T, Line, Column + 1, $', Scope, Tokens); % Operator atoms @@ -786,7 +782,11 @@ handle_strings(T, Line, Column, H, Scope, Tokens) -> "number do not require quotes", [hd(Parts)] ), - prepend_warning(Line, Column, WarnMsg, InterScope); + prepend_warning(Line, Column-1, WarnMsg, InterScope); + + false when H =:= $' -> + WarnMsg = "single quotes around keywords are deprecated. Use double quotes instead", + prepend_warning(Line, Column-1, WarnMsg, InterScope); false -> InterScope @@ -814,7 +814,20 @@ handle_strings(T, Line, Column, H, Scope, Tokens) -> error(Reason, Rest, NewScope, Tokens) end; - {NewLine, NewColumn, Parts, Rest, NewScope} -> + {NewLine, NewColumn, Parts, Rest, InterScope} -> + NewScope = + case H of + $' -> + Message = "using single-quoted strings to represent charlists is deprecated.\n" + "Use ~c\"\" if you indeed want a charlist or use \"\" instead.\n" + "You may run \"mix format --migrate\" to change all single-quoted\n" + "strings to use the ~c sigil and fix this warning.", + prepend_warning(Line, Column-1, Message, InterScope); + + _ -> + InterScope + end, + case unescape_tokens(Parts, Line, Column, NewScope) of {ok, Unescaped} -> Token = {string_type(H), {Line, Column - 1, nil}, Unescaped}, @@ -1750,84 +1763,81 @@ add_cursor(_Line, Column, noprune, Terminators, Tokens) -> {Column, Terminators, Tokens}; add_cursor(Line, Column, prune_and_cursor, Terminators, Tokens) -> PrePrunedTokens = prune_identifier(Tokens), - {PrunedTokens, PrunedTerminators} = prune_tokens(PrePrunedTokens, [], Terminators), + PrunedTokens = prune_tokens(PrePrunedTokens, []), CursorTokens = [ {')', {Line, Column + 11, nil}}, {'(', {Line, Column + 10, nil}}, {paren_identifier, {Line, Column, nil}, '__cursor__'} | PrunedTokens ], - {Column + 12, PrunedTerminators, CursorTokens}. + {Column + 12, Terminators, CursorTokens}. prune_identifier([{identifier, _, _} | Tokens]) -> Tokens; prune_identifier(Tokens) -> Tokens. %%% Any terminator needs to be closed -prune_tokens([{'end', _} | Tokens], Opener, Terminators) -> - prune_tokens(Tokens, ['end' | Opener], Terminators); -prune_tokens([{')', _} | Tokens], Opener, Terminators) -> - prune_tokens(Tokens, [')' | Opener], Terminators); -prune_tokens([{']', _} | Tokens], Opener, Terminators) -> - prune_tokens(Tokens, [']' | Opener], Terminators); -prune_tokens([{'}', _} | Tokens], Opener, Terminators) -> - prune_tokens(Tokens, ['}' | Opener], Terminators); -prune_tokens([{'>>', _} | Tokens], Opener, Terminators) -> - prune_tokens(Tokens, ['>>' | Opener], Terminators); +prune_tokens([{'end', _} | Tokens], Opener) -> + prune_tokens(Tokens, ['end' | Opener]); +prune_tokens([{')', _} | Tokens], Opener) -> + prune_tokens(Tokens, [')' | Opener]); +prune_tokens([{']', _} | Tokens], Opener) -> + prune_tokens(Tokens, [']' | Opener]); +prune_tokens([{'}', _} | Tokens], Opener) -> + prune_tokens(Tokens, ['}' | Opener]); +prune_tokens([{'>>', _} | Tokens], Opener) -> + prune_tokens(Tokens, ['>>' | Opener]); %%% Close opened terminators -prune_tokens([{'fn', _} | Tokens], ['end' | Opener], Terminators) -> - prune_tokens(Tokens, Opener, Terminators); -prune_tokens([{'do', _} | Tokens], ['end' | Opener], Terminators) -> - prune_tokens(Tokens, Opener, Terminators); -prune_tokens([{'(', _} | Tokens], [')' | Opener], Terminators) -> - prune_tokens(Tokens, Opener, Terminators); -prune_tokens([{'[', _} | Tokens], [']' | Opener], Terminators) -> - prune_tokens(Tokens, Opener, Terminators); -prune_tokens([{'{', _} | Tokens], ['}' | Opener], Terminators) -> - prune_tokens(Tokens, Opener, Terminators); -prune_tokens([{'<<', _} | Tokens], ['>>' | Opener], Terminators) -> - prune_tokens(Tokens, Opener, Terminators); -%%% Handle anonymous functions -prune_tokens([{'(', _}, {capture_op, _, _} | Tokens], [], [{'(', _, _} | Terminators]) -> - prune_tokens(Tokens, [], Terminators); +prune_tokens([{'fn', _} | Tokens], ['end' | Opener]) -> + prune_tokens(Tokens, Opener); +prune_tokens([{'do', _} | Tokens], ['end' | Opener]) -> + prune_tokens(Tokens, Opener); +prune_tokens([{'(', _} | Tokens], [')' | Opener]) -> + prune_tokens(Tokens, Opener); +prune_tokens([{'[', _} | Tokens], [']' | Opener]) -> + prune_tokens(Tokens, Opener); +prune_tokens([{'{', _} | Tokens], ['}' | Opener]) -> + prune_tokens(Tokens, Opener); +prune_tokens([{'<<', _} | Tokens], ['>>' | Opener]) -> + prune_tokens(Tokens, Opener); %%% or it is time to stop... -prune_tokens([{';', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{'eol', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{',', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{'fn', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{'do', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{'(', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{'[', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{'{', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{'<<', _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{identifier, _, _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{block_identifier, _, _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{kw_identifier, _, _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{kw_identifier_safe, _, _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{kw_identifier_unsafe, _, _} | _] = Tokens, [], Terminators) -> - {Tokens, Terminators}; -prune_tokens([{OpType, _, _} | _] = Tokens, [], Terminators) +prune_tokens([{';', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{'eol', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{',', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{'fn', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{'do', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{'(', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{'[', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{'{', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{'<<', _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{identifier, _, _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{block_identifier, _, _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{kw_identifier, _, _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{kw_identifier_safe, _, _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{kw_identifier_unsafe, _, _} | _] = Tokens, []) -> + Tokens; +prune_tokens([{OpType, _, _} | _] = Tokens, []) when OpType =:= comp_op; OpType =:= at_op; OpType =:= unary_op; OpType =:= and_op; OpType =:= or_op; OpType =:= arrow_op; OpType =:= match_op; OpType =:= in_op; OpType =:= in_match_op; OpType =:= type_op; OpType =:= dual_op; OpType =:= mult_op; OpType =:= power_op; OpType =:= concat_op; OpType =:= range_op; OpType =:= xor_op; OpType =:= pipe_op; OpType =:= stab_op; OpType =:= when_op; OpType =:= assoc_op; OpType =:= rel_op; OpType =:= ternary_op; OpType =:= capture_op; OpType =:= ellipsis_op -> - {Tokens, Terminators}; + Tokens; %%% or we traverse until the end. -prune_tokens([_ | Tokens], Opener, Terminators) -> - prune_tokens(Tokens, Opener, Terminators); -prune_tokens([], [], Terminators) -> - {[], Terminators}. +prune_tokens([_ | Tokens], Opener) -> + prune_tokens(Tokens, Opener); +prune_tokens([], _Opener) -> + []. diff --git a/lib/elixir/test/elixir/code_fragment_test.exs b/lib/elixir/test/elixir/code_fragment_test.exs index a36f498e05f..1efa1974724 100644 --- a/lib/elixir/test/elixir/code_fragment_test.exs +++ b/lib/elixir/test/elixir/code_fragment_test.exs @@ -1315,9 +1315,15 @@ defmodule CodeFragmentTest do assert cc2q!("(fn x ->", trailing_fragment: ":ok end)") == s2q!("(fn x -> __cursor__() end)") - assert cc2q!("(fn x ->", trailing_fragment: ":ok end)") == + assert cc2q!("(fn x ->", trailing_fragment: "\n:ok end)") == s2q!("(fn x -> __cursor__() end)") + assert cc2q!("(fn x when ", trailing_fragment: "-> :ok end)") == + s2q!("(fn x when __cursor__() -> :ok end)") + + assert cc2q!("(fn x when ", trailing_fragment: "->\n:ok end)") == + s2q!("(fn x when __cursor__() -> :ok end)") + assert cc2q!("(fn") == s2q!("(__cursor__())") assert cc2q!("(fn x") == s2q!("(__cursor__())") assert cc2q!("(fn x,") == s2q!("(__cursor__())") @@ -1327,6 +1333,26 @@ defmodule CodeFragmentTest do assert cc2q!("(fn x, y -> x + y end") == s2q!("(__cursor__())") end + test "do -> end" do + assert cc2q!("if do\nx ->\n", trailing_fragment: "y\nz ->\nw\nend") == + s2q!("if do\nx ->\n__cursor__()\nz -> \nw\nend") + + assert cc2q!("if do\nx ->\ny", trailing_fragment: "\nz ->\nw\nend") == + s2q!("if do\nx ->\n__cursor__()\nz -> \nw\nend") + + assert cc2q!("if do\nx ->\ny\n", trailing_fragment: "\nz ->\nw\nend") == + s2q!("if do\nx ->\ny\n__cursor__()\nz -> \nw\nend") + + assert cc2q!("for x <- [], reduce: %{} do\ny, ", trailing_fragment: "-> :ok\nend") == + s2q!("for x <- [], reduce: %{} do\ny, __cursor__() -> :ok\nend") + + assert cc2q!("for x <- [], reduce: %{} do\ny, z when ", trailing_fragment: "-> :ok\nend") == + s2q!("for x <- [], reduce: %{} do\ny, z when __cursor__() -> :ok\nend") + + assert cc2q!("case do\na -> a\nb = ", trailing_fragment: "c -> c\nend") == + s2q!("case do\na -> a\nb = __cursor__() -> c\nend") + end + test "removes tokens until opening" do assert cc2q!("(123") == s2q!("(__cursor__())") assert cc2q!("[foo") == s2q!("[__cursor__()]") diff --git a/lib/elixir/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index 483849c4f22..e19c21381bf 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -1517,6 +1517,18 @@ defmodule EnumTest do assert Enum.zip([], []) == [] end + test "zip/2 with infinite streams" do + assert Enum.zip([], Stream.cycle([1, 2])) == [] + assert Enum.zip([], Stream.cycle(1..2)) == [] + assert Enum.zip(.., Stream.cycle([1, 2])) == [] + assert Enum.zip(.., Stream.cycle(1..2)) == [] + + assert Enum.zip(Stream.cycle([1, 2]), ..) == [] + assert Enum.zip(Stream.cycle(1..2), ..) == [] + assert Enum.zip(Stream.cycle([1, 2]), ..) == [] + assert Enum.zip(Stream.cycle(1..2), ..) == [] + end + test "zip/1" do assert Enum.zip([[:a, :b], [1, 2], ["foo", "bar"]]) == [{:a, 1, "foo"}, {:b, 2, "bar"}] diff --git a/lib/elixir/test/elixir/exception_test.exs b/lib/elixir/test/elixir/exception_test.exs index a0032a85f45..4059cf37054 100644 --- a/lib/elixir/test/elixir/exception_test.exs +++ b/lib/elixir/test/elixir/exception_test.exs @@ -46,6 +46,12 @@ defmodule ExceptionTest do {:io, :put_chars, [self(), <<222>>], [error_info: %{module: __MODULE__, function: :dummy_error_extras}]} ]) + + assert %ErlangError{original: {:failed_load_cacerts, :enoent}, reason: ": this is chardata"} = + Exception.normalize(:error, {:failed_load_cacerts, :enoent}, [ + {:pubkey_os_cacerts, :get, 0, + [error_info: %{module: __MODULE__, function: :dummy_error_chardata}]} + ]) end test "format/2 without stacktrace" do @@ -1026,4 +1032,8 @@ defmodule ExceptionTest do end def dummy_error_extras(_exception, _stacktrace), do: %{general: "foo"} + + def dummy_error_chardata(_exception, _stacktrace) do + %{general: ~c"this is " ++ [~c"chardata"], reason: ~c"this " ++ [~c"too"]} + end end diff --git a/lib/elixir/test/elixir/fixtures/dialyzer/protocol_opaque.ex b/lib/elixir/test/elixir/fixtures/dialyzer/protocol_opaque.ex deleted file mode 100644 index d6a00f31229..00000000000 --- a/lib/elixir/test/elixir/fixtures/dialyzer/protocol_opaque.ex +++ /dev/null @@ -1,29 +0,0 @@ -defmodule Dialyzer.ProtocolOpaque do - def circus() do - duck = Dialyzer.ProtocolOpaque.Duck.new() - Dialyzer.ProtocolOpaque.Entity.speak(duck) - end -end - -defprotocol Dialyzer.ProtocolOpaque.Entity do - @fallback_to_any true - def speak(entity) -end - -defmodule Dialyzer.ProtocolOpaque.Duck do - @opaque t :: %__MODULE__{feathers: :white_and_grey} - defstruct feathers: :white_and_grey - - @spec new :: t - def new(), do: %__MODULE__{} - - defimpl Dialyzer.ProtocolOpaque.Entity do - def speak(%Dialyzer.ProtocolOpaque.Duck{}), do: "Quack!" - end -end - -defimpl Dialyzer.ProtocolOpaque.Entity, for: Any do - def speak(_any) do - "I can be anything" - end -end diff --git a/lib/elixir/test/elixir/json_test.exs b/lib/elixir/test/elixir/json_test.exs index 8e301944834..6411db2fbe0 100644 --- a/lib/elixir/test/elixir/json_test.exs +++ b/lib/elixir/test/elixir/json_test.exs @@ -33,8 +33,8 @@ defmodule JSONTest do end test "maps" do - assert JSON.encode!(%{1 => 2, 3.0 => 4.0, key: :bar}) == - "{\"1\":2,\"3.0\":4.0,\"key\":\"bar\"}" + assert JSON.encode!(%{1 => 2, 3.0 => 4.0, ~c"list" => ~c"list", key: :bar}) == + "{\"1\":2,\"3.0\":4.0,\"key\":\"bar\",\"list\":[108,105,115,116]}" end test "lists" do @@ -46,6 +46,14 @@ defmodule JSONTest do assert JSON.encode!(%Token{value: :example}) == "[\"example\"]" assert JSON.encode!(%Token{value: "hello\0world"}) == "[\"hello\\u0000world\"]" end + + test "calendar" do + assert JSON.encode!(~D[2010-04-17]) == "\"2010-04-17\"" + assert JSON.encode!(~T[14:00:00.123]) == "\"14:00:00.123\"" + assert JSON.encode!(~N[2010-04-17 14:00:00.123]) == "\"2010-04-17T14:00:00.123\"" + assert JSON.encode!(~U[2010-04-17 14:00:00.123Z]) == "\"2010-04-17T14:00:00.123Z\"" + assert JSON.encode!(Duration.new!(month: 2, hour: 3)) == "\"P2MT3H\"" + end end describe "JSON.Encoder" do @@ -72,8 +80,8 @@ defmodule JSONTest do end test "maps" do - assert protocol_encode(%{1 => 2, 3.0 => 4.0, key: :bar}) == - "{\"1\":2,\"3.0\":4.0,\"key\":\"bar\"}" + assert protocol_encode(%{1 => 2, 3.0 => 4.0, ~c"list" => ~c"list", key: :bar}) == + "{\"1\":2,\"3.0\":4.0,\"key\":\"bar\",\"list\":[108,105,115,116]}" end test "lists" do diff --git a/lib/elixir/test/elixir/kernel/cli_test.exs b/lib/elixir/test/elixir/kernel/cli_test.exs index 79602df7b72..6779161fad9 100644 --- a/lib/elixir/test/elixir/kernel/cli_test.exs +++ b/lib/elixir/test/elixir/kernel/cli_test.exs @@ -64,9 +64,7 @@ end test_parameters = if(PathHelpers.windows?(), do: [%{cli_extension: ".bat"}], - else: - [%{cli_extension: ""}] ++ - if(System.find_executable("pwsh"), do: [%{cli_extension: ".ps1"}], else: []) + else: [%{cli_extension: ""}] ) defmodule Kernel.CLI.ExecutableTest do diff --git a/lib/elixir/test/elixir/kernel/diagnostics_test.exs b/lib/elixir/test/elixir/kernel/diagnostics_test.exs index f016ac75cc7..0ce2ed7a88b 100644 --- a/lib/elixir/test/elixir/kernel/diagnostics_test.exs +++ b/lib/elixir/test/elixir/kernel/diagnostics_test.exs @@ -859,6 +859,35 @@ defmodule Kernel.DiagnosticsTest do purge(Sample) end + @tag :tmp_dir + test "simple warning with tabs (line + file)", %{tmp_dir: tmp_dir} do + path = make_relative_tmp(tmp_dir, "long-warning.ex") + + source = """ + defmodule Sample do + \t@file "#{path}" + \tdefp a do + \t\tUnknown.b() + \tend + end + """ + + File.write!(path, source) + + expected = """ + warning: Unknown.b/0 is undefined (module Unknown is not available or is yet to be defined). Make sure the module name is correct and has been specified in full (or that an alias has been defined) + │ + 4 │ \t\tUnknown.b() + │ ~~~~~~~~~~~ + │ + └─ #{path}:4: Sample.a/0 + """ + + assert capture_eval(source, columns: false) =~ expected + after + purge(Sample) + end + test "simple warning (no file)" do source = """ defmodule Sample do diff --git a/lib/elixir/test/elixir/kernel/dialyzer_test.exs b/lib/elixir/test/elixir/kernel/dialyzer_test.exs index d055e71c618..868127c264f 100644 --- a/lib/elixir/test/elixir/kernel/dialyzer_test.exs +++ b/lib/elixir/test/elixir/kernel/dialyzer_test.exs @@ -119,23 +119,6 @@ defmodule Kernel.DialyzerTest do assert_dialyze_no_warnings!(context) end - @tag warnings: [:specdiffs] - test "no warnings on protocol calls with opaque types", context do - alias Dialyzer.ProtocolOpaque - - copy_beam!(context, ProtocolOpaque) - copy_beam!(context, ProtocolOpaque.Entity) - copy_beam!(context, ProtocolOpaque.Entity.Any) - copy_beam!(context, ProtocolOpaque.Duck) - assert_dialyze_no_warnings!(context) - - # Also ensure no warnings after consolidation. - Code.prepend_path(context.base_dir) - {:ok, binary} = Protocol.consolidate(ProtocolOpaque.Entity, [ProtocolOpaque.Duck, Any]) - File.write!(Path.join(context.outdir, "#{ProtocolOpaque.Entity}.beam"), binary) - assert_dialyze_no_warnings!(context) - end - test "no warnings on and/2 and or/2", context do copy_beam!(context, Dialyzer.BooleanCheck) assert_dialyze_no_warnings!(context) diff --git a/lib/elixir/test/elixir/kernel/docs_test.exs b/lib/elixir/test/elixir/kernel/docs_test.exs index c87c9d045c1..ba1cea74f78 100644 --- a/lib/elixir/test/elixir/kernel/docs_test.exs +++ b/lib/elixir/test/elixir/kernel/docs_test.exs @@ -255,7 +255,7 @@ defmodule Kernel.DocsTest do assert module_doc == "Module doc" - file = __ENV__.file + file = String.to_charlist(__ENV__.file) source_annos = [:erl_anno.new({line + 3, 19})] diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index 86e500e0fb4..4e08384a5b0 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -190,17 +190,6 @@ defmodule Kernel.ExpansionTest do end test "errors on directly recursive definitions" do - assert_compile_error( - ~r""" - recursive variable definition in patterns: - - x = x - - the variable "x" \(context Kernel.ExpansionTest\) is defined in function of itself - """, - fn -> expand(quote(do: (x = x) = :ok)) end - ) - assert_compile_error( ~r""" recursive variable definition in patterns: @@ -1214,6 +1203,11 @@ defmodule Kernel.ExpansionTest do [{:->, [{:line, 1}], [[{:capture, [line: 1], nil}], {:capture, [line: 1], nil}]}]} end + test "removes no_parens when expanding 0-arity capture to fn" do + assert expand(quote(do: &foo().bar/0)) == + quote(do: fn -> foo().bar() end) + end + test "expands remotes" do assert expand(quote(do: &List.flatten/2)) == quote(do: &:"Elixir.List".flatten/2) diff --git a/lib/elixir/test/elixir/kernel/fn_test.exs b/lib/elixir/test/elixir/kernel/fn_test.exs index 1304d753577..0d0e8471994 100644 --- a/lib/elixir/test/elixir/kernel/fn_test.exs +++ b/lib/elixir/test/elixir/kernel/fn_test.exs @@ -95,6 +95,12 @@ defmodule Kernel.FnTest do assert (&mod.flatten/1) == (&List.flatten/1) end + test "capture with module from local call" do + assert (&math_mod().pi/0).() == :math.pi() + end + + defp math_mod, do: :math + test "local partial application" do assert (&atb(&1, :utf8)).(:a) == "a" assert (&atb(List.to_atom(&1), :utf8)).(~c"a") == "a" diff --git a/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs b/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs index c4e10309073..c464899b042 100644 --- a/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs +++ b/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs @@ -517,5 +517,55 @@ defmodule Kernel.LexicalTrackerTest do refute URI in exports refute URI in runtime end + + test "imported functions from quote adds dependencies" do + {{compile, exports, runtime, _}, _binding} = + Code.eval_string(""" + defmodule Kernel.LexicalTrackerTest.QuotedFun do + import URI + + defmacro parse_root() do + quote do + parse("/") + end + end + end + + defmodule Kernel.LexicalTrackerTest.UsingQuotedFun do + require Kernel.LexicalTrackerTest.QuotedFun, as: QF + QF.parse_root() + Kernel.LexicalTracker.references(__ENV__.lexical_tracker) + end |> elem(3) + """) + + assert URI in compile + refute URI in exports + refute URI in runtime + end + + test "imported macro from quote adds dependencies" do + {{compile, exports, runtime, _}, _binding} = + Code.eval_string(""" + defmodule Kernel.LexicalTrackerTest.QuotedMacro do + import Config + + defmacro config_env() do + quote do + config_env() + end + end + end + + defmodule Kernel.LexicalTrackerTest.UsingQuotedMacro do + require Kernel.LexicalTrackerTest.QuotedMacro, as: QM + def fun(), do: QM.config_env() + Kernel.LexicalTracker.references(__ENV__.lexical_tracker) + end |> elem(3) + """) + + assert Config in compile + refute Config in exports + refute Config in runtime + end end end diff --git a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs index 95b5ae32b9e..6a0dc97a051 100644 --- a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs +++ b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs @@ -421,40 +421,6 @@ defmodule Kernel.ParallelCompilerTest do end) end - test "supports warnings as errors" do - [fixture] = - write_tmp( - "warnings_as_errors", - warnings_as_errors: """ - defmodule WarningsSample do - def hello(a), do: a - def hello(b), do: b - end - """ - ) - - output = tmp_path("not_to_be_used") - - try do - msg = - capture_io(:stderr, fn -> - assert {:error, [error], []} = - Kernel.ParallelCompiler.compile_to_path([fixture], output, - warnings_as_errors: true - ) - - assert {^fixture, {3, 7}, "this clause " <> _} = error - end) - - assert msg =~ - "Compilation failed due to warnings while using the --warnings-as-errors option\n" - after - purge([WarningsSample]) - end - - refute File.exists?(output) - end - test "does not use incorrect line number when error originates in another file" do File.mkdir_p!(tmp_path()) @@ -587,33 +553,5 @@ defmodule Kernel.ParallelCompilerTest do "cannot compile module WithBehaviourAndStruct (errors have been logged)" end) =~ expected_msg end - - test "supports warnings as errors" do - [fixture] = - write_tmp( - "warnings_as_errors", - warnings_as_errors: """ - defmodule WarningsSample do - def hello(a), do: a - def hello(b), do: b - end - """ - ) - - try do - msg = - capture_io(:stderr, fn -> - assert {:error, [error], []} = - Kernel.ParallelCompiler.require([fixture], warnings_as_errors: true) - - assert {^fixture, {3, 7}, "this clause " <> _} = error - end) - - assert msg =~ - "Compilation failed due to warnings while using the --warnings-as-errors option\n" - after - purge([WarningsSample]) - end - end end end diff --git a/lib/elixir/test/elixir/kernel/warning_test.exs b/lib/elixir/test/elixir/kernel/warning_test.exs index 1f0f975adbc..1112b350b20 100644 --- a/lib/elixir/test/elixir/kernel/warning_test.exs +++ b/lib/elixir/test/elixir/kernel/warning_test.exs @@ -190,7 +190,7 @@ defmodule Kernel.WarningTest do ) assert_warn_eval( - ["nofile:1:3", "found quoted keyword \"foo\" but the quotes are not required"], + ["nofile:1:2", "found quoted keyword \"foo\" but the quotes are not required"], ~s/["foo": :bar]/ ) @@ -264,6 +264,20 @@ defmodule Kernel.WarningTest do purge(Sample) end + test "duplicate pattern" do + output = + capture_eval(""" + defmodule Sample do + var = quote(do: x) + def hello(unquote(var) = unquote(var)), do: unquote(var) + end + """) + + assert output =~ "this pattern is matched against itself inside a match: x = x" + after + purge(Sample) + end + test "unused compiler variable" do output = capture_eval(""" diff --git a/lib/elixir/test/elixir/kernel_test.exs b/lib/elixir/test/elixir/kernel_test.exs index bae0550e3b9..4a458e9fd17 100644 --- a/lib/elixir/test/elixir/kernel_test.exs +++ b/lib/elixir/test/elixir/kernel_test.exs @@ -205,6 +205,10 @@ defmodule KernelTest do defmodule User do assert is_map(defstruct name: "john") + # Ensure we keep the line information around. + # It is important for debugging tools, ExDoc, etc. + {:v1, :def, anno, _clauses} = Module.get_definition(__MODULE__, {:__struct__, 1}) + anno[:line] == __ENV__.line - 4 end test "struct/1 and struct/2" do @@ -1136,6 +1140,8 @@ defmodule KernelTest do test "pop_in/1" do users = %{"john" => %{age: 27}, "meg" => %{age: 23}} + assert pop_in(users["john"]) == {%{age: 27}, %{"meg" => %{age: 23}}} + assert pop_in(users["john"][:age]) == {27, %{"john" => %{}, "meg" => %{age: 23}}} assert pop_in(users["john"][:name]) == {nil, %{"john" => %{age: 27}, "meg" => %{age: 23}}} assert pop_in(users["bob"][:age]) == {nil, %{"john" => %{age: 27}, "meg" => %{age: 23}}} diff --git a/lib/elixir/test/elixir/map_test.exs b/lib/elixir/test/elixir/map_test.exs index 553d1956f48..a2fdcbb01d8 100644 --- a/lib/elixir/test/elixir/map_test.exs +++ b/lib/elixir/test/elixir/map_test.exs @@ -395,6 +395,15 @@ defmodule MapTest do assert quoted == {:%, [], [User, {:%{}, [], [{:foo, 1}]}]} end + test "structs with bitstring defaults" do + defmodule WithBitstring do + defstruct bitstring: <<255, 127::7>> + end + + info = Macro.struct_info!(WithBitstring, __ENV__) + assert info == [%{default: <<255, 127::7>>, field: :bitstring}] + end + test "defstruct can only be used once in a module" do message = "defstruct has already been called for TestMod, " <> diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index b5100b01b6b..e63ed883735 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -101,6 +101,46 @@ defmodule Module.Types.DescrTest do assert union(difference(list(term()), list(integer())), list(integer())) |> equal?(list(term())) end + + test "optimizations" do + # The tests are checking the actual implementation, not the semantics. + # This is why we are using structural comparisons. + # It's fine to remove these if the implementation changes, but breaking + # these might have an important impact on compile times. + + # Optimization one: same tags, all but one key are structurally equal + assert union( + open_map(a: float(), b: atom()), + open_map(a: integer(), b: atom()) + ) == open_map(a: union(float(), integer()), b: atom()) + + assert union( + closed_map(a: float(), b: atom()), + closed_map(a: integer(), b: atom()) + ) == closed_map(a: union(float(), integer()), b: atom()) + + # Optimization two: we can tell that one map is a trivial subtype of the other: + + assert union( + closed_map(a: term(), b: term()), + closed_map(a: float(), b: binary()) + ) == closed_map(a: term(), b: term()) + + assert union( + open_map(a: term()), + closed_map(a: float(), b: binary()) + ) == open_map(a: term()) + + assert union( + closed_map(a: float(), b: binary()), + open_map(a: term()) + ) == open_map(a: term()) + + assert union( + closed_map(a: term(), b: tuple([term(), term()])), + closed_map(a: float(), b: tuple([atom(), binary()])) + ) == closed_map(a: term(), b: tuple([term(), term()])) + end end describe "intersection" do @@ -163,6 +203,9 @@ defmodule Module.Types.DescrTest do assert intersection(closed_map(a: integer()), open_map(b: not_set())) == closed_map(a: integer()) + assert intersection(closed_map(a: integer()), open_map(b: if_set(integer()))) == + closed_map(a: integer()) + assert equal?( intersection(closed_map(a: integer()), closed_map(a: if_set(integer()))), closed_map(a: integer()) @@ -1184,7 +1227,7 @@ defmodule Module.Types.DescrTest do test "boolean" do assert boolean() |> to_quoted_string() == "boolean()" - assert atom([true, false, :a]) |> to_quoted_string() == "boolean() or :a" + assert atom([true, false, :a]) |> to_quoted_string() == ":a or boolean()" assert atom([true, :a]) |> to_quoted_string() == ":a or true" assert difference(atom(), boolean()) |> to_quoted_string() == "atom() and not boolean()" end @@ -1199,7 +1242,7 @@ defmodule Module.Types.DescrTest do assert intersection(atom(), dynamic()) |> to_quoted_string() == "dynamic(atom())" assert union(atom([:foo, :bar]), dynamic()) |> to_quoted_string() == - "dynamic() or (:bar or :foo)" + "dynamic() or :bar or :foo" assert intersection(dynamic(), closed_map(a: integer())) |> to_quoted_string() == "dynamic(%{a: integer()})" @@ -1256,7 +1299,7 @@ defmodule Module.Types.DescrTest do assert open_tuple([integer(), atom()]) |> to_quoted_string() == "{integer(), atom(), ...}" assert union(tuple([integer(), atom()]), open_tuple([atom()])) |> to_quoted_string() == - "{integer(), atom()} or {atom(), ...}" + "{atom(), ...} or {integer(), atom()}" assert difference(tuple([integer(), atom()]), open_tuple([atom()])) |> to_quoted_string() == "{integer(), atom()}" @@ -1268,6 +1311,96 @@ defmodule Module.Types.DescrTest do # assert difference(tuple([number(), term()]), tuple([integer(), atom()])) # |> to_quoted_string() == # "{float(), term()} or {number(), term() and not atom()}" + + assert union(tuple([integer(), atom()]), tuple([integer(), atom()])) |> to_quoted_string() == + "{integer(), atom()}" + + assert union(tuple([integer(), atom()]), tuple([float(), atom()])) |> to_quoted_string() == + "{float() or integer(), atom()}" + + assert union(tuple([integer(), atom()]), tuple([float(), atom()])) + |> union(tuple([pid(), pid(), port()])) + |> union(tuple([pid(), pid(), atom()])) + |> to_quoted_string() == + "{float() or integer(), atom()} or {pid(), pid(), atom() or port()}" + + assert union(open_tuple([integer()]), open_tuple([float()])) |> to_quoted_string() == + "{float() or integer(), ...}" + + # {:ok, {term(), integer()}} or {:ok, {term(), float()}} or {:exit, :kill} or {:exit, :timeout} + assert tuple([atom([:ok]), tuple([term(), empty_list()])]) + |> union(tuple([atom([:ok]), tuple([term(), open_map()])])) + |> union(tuple([atom([:exit]), atom([:kill])])) + |> union(tuple([atom([:exit]), atom([:timeout])])) + |> to_quoted_string() == + "{:exit, :kill or :timeout} or {:ok, {term(), %{...} or empty_list()}}" + + # Detection of duplicates + assert tuple([atom([:ok]), term()]) + |> union(tuple([atom([:ok]), term()])) + |> to_quoted_string() == "{:ok, term()}" + + assert tuple([closed_map(a: integer(), b: atom()), open_map()]) + |> union(tuple([closed_map(a: integer(), b: atom()), open_map()])) + |> to_quoted_string() == + "{%{a: integer(), b: atom()}, %{...}}" + + # Nested fusion + assert tuple([closed_map(a: integer(), b: atom()), open_map()]) + |> union(tuple([closed_map(a: float(), b: atom()), open_map()])) + |> to_quoted_string() == + "{%{a: float() or integer(), b: atom()}, %{...}}" + + # Complex simplification of map/tuple combinations. Initial type is: + # ``` + # dynamic( + # :error or + # ({%Decimal{coef: :inf, exp: integer(), sign: integer()}, binary()} or + # {%Decimal{coef: :NaN, exp: integer(), sign: integer()}, binary()} or + # {%Decimal{coef: integer(), exp: integer(), sign: integer()}, term()} or + # {%Decimal{coef: :inf, exp: integer(), sign: integer()} or + # %Decimal{coef: :NaN, exp: integer(), sign: integer()} or + # %Decimal{coef: integer(), exp: integer(), sign: integer()}, term()}) + # ) + # ``` + decimal_inf = + closed_map( + __struct__: atom([Decimal]), + coef: atom([:inf]), + exp: integer(), + sign: integer() + ) + + decimal_nan = + closed_map( + __struct__: atom([Decimal]), + coef: atom([:NaN]), + exp: integer(), + sign: integer() + ) + + decimal_int = + closed_map(__struct__: atom([Decimal]), coef: integer(), exp: integer(), sign: integer()) + + assert atom([:error]) + |> union( + tuple([decimal_inf, binary()]) + |> union( + tuple([decimal_nan, binary()]) + |> union( + tuple([decimal_int, term()]) + |> union(tuple([union(decimal_inf, union(decimal_nan, decimal_int)), term()])) + ) + ) + ) + |> dynamic() + |> to_quoted_string() == + """ + dynamic( + :error or {%Decimal{coef: :NaN or :inf, exp: integer(), sign: integer()}, binary()} or + {%Decimal{coef: :NaN or :inf or integer(), exp: integer(), sign: integer()}, term()} + )\ + """ end test "map" do @@ -1311,6 +1444,50 @@ defmodule Module.Types.DescrTest do assert difference(open_map(a: number(), b: atom()), open_map(a: integer())) |> to_quoted_string() == "%{..., a: float(), b: atom()}" + # Basic map fusion + assert union(closed_map(a: integer()), closed_map(a: integer())) |> to_quoted_string() == + "%{a: integer()}" + + assert union(closed_map(a: integer()), closed_map(a: float())) |> to_quoted_string() == + "%{a: float() or integer()}" + + # Nested fusion + assert union(closed_map(a: integer(), b: atom()), closed_map(a: float(), b: atom())) + |> union(closed_map(x: pid(), y: pid(), z: port())) + |> union(closed_map(x: pid(), y: pid(), z: atom())) + |> to_quoted_string() == + "%{a: float() or integer(), b: atom()} or %{x: pid(), y: pid(), z: atom() or port()}" + + # Open map fusion + assert union(open_map(a: integer()), open_map(a: float())) |> to_quoted_string() == + "%{..., a: float() or integer()}" + + # Fusing complex nested maps with unions + assert closed_map(status: atom([:ok]), data: closed_map(value: term(), count: empty_list())) + |> union( + closed_map(status: atom([:ok]), data: closed_map(value: term(), count: open_map())) + ) + |> union(closed_map(status: atom([:error]), reason: atom([:timeout]))) + |> union(closed_map(status: atom([:error]), reason: atom([:crash]))) + |> to_quoted_string() == + "%{data: %{count: %{...} or empty_list(), value: term()}, status: :ok} or\n %{reason: :crash or :timeout, status: :error}" + + # Difference and union tests + assert closed_map(status: atom([:ok]), value: term()) + |> difference(closed_map(status: atom([:ok]), value: float())) + |> union( + closed_map(status: atom([:ok]), value: term()) + |> difference(closed_map(status: atom([:ok]), value: integer())) + ) + |> to_quoted_string() == + "%{status: :ok, value: term()}" + + # Nested map fusion + assert closed_map(data: closed_map(x: integer(), y: atom()), meta: open_map()) + |> union(closed_map(data: closed_map(x: float(), y: atom()), meta: open_map())) + |> to_quoted_string() == + "%{data: %{x: float() or integer(), y: atom()}, meta: %{...}}" + # Test complex combinations assert intersection(open_map(a: number(), b: atom()), open_map(a: integer(), c: boolean())) |> union(difference(open_map(x: atom()), open_map(x: boolean()))) diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 7e5499cde56..49c4c8bf932 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -243,6 +243,35 @@ defmodule Module.Types.ExprTest do """ end + test "calling a function with invalid arguments on variables" do + assert typeerror!( + ( + x = List + x.to_tuple(123) + ) + ) + |> strip_ansi() == + ~l""" + incompatible types given to List.to_tuple/1: + + x.to_tuple(123) + + given types: + + integer() + + but expected one of: + + list(term()) + + where "x" was given the type: + + # type: List + # from: types_test.ex:LINE-5 + x = List + """ + end + test "capture a function with non atoms" do assert typeerror!([<>], &x.foo_bar/2) == ~l""" @@ -798,9 +827,9 @@ defmodule Module.Types.ExprTest do x.foo_bar - where "x" was given the type: + the given type does not have the given key: - # type: dynamic(%URI{ + dynamic(%URI{ authority: term(), fragment: term(), host: term(), @@ -810,6 +839,10 @@ defmodule Module.Types.ExprTest do scheme: term(), userinfo: term() }) + + where "x" was given the type: + + # type: dynamic(%URI{}) # from: types_test.ex:LINE-4:43 x = %URI{} """ @@ -827,7 +860,11 @@ defmodule Module.Types.ExprTest do test "in dynamic mode" do assert typedyn!([x = 123, y = 456.0], x < y) == dynamic(boolean()) assert typedyn!([x = 123, y = 456.0], x == y) == dynamic(boolean()) - assert typedyn!(123 == 456) == boolean() + assert typedyn!([x = 123, y = 456], x == y) == dynamic(boolean()) + end + + test "using literals" do + assert typecheck!(:foo == :bar) == boolean() end test "min/max" do @@ -845,6 +882,10 @@ defmodule Module.Types.ExprTest do min(x, y) + given types: + + min(dynamic(:foo), integer()) + where "x" was given the type: # type: dynamic(:foo) @@ -868,6 +909,10 @@ defmodule Module.Types.ExprTest do x === y + given types: + + integer() === float() + where "x" was given the type: # type: integer() @@ -893,6 +938,10 @@ defmodule Module.Types.ExprTest do mod.<=(x, y) + given types: + + dynamic(:foo) <= dynamic(%Point{}) + where "mod" was given the type: # type: dynamic(Kernel) @@ -907,7 +956,7 @@ defmodule Module.Types.ExprTest do where "y" was given the type: - # type: dynamic(%Point{x: term(), y: term(), z: term()}) + # type: dynamic(%Point{}) # from: types_test.ex:LINE-2 y = %Point{} @@ -1015,6 +1064,15 @@ defmodule Module.Types.ExprTest do end describe "case" do + test "does not type check literals" do + assert typecheck!( + case :dev do + :dev -> :ok + :prod -> :error + end + ) == atom([:ok, :error]) + end + test "returns unions of all clauses" do assert typecheck!( [x], @@ -1070,7 +1128,7 @@ defmodule Module.Types.ExprTest do end describe "conditionals" do - test "if does not report on literal atoms" do + test "if does not report on literals" do assert typecheck!( if true do :ok @@ -1078,15 +1136,15 @@ defmodule Module.Types.ExprTest do ) == atom([:ok, nil]) end - test "and does not report on literal atoms" do + test "and does not report on literals" do assert typecheck!(false and true) == boolean() end - test "and reports on non-atom literals" do - assert typeerror!(1 and true) == ~l""" + test "and reports violations" do + assert typeerror!([x = 123], x and true) =~ """ the following conditional expression will always evaluate to integer(): - 1 + x """ end end @@ -1224,16 +1282,18 @@ defmodule Module.Types.ExprTest do e end ) == - union( - closed_map( - __struct__: atom([ArgumentError]), - __exception__: atom([true]), - message: term() - ), - closed_map( - __struct__: atom([RuntimeError]), - __exception__: atom([true]), - message: term() + dynamic( + union( + closed_map( + __struct__: atom([ArgumentError]), + __exception__: atom([true]), + message: term() + ), + closed_map( + __struct__: atom([RuntimeError]), + __exception__: atom([true]), + message: term() + ) ) ) end @@ -1382,6 +1442,16 @@ defmodule Module.Types.ExprTest do assert typewarn!(:string.__info__(:functions)) == {dynamic(), ":string.__info__/1 is undefined or private"} + + assert typeerror!([x], x.__info__(:whatever)) |> strip_ansi() =~ """ + incompatible types given to __info__/1: + + x.__info__(:whatever) + + given types: + + :whatever + """ end test "behaviour_info/1" do diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index 3208cd2cbd0..5ca6f17bb34 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -36,7 +36,7 @@ defmodule Module.Types.IntegrationTest do """ } - modules = compile(files) + modules = compile_modules(files) assert [ {{:c, 0}, %{}}, @@ -195,6 +195,118 @@ defmodule Module.Types.IntegrationTest do assert_warnings(files, warnings) end + + test "unused generated private clauses" do + files = %{ + "a.ex" => """ + defmodule A do + use B + def public(x), do: private(List.to_tuple(x)) + end + """, + "b.ex" => """ + defmodule B do + defmacro __using__(_) do + quote generated: true do + defp private({:ok, ok}), do: ok + defp private(:error), do: :error + end + end + end + """ + } + + assert_no_warnings(files) + end + + test "unused overridable private clauses" do + files = %{ + "a.ex" => """ + defmodule A do + use B + def public(x), do: private(x) + defp private(x), do: super(List.to_tuple(x)) + end + """, + "b.ex" => """ + defmodule B do + defmacro __using__(_) do + quote do + defp private({:ok, ok}), do: ok + defp private(:error), do: :error + defoverridable private: 1 + end + end + end + """ + } + + assert_no_warnings(files) + end + + test "incompatible default argument" do + files = %{ + "a.ex" => """ + defmodule A do + def ok(x = :ok \\\\ nil) do + x + end + end + """ + } + + warnings = [ + ~S""" + warning: incompatible types given as default arguments to ok/1: + + -nil- + + but expected one of: + + dynamic(:ok) + + typing violation found at: + │ + 2 │ def ok(x = :ok \\ nil) do + │ ~ + │ + └─ a.ex:2:18: A.ok/0 + """ + ] + + assert_warnings(files, warnings) + end + + test "returns diagnostics with source and file" do + files = %{ + "a.ex" => """ + defmodule A do + @file "generated.ex" + def fun(arg) do + :ok = List.to_tuple(arg) + end + end + """ + } + + {_modules, warnings} = with_compile_warnings(files) + + assert [ + %{ + message: "the following pattern will never match" <> _, + file: file, + source: source + } + ] = warnings.runtime_warnings + + assert String.ends_with?(source, "a.ex") + assert Path.type(source) == :absolute + assert String.ends_with?(file, "generated.ex") + assert Path.type(file) == :absolute + after + :code.delete(A) + :code.purge(A) + end end describe "undefined warnings" do @@ -932,26 +1044,39 @@ defmodule Module.Types.IntegrationTest do defp capture_compile_warnings(files) do in_tmp(fn -> paths = generate_files(files) - capture_io(:stderr, fn -> compile_files(paths) end) + capture_io(:stderr, fn -> compile_to_path(paths) end) end) end - defp compile(files) do + defp with_compile_warnings(files) do in_tmp(fn -> paths = generate_files(files) - compile_files(paths) + with_io(:stderr, fn -> compile_to_path(paths) end) |> elem(0) end) end - defp compile_files(paths) do - {:ok, modules, _warnings} = Kernel.ParallelCompiler.compile_to_path(paths, ".") + defp compile_modules(files) do + in_tmp(fn -> + paths = generate_files(files) + {modules, _warnings} = compile_to_path(paths) + + Map.new(modules, fn module -> + {^module, binary, _filename} = :code.get_object_code(module) + {module, binary} + end) + end) + end - Map.new(modules, fn module -> - {^module, binary, _filename} = :code.get_object_code(module) + defp compile_to_path(paths) do + {:ok, modules, warnings} = + Kernel.ParallelCompiler.compile_to_path(paths, ".", return_diagnostics: true) + + for module <- modules do :code.delete(module) :code.purge(module) - {module, binary} - end) + end + + {modules, warnings} end defp generate_files(files) do diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs index 490c725a4b8..8e940d2ea06 100644 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -166,9 +166,13 @@ defmodule Module.Types.PatternTest do x.foo_bar + the given type does not have the given key: + + dynamic(%Point{x: term(), y: term(), z: term()}) + where "x" was given the type: - # type: dynamic(%Point{x: term(), y: term(), z: term()}) + # type: dynamic(%Point{}) # from: types_test.ex:LINE-1 x = %Point{} """ @@ -247,6 +251,10 @@ defmodule Module.Types.PatternTest do assert typecheck!([<>], x) == integer() end + test "nested" do + assert typecheck!([<<0, <>::binary>>], x) == binary() + end + test "error" do assert typeerror!([<>], x) == ~l""" incompatible types assigned to "x": diff --git a/lib/elixir/test/elixir/option_parser_test.exs b/lib/elixir/test/elixir/option_parser_test.exs index 39df43bdcb0..f2cab66a0d1 100644 --- a/lib/elixir/test/elixir/option_parser_test.exs +++ b/lib/elixir/test/elixir/option_parser_test.exs @@ -428,10 +428,9 @@ end defmodule OptionsParserDeprecationsTest do use ExUnit.Case, async: true - @warning ~r[not passing the :switches or :strict option to OptionParser is deprecated] - def assert_deprecated(fun) do - assert ExUnit.CaptureIO.capture_io(:stderr, fun) =~ @warning + assert ExUnit.CaptureIO.capture_io(:stderr, fun) =~ + "not passing the :switches or :strict option to OptionParser is deprecated" end test "parses boolean option" do diff --git a/lib/elixir/test/elixir/protocol_test.exs b/lib/elixir/test/elixir/protocol_test.exs index 210f41b7088..91d20a76b98 100644 --- a/lib/elixir/test/elixir/protocol_test.exs +++ b/lib/elixir/test/elixir/protocol_test.exs @@ -106,15 +106,18 @@ defmodule ProtocolTest do assert Sample.impl_for(%ImplStruct{}) == Sample.ProtocolTest.ImplStruct assert Sample.impl_for(%ImplStructExplicitFor{}) == Sample.ProtocolTest.ImplStructExplicitFor assert Sample.impl_for(%NoImplStruct{}) == nil + assert is_nil(Sample.impl_for(%{__struct__: nil})) end test "protocol implementation with Any and struct fallbacks" do assert WithAny.impl_for(%NoImplStruct{}) == WithAny.Any - # Derived - assert WithAny.impl_for(%ImplStruct{}) == ProtocolTest.WithAny.ProtocolTest.ImplStruct + assert WithAny.impl_for(%{__struct__: nil}) == WithAny.Any assert WithAny.impl_for(%{__struct__: "foo"}) == WithAny.Map assert WithAny.impl_for(%{}) == WithAny.Map assert WithAny.impl_for(self()) == WithAny.Any + + # Derived + assert WithAny.impl_for(%ImplStruct{}) == ProtocolTest.WithAny.ProtocolTest.ImplStruct end test "protocol not implemented" do diff --git a/lib/elixir/test/elixir/range_test.exs b/lib/elixir/test/elixir/range_test.exs index 05666d34c4d..a974b3c008e 100644 --- a/lib/elixir/test/elixir/range_test.exs +++ b/lib/elixir/test/elixir/range_test.exs @@ -35,7 +35,7 @@ defmodule RangeTest do assert ExUnit.CaptureIO.capture_io(:stderr, fn -> assert Range.new(3, 1) == 3..1//-1 - end) =~ "has a default step of -1" + end) =~ "default to a step of -1" end test "fields" do diff --git a/lib/elixir/test/elixir/regex_test.exs b/lib/elixir/test/elixir/regex_test.exs index 9b72e967dbe..64f9c91d6d8 100644 --- a/lib/elixir/test/elixir/regex_test.exs +++ b/lib/elixir/test/elixir/regex_test.exs @@ -3,41 +3,25 @@ Code.require_file("test_helper.exs", __DIR__) defmodule RegexTest do use ExUnit.Case, async: true - @re_21_3_little %Regex{ - re_pattern: - {:re_pattern, 1, 0, 0, - <<69, 82, 67, 80, 94, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, - 255, 99, 0, 0, 0, 0, 0, 1, 0, 0, 0, 64, 0, 6, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 102, 111, 111, 0, 131, 0, 20, 29, 99, 133, - 0, 7, 0, 1, 29, 100, 119, 0, 5, 29, 101, 120, 0, 12, 120, 0, 20, 0>>}, - re_version: {"8.42 2018-03-20", :little}, - source: "c(?d|e)" - } - - @re_21_3_big %Regex{ - re_pattern: - {:re_pattern, 1, 0, 0, - <<80, 67, 82, 69, 0, 0, 0, 86, 0, 0, 0, 0, 0, 0, 0, 17, 255, 255, 255, 255, 255, 255, 255, - 255, 0, 99, 0, 0, 0, 0, 0, 1, 0, 0, 0, 56, 0, 6, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 102, 111, 111, 0, 131, 0, 20, 29, 99, 133, 0, 7, 0, 1, 29, 100, 119, - 0, 5, 29, 101, 120, 0, 12, 120, 0, 20, 0>>}, - re_version: {"8.42 2018-03-20", :big}, - source: "c(?d|e)" - } - - @re_19_3_little %Regex{ - re_pattern: - {:re_pattern, 1, 0, 0, - <<69, 82, 67, 80, 94, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, - 255, 99, 0, 0, 0, 0, 0, 1, 0, 0, 0, 64, 0, 6, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 102, 111, 111, 0, 125, 0, 20, 29, 99, 127, - 0, 7, 0, 1, 29, 100, 113, 0, 5, 29, 101, 114, 0, 12, 114, 0, 20, 0>>}, - re_version: {"8.33 2013-05-29", :little}, - source: "c(?d|e)" - } - doctest Regex + if System.otp_release() >= "28" do + test "module attribute" do + defmodule ModAttr do + @regex ~r/example/ + def regex, do: @regex + + @bare_regex :erlang.term_to_binary(@regex) + def bare_regex, do: :erlang.binary_to_term(@bare_regex) + + # We don't rewrite outside of functions + assert @regex.re_pattern == :erlang.binary_to_term(@bare_regex).re_pattern + end + + assert ModAttr.regex().re_pattern != ModAttr.bare_regex().re_pattern + end + end + test "multiline" do refute Regex.match?(~r/^b$/, "a\nb\nc") assert Regex.match?(~r/^b$/m, "a\nb\nc") @@ -68,16 +52,9 @@ defmodule RegexTest do test "literal source" do assert Regex.source(Regex.compile!("foo")) == "foo" assert Regex.source(~r"foo") == "foo" - assert Regex.re_pattern(Regex.compile!("foo")) == Regex.re_pattern(~r"foo") assert Regex.source(Regex.compile!("\a\b\d\e\f\n\r\s\t\v")) == "\a\b\d\e\f\n\r\s\t\v" assert Regex.source(~r<\a\b\d\e\f\n\r\s\t\v>) == "\\a\\b\\d\\e\\f\\n\\r\\s\\t\\v" - - assert Regex.re_pattern(Regex.compile!("\a\b\d\e\f\n\r\s\t\v")) == - Regex.re_pattern(~r"\x07\x08\x7F\x1B\x0C\x0A\x0D\x20\x09\x0B") - - assert Regex.re_pattern(Regex.compile!("\\a\\b\\d\e\f\\n\\r\\s\\t\\v")) == - Regex.re_pattern(~r"\a\b\d\e\f\n\r\s\t\v") end test "Unicode" do @@ -116,16 +93,6 @@ defmodule RegexTest do end end - test "recompile/1" do - new_regex = ~r/foo/ - {:ok, %Regex{}} = Regex.recompile(new_regex) - assert %Regex{} = Regex.recompile!(new_regex) - - old_regex = Map.delete(~r/foo/, :re_version) - {:ok, %Regex{}} = Regex.recompile(old_regex) - assert %Regex{} = Regex.recompile!(old_regex) - end - test "opts/1" do assert Regex.opts(Regex.compile!("foo", "i")) == [:caseless] assert Regex.opts(Regex.compile!("foo", [:ucp])) == [:ucp] @@ -179,16 +146,6 @@ defmodule RegexTest do assert Regex.run(~r"bar", "foobar", offset: 2, return: :index) == [{3, 3}] end - test "run/3 with regexes compiled in different systems" do - assert Regex.run(@re_21_3_little, "abcd abce", capture: :all_names) == ["d"] - assert Regex.run(@re_21_3_big, "abcd abce", capture: :all_names) == ["d"] - assert Regex.run(@re_19_3_little, "abcd abce", capture: :all_names) == ["d"] - end - - test "run/3 with regexes with options compiled in different systems" do - assert Regex.run(%{~r/foo/i | re_version: "bad version"}, "FOO") == ["FOO"] - end - test "scan/2" do assert Regex.scan(~r"c(d|e)", "abcd abce") == [["cd", "d"], ["ce", "e"]] assert Regex.scan(~r"c(?:d|e)", "abcd abce") == [["cd"], ["ce"]] @@ -207,16 +164,6 @@ defmodule RegexTest do assert Regex.scan(~r"^foo", "foobar", offset: 1) == [] end - test "scan/2 with regexes compiled in different systems" do - assert Regex.scan(@re_21_3_little, "abcd abce", capture: :all_names) == [["d"], ["e"]] - assert Regex.scan(@re_21_3_big, "abcd abce", capture: :all_names) == [["d"], ["e"]] - assert Regex.scan(@re_19_3_little, "abcd abce", capture: :all_names) == [["d"], ["e"]] - end - - test "scan/2 with regexes with options compiled in different systems" do - assert Regex.scan(%{~r/foo/i | re_version: "bad version"}, "FOO") == [["FOO"]] - end - test "split/2,3" do assert Regex.split(~r",", "") == [""] assert Regex.split(~r",", "", trim: true) == [] diff --git a/lib/elixir/test/elixir/typespec_test.exs b/lib/elixir/test/elixir/typespec_test.exs index 21b38f0d8aa..0658f2e0f28 100644 --- a/lib/elixir/test/elixir/typespec_test.exs +++ b/lib/elixir/test/elixir/typespec_test.exs @@ -75,6 +75,14 @@ defmodule TypespecTest do @type my_type :: %URI.t(){} end end + + assert_raise Kernel.TypespecError, + ~r"unexpected expression in typespec: t\.Foo", + fn -> + test_module do + @type my_type :: t.Foo + end + end end test "invalid function specification" do @@ -120,7 +128,7 @@ defmodule TypespecTest do test "redefined type" do assert_raise Kernel.TypespecError, - ~r"type foo/0 is already defined in .*test/elixir/typespec_test.exs:126", + ~r"type foo/0 is already defined in .*test/elixir/typespec_test.exs:134", fn -> test_module do @type foo :: atom @@ -129,7 +137,7 @@ defmodule TypespecTest do end assert_raise Kernel.TypespecError, - ~r"type foo/2 is already defined in .*test/elixir/typespec_test.exs:136", + ~r"type foo/2 is already defined in .*test/elixir/typespec_test.exs:144", fn -> test_module do @type foo :: atom @@ -139,7 +147,7 @@ defmodule TypespecTest do end assert_raise Kernel.TypespecError, - ~r"type foo/0 is already defined in .*test/elixir/typespec_test.exs:145", + ~r"type foo/0 is already defined in .*test/elixir/typespec_test.exs:153", fn -> test_module do @type foo :: atom @@ -385,6 +393,7 @@ defmodule TypespecTest do @type size :: <<_::3>> @type unit :: <<_::_*8>> @type size_and_unit :: <<_::3, _::_*8>> + @type size_prod_unit :: <<_::3*8>> end assert [ @@ -393,17 +402,15 @@ defmodule TypespecTest do type: {:size, {:type, _, :binary, [{:integer, _, 3}, {:integer, _, 0}]}, []}, type: {:size_and_unit, {:type, _, :binary, [{:integer, _, 3}, {:integer, _, 8}]}, []}, + type: + {:size_prod_unit, + {:type, _, :binary, + [{:op, _, :*, {:integer, _, 3}, {:integer, _, 8}}, {:integer, _, 0}]}, []}, type: {:unit, {:type, _, :binary, [{:integer, _, 0}, {:integer, _, 8}]}, []} ] = types(bytecode) end test "@type with invalid binary spec" do - assert_raise Kernel.TypespecError, ~r"invalid binary specification", fn -> - test_module do - @type my_type :: <<_::3*8>> - end - end - assert_raise Kernel.TypespecError, ~r"invalid binary specification", fn -> test_module do @type my_type :: <<_::atom()>> @@ -710,7 +717,8 @@ defmodule TypespecTest do @type my_type :: (... -> any) end - assert [type: {:my_type, {:type, _, :fun, []}, []}] = types(bytecode) + assert [type: {:my_type, {:type, _, :fun, [{:type, _, :any}, {:type, _, :any, []}]}, []}] = + types(bytecode) end test "@type with a fun with multiple arguments and return type" do @@ -840,6 +848,19 @@ defmodule TypespecTest do assert [{:atom, _, Keyword}, {:atom, _, :t}, [{:var, _, :value}]] = kw_with_value_args end + test "@type with macro in alias" do + bytecode = + test_module do + defmacro module() do + quote do: __MODULE__ + end + + @type my_type :: module().Foo + end + + assert [type: {:my_type, {:atom, _, TypespecTest.TypespecSample.Foo}, []}] = types(bytecode) + end + test "@type with a reserved signature" do assert_raise Kernel.TypespecError, ~r"type required\/1 is a reserved type and it cannot be defined", @@ -1195,6 +1216,7 @@ defmodule TypespecTest do quote(do: @type(binary_type1() :: <<_::_*8>>)), quote(do: @type(binary_type2() :: <<_::3>>)), quote(do: @type(binary_type3() :: <<_::3, _::_*8>>)), + quote(do: @type(binary_type4() :: <<_::3*8>>)), quote(do: @type(tuple_type() :: {integer()})), quote(do: @type(ftype() :: (-> any()) | (-> integer()) | (integer() -> integer()))), quote(do: @type(cl() :: charlist())), @@ -1520,9 +1542,6 @@ defmodule TypespecTest do assert ast_string == "@type literal_struct_all_fields_key_type() :: %TypespecTest.SomeStruct{key: integer()}" - {:built_in_fun, _, _} -> - assert ast_string == "@type built_in_fun() :: (... -> any())" - {:built_in_nonempty_list, _, _} -> assert ast_string == "@type built_in_nonempty_list() :: [...]" diff --git a/lib/ex_unit/lib/ex_unit/assertions.ex b/lib/ex_unit/lib/ex_unit/assertions.ex index ba119f1057b..22b1a790e57 100644 --- a/lib/ex_unit/lib/ex_unit/assertions.ex +++ b/lib/ex_unit/lib/ex_unit/assertions.ex @@ -786,7 +786,7 @@ defmodule ExUnit.Assertions do ## Examples assert_raise ArithmeticError, "bad argument in arithmetic expression", fn -> - 1 + "test" + 1 / 0 end assert_raise RuntimeError, ~r/^today's lucky number is 0\.\d+!$/, fn -> diff --git a/lib/ex_unit/lib/ex_unit/case.ex b/lib/ex_unit/lib/ex_unit/case.ex index 9810df94e9e..519ed08a1ba 100644 --- a/lib/ex_unit/lib/ex_unit/case.ex +++ b/lib/ex_unit/lib/ex_unit/case.ex @@ -315,6 +315,11 @@ defmodule ExUnit.Case do end end + @keys [:async, :group, :parameterize, :register] + + @doc false + def __keys__(opts), do: Keyword.take(opts, @keys) + @doc false def __register__(module, opts) do if not Keyword.keyword?(opts) do @@ -324,7 +329,7 @@ defmodule ExUnit.Case do end {register?, opts} = Keyword.pop(opts, :register, true) - {next_opts, opts} = Keyword.split(opts, [:async, :group, :parameterize]) + {next_opts, opts} = Keyword.split(opts, @keys) if opts != [] do IO.warn("unknown options given to ExUnit.Case: #{inspect(opts)}") @@ -561,6 +566,10 @@ defmodule ExUnit.Case do group = Keyword.get(opts, :group, nil) parameterize = Keyword.get(opts, :parameterize, nil) + if not is_boolean(async?) do + raise ArgumentError, ":async must be a boolean, got: #{inspect(async?)}" + end + if not (parameterize == nil or (is_list(parameterize) and Enum.all?(parameterize, &is_map/1))) do raise ArgumentError, ":parameterize must be a list of maps, got: #{inspect(parameterize)}" end diff --git a/lib/ex_unit/lib/ex_unit/case_template.ex b/lib/ex_unit/lib/ex_unit/case_template.ex index 0e4023a8ff0..2449b7c1998 100644 --- a/lib/ex_unit/lib/ex_unit/case_template.ex +++ b/lib/ex_unit/lib/ex_unit/case_template.ex @@ -83,7 +83,7 @@ defmodule ExUnit.CaseTemplate do # We inject this code in the module that calls "use MyTemplate". def __proxy__(module, opts) do quote do - use ExUnit.Case, unquote(opts) + use ExUnit.Case, ExUnit.Case.__keys__(unquote(opts)) setup_all context do unquote(module).__ex_unit__(:setup_all, context) diff --git a/lib/ex_unit/lib/ex_unit/diff.ex b/lib/ex_unit/lib/ex_unit/diff.ex index 03c75c96e45..93fe0113727 100644 --- a/lib/ex_unit/lib/ex_unit/diff.ex +++ b/lib/ex_unit/lib/ex_unit/diff.ex @@ -878,7 +878,8 @@ defmodule ExUnit.Diff do end defp rebuild_split_strings(%{contents: contents, delimiter: delimiter}, right) do - %{contents: contents ++ [{false, right}], delimiter: delimiter} + {new_right, diff} = extract_diff_meta(right) + %{contents: contents ++ [{diff, new_right}], delimiter: delimiter} end defp rebuild_concat_string(literal, nil, []) do @@ -1155,6 +1156,7 @@ defmodule ExUnit.Diff do else other |> Map.to_list() + |> Enum.map(&escape_pair/1) |> build_map_or_struct(struct) end end diff --git a/lib/ex_unit/lib/ex_unit/doc_test.ex b/lib/ex_unit/lib/ex_unit/doc_test.ex index dd354b2e4f8..0b1a04a7bfb 100644 --- a/lib/ex_unit/lib/ex_unit/doc_test.ex +++ b/lib/ex_unit/lib/ex_unit/doc_test.ex @@ -149,8 +149,6 @@ defmodule ExUnit.DocTest do suite run. """ - @opaque_type_regex ~r/#[\w\.]+ """ \nIf you are planning to assert on the result of an iex> expression \ diff --git a/lib/ex_unit/lib/ex_unit/runner.ex b/lib/ex_unit/lib/ex_unit/runner.ex index 89996230cca..d94ee86f70e 100644 --- a/lib/ex_unit/lib/ex_unit/runner.ex +++ b/lib/ex_unit/lib/ex_unit/runner.ex @@ -125,7 +125,7 @@ defmodule ExUnit.Runner do # Run all sync modules directly for pair <- sync_modules do - running = spawn_modules(config, [[pair]], false, %{}) + running = spawn_modules(config, [{nil, [pair]}], false, %{}) running != %{} and wait_until_available(config, running) end @@ -161,7 +161,7 @@ defmodule ExUnit.Runner do running end - defp spawn_modules(config, [[_ | _] = modules | groups], async?, running) do + defp spawn_modules(config, [{_group, [_ | _] = modules} | groups], async?, running) do if max_failures_reached?(config) do running else diff --git a/lib/ex_unit/lib/ex_unit/server.ex b/lib/ex_unit/lib/ex_unit/server.ex index 820e6c1ca0e..b991816c873 100644 --- a/lib/ex_unit/lib/ex_unit/server.ex +++ b/lib/ex_unit/lib/ex_unit/server.ex @@ -57,9 +57,10 @@ defmodule ExUnit.Server do state = %{ loaded: System.monotonic_time(), waiting: nil, - async_groups: %{}, + groups: %{}, + async_groups: [], async_modules: :queue.new(), - sync_modules: :queue.new() + sync_modules: [] } {:ok, state} @@ -72,31 +73,31 @@ defmodule ExUnit.Server do # Called once after all async modules have been sent and reverts the state. def handle_call(:take_sync_modules, _from, state) do - %{waiting: nil, loaded: :done, async_modules: async_modules} = state - 0 = :queue.len(async_modules) + %{waiting: nil, loaded: :done, async_groups: []} = state + true = :queue.is_empty(state.async_modules) - {:reply, :queue.to_list(state.sync_modules), - %{state | sync_modules: :queue.new(), loaded: System.monotonic_time()}} + {:reply, state.sync_modules, %{state | sync_modules: [], loaded: System.monotonic_time()}} end # Called by the runner when --repeat-until-failure is used. def handle_call({:restore_modules, async_modules, sync_modules}, _from, state) do - {async_modules, async_groups} = - Enum.map_reduce(async_modules, %{}, fn - {nil, [module]}, {modules, groups} -> - {[{:module, module} | modules], groups} + {async_modules, async_groups, groups} = + Enum.reduce(async_modules, {[], [], []}, fn + {nil, [module]}, {async_modules, async_groups, groups} -> + {[module | async_modules], async_groups, groups} - {group, group_modules}, {modules, groups} -> - {[{:group, group} | modules], Map.put(groups, group, group_modules)} + {group, group_modules}, {async_modules, async_groups, groups} -> + {async_modules, [group | async_groups], [{group, group_modules} | groups]} end) {:reply, :ok, %{ state | loaded: :done, + groups: Map.new(groups), async_groups: async_groups, async_modules: :queue.from_list(async_modules), - sync_modules: :queue.from_list(sync_modules) + sync_modules: sync_modules }} end @@ -108,22 +109,24 @@ defmodule ExUnit.Server do when is_integer(loaded) do state = if uniq? do - async_groups = - Map.new(state.async_groups, fn {group, modules} -> - {group, Enum.uniq(modules)} - end) - + groups = Map.new(state.groups, fn {group, modules} -> {group, Enum.uniq(modules)} end) + async_groups = state.async_groups |> Enum.uniq() |> Enum.reverse() async_modules = :queue.to_list(state.async_modules) |> Enum.uniq() |> :queue.from_list() - sync_modules = :queue.to_list(state.sync_modules) |> Enum.uniq() |> :queue.from_list() + sync_modules = state.sync_modules |> Enum.uniq() |> Enum.reverse() %{ state - | async_groups: async_groups, + | groups: groups, + async_groups: async_groups, async_modules: async_modules, sync_modules: sync_modules } else - state + %{ + state + | async_groups: Enum.reverse(state.async_groups), + sync_modules: Enum.reverse(state.sync_modules) + } end diff = System.convert_time_unit(System.monotonic_time() - loaded, :native, :microsecond) @@ -132,9 +135,7 @@ defmodule ExUnit.Server do def handle_call({:add, false = _async, _group, names}, _from, %{loaded: loaded} = state) when is_integer(loaded) do - state = - update_in(state.sync_modules, &Enum.reduce(names, &1, fn name, q -> :queue.in(name, q) end)) - + state = update_in(state.sync_modules, &Enum.reverse(names, &1)) {:reply, :ok, state} end @@ -143,7 +144,7 @@ defmodule ExUnit.Server do state = update_in( state.async_modules, - &Enum.reduce(names, &1, fn name, q -> :queue.in({:module, name}, q) end) + &Enum.reduce(names, &1, fn name, q -> :queue.in(name, q) end) ) {:reply, :ok, take_modules(state)} @@ -151,17 +152,16 @@ defmodule ExUnit.Server do def handle_call({:add, true = _async, group, names}, _from, %{loaded: loaded} = state) when is_integer(loaded) do - {async_groups, async_modules} = - case state.async_groups do - %{^group => entries} = async_groups -> - {%{async_groups | group => names ++ entries}, state.async_modules} + {groups, async_groups} = + case state.groups do + %{^group => entries} = groups -> + {%{groups | group => Enum.reverse(names, entries)}, state.async_groups} - %{} = async_groups -> - {Map.put(async_groups, group, names), :queue.in({:group, group}, state.async_modules)} + %{} = groups -> + {Map.put(groups, group, names), [group | state.async_groups]} end - {:reply, :ok, - take_modules(%{state | async_groups: async_groups, async_modules: async_modules})} + {:reply, :ok, take_modules(%{state | groups: groups, async_groups: async_groups})} end def handle_call({:add, _async?, _group, _names}, _from, state) do @@ -173,50 +173,42 @@ defmodule ExUnit.Server do end defp take_modules(%{waiting: {from, count}} = state) do - has_async_modules? = not :queue.is_empty(state.async_modules) - cond do - not has_async_modules? and state.loaded == :done -> + not :queue.is_empty(state.async_modules) -> + {reply, remaining_modules} = take_until(count, state.async_modules) + GenServer.reply(from, reply) + %{state | async_modules: remaining_modules, waiting: nil} + + state.async_groups != [] and state.loaded == :done -> + {groups, remaining_groups} = Enum.split(state.async_groups, count) + + {reply, groups} = + Enum.map_reduce(groups, state.groups, fn group, acc -> + {entries, acc} = Map.pop!(acc, group) + {{group, Enum.reverse(entries)}, acc} + end) + + GenServer.reply(from, reply) + %{state | groups: groups, async_groups: remaining_groups, waiting: nil} + + state.loaded == :done -> GenServer.reply(from, nil) %{state | waiting: nil} - not has_async_modules? -> - state - true -> - {async_modules, remaining_modules} = take_until(count, state.async_modules) - - {async_modules, remaining_groups} = - Enum.map_reduce(async_modules, state.async_groups, fn - {:module, module}, async_groups -> - {[module], async_groups} - - {:group, group}, async_groups -> - {group_modules, async_groups} = Map.pop!(async_groups, group) - {Enum.reverse(group_modules), async_groups} - end) - - GenServer.reply(from, async_modules) - - %{ - state - | async_groups: remaining_groups, - async_modules: remaining_modules, - waiting: nil - } + state end end - # :queue.split fails if the provided count is larger than the queue size; - # as we also want to return the values as a list later, we directly - # return {list, queue} instead of {queue, queue} + # :queue.split fails if the provided count is larger than the queue size. + # We also want to return the values as tuples of shape {group, [modules]}. defp take_until(n, queue), do: take_until(n, queue, []) defp take_until(0, queue, acc), do: {Enum.reverse(acc), queue} defp take_until(n, queue, acc) do case :queue.out(queue) do - {{:value, item}, queue} -> take_until(n - 1, queue, [item | acc]) + {{:value, item}, queue} -> take_until(n - 1, queue, [{nil, [item]} | acc]) {:empty, queue} -> {Enum.reverse(acc), queue} end end diff --git a/lib/ex_unit/test/ex_unit/assertions_test.exs b/lib/ex_unit/test/ex_unit/assertions_test.exs index e2094f44535..8a0d7107993 100644 --- a/lib/ex_unit/test/ex_unit/assertions_test.exs +++ b/lib/ex_unit/test/ex_unit/assertions_test.exs @@ -707,7 +707,7 @@ defmodule ExUnit.AssertionsTest do rescue error in [ExUnit.AssertionError] -> "foo" = error.left - ~r{a} = error.right + %Regex{} = error.right end end @@ -722,7 +722,7 @@ defmodule ExUnit.AssertionsTest do rescue error in [ExUnit.AssertionError] -> "foo" = error.left - ~r"o" = error.right + %Regex{} = error.right end end diff --git a/lib/ex_unit/test/ex_unit/case_template_test.exs b/lib/ex_unit/test/ex_unit/case_template_test.exs index b5265a8ce55..d660a32eb8d 100644 --- a/lib/ex_unit/test/ex_unit/case_template_test.exs +++ b/lib/ex_unit/test/ex_unit/case_template_test.exs @@ -36,7 +36,7 @@ defmodule ExUnit.NestedCase do end defmodule ExUnit.CaseTemplateTest do - use ExUnit.SampleCase, async: true + use ExUnit.SampleCase, async: true, another_option: 123 use ExUnit.NestedCase two = 2 diff --git a/lib/ex_unit/test/ex_unit/diff_test.exs b/lib/ex_unit/test/ex_unit/diff_test.exs index 019f6c754c7..87474a2324c 100644 --- a/lib/ex_unit/test/ex_unit/diff_test.exs +++ b/lib/ex_unit/test/ex_unit/diff_test.exs @@ -330,6 +330,8 @@ defmodule ExUnit.DiffTest do "[[[[], \"Hello-,- \"] | \"world\"] | \"!\"]", "[[[[], \"Hello \"] | \"world\"] | \"!\"]" ) + + refute_diff(:foo = %{bar: [:a | :b]}, "", "") end test "proper lists" do @@ -1133,6 +1135,12 @@ defmodule ExUnit.DiffTest do "-<> <> \"baz\"-", "+\"foobar\"+" ) + + refute_diff( + "hello " <> <<_::binary-size(6)>> = "hello world", + "\"hello \" <> -<<_::binary-size(6)>>-", + "\"hello +world+\"" + ) end test "underscore" do diff --git a/lib/ex_unit/test/ex_unit/formatter_test.exs b/lib/ex_unit/test/ex_unit/formatter_test.exs index a1e5506c1f6..6d115da9bc0 100644 --- a/lib/ex_unit/test/ex_unit/formatter_test.exs +++ b/lib/ex_unit/test/ex_unit/formatter_test.exs @@ -503,6 +503,17 @@ defmodule ExUnit.FormatterTest do """ end + test "formats assertions with nested improper list diffing" do + failure = [{:error, catch_assertion(assert :foo = %{bar: [1 | 2]}), []}] + + assert format_test_all_failure(test_module(), failure, 1, :infinity, &diff_formatter/2) =~ """ + match (=) failed + code: assert :foo = %{bar: [1 | 2]} + left: :foo + right: %{bar: [1 | 2]} + """ + end + defmodule BadInspect do defstruct key: 0 diff --git a/lib/ex_unit/test/ex_unit_test.exs b/lib/ex_unit/test/ex_unit_test.exs index 3db534d401e..152a9122f7b 100644 --- a/lib/ex_unit/test/ex_unit_test.exs +++ b/lib/ex_unit/test/ex_unit_test.exs @@ -997,6 +997,24 @@ defmodule ExUnitTest do assert length(runs) == 6 end + test "repeats tests up to the configured number of times with groups" do + defmodule TestGroupedRepeatUntilFailureReached do + use ExUnit.Case, async: true, group: :example + test __ENV__.line, do: assert(true) + end + + configure_and_reload_on_exit(repeat_until_failure: 5) + + output = + capture_io(fn -> + assert ExUnit.run() == %{total: 1, failures: 0, skipped: 0, excluded: 0} + end) + + runs = String.split(output, "Running ExUnit", trim: true) + # 6 runs in total, 5 repeats + assert length(runs) == 6 + end + test "stops on failure" do {:ok, pid} = Agent.start_link(fn -> 0 end) Process.register(pid, :ex_unit_repeat_until_failure_count) diff --git a/lib/iex/lib/iex.ex b/lib/iex/lib/iex.ex index 68a3dd31e72..222679237b1 100644 --- a/lib/iex/lib/iex.ex +++ b/lib/iex/lib/iex.ex @@ -391,16 +391,17 @@ defmodule IEx do The supported options are: + * `:auto_reload` + * `:alive_continuation_prompt` + * `:alive_prompt` * `:colors` - * `:inspect` - * `:width` - * `:history_size` - * `:default_prompt` * `:continuation_prompt` - * `:alive_prompt` - * `:alive_continuation_prompt` - * `:parser` + * `:default_prompt` * `:dot_iex` + * `:history_size` + * `:inspect` + * `:parser` + * `:width` They are discussed individually in the sections below. @@ -488,9 +489,6 @@ defmodule IEx do * `:alive_continuation_prompt` - used when `Node.alive?/0` returns `true` and more input is expected - * `:auto_reload` - when set to `true`, automatically purges in-memory - modules when they get invalidated by a concurrent compilation - The following values in the prompt string will be replaced appropriately: * `%counter` - the index of the history @@ -510,10 +508,16 @@ defmodule IEx do If the parser raises, the buffer is reset to an empty string. - ## dot_iex + ## `.iex` Configure the file loaded into your IEx session when it starts. See more information [in the `.iex.exs` documentation](`m:IEx#module-the-iex-exs-file`). + + ## Auto reloading + + When set to `true`, the `:auto_reload` option automatically purges + in-memory modules when they get invalidated by a concurrent compilation + happening in the Operating System. """ @spec configure(keyword()) :: :ok def configure(options) do diff --git a/lib/iex/lib/iex/autocomplete.ex b/lib/iex/lib/iex/autocomplete.ex index ae245e2a6fb..5dbd9992690 100644 --- a/lib/iex/lib/iex/autocomplete.ex +++ b/lib/iex/lib/iex/autocomplete.ex @@ -363,7 +363,7 @@ defmodule IEx.Autocomplete do end) entries = - for {key, _value} <- pairs, + for key when key != :__struct__ <- Map.keys(pairs), name = Atom.to_string(key), if(hint == "", do: not String.starts_with?(name, "_"), @@ -378,7 +378,8 @@ defmodule IEx.Autocomplete do case Code.Fragment.container_cursor_to_quoted(code) do {:ok, quoted} -> case Macro.path(quoted, &match?({:__cursor__, _, []}, &1)) do - [cursor, {:%{}, _, pairs}, {:%, _, [{:__aliases__, _, aliases}, _map]} | _] -> + [cursor, {:%{}, _, pairs}, {:%, _, [{:__aliases__, _, aliases = [h | _]}, _map]} | _] + when is_atom(h) -> container_context_struct(cursor, pairs, aliases, shell) [ @@ -386,8 +387,9 @@ defmodule IEx.Autocomplete do pairs, {:|, _, _}, {:%{}, _, _}, - {:%, _, [{:__aliases__, _, aliases}, _map]} | _ - ] -> + {:%, _, [{:__aliases__, _, aliases = [h | _]}, _map]} | _ + ] + when is_atom(h) -> container_context_struct(cursor, pairs, aliases, shell) [cursor, pairs, {:|, _, [{variable, _, nil} | _]}, {:%{}, _, _} | _] -> diff --git a/lib/iex/lib/iex/mix_listener.ex b/lib/iex/lib/iex/mix_listener.ex index 57f2e0f7f17..ccb194149d3 100644 --- a/lib/iex/lib/iex/mix_listener.ex +++ b/lib/iex/lib/iex/mix_listener.ex @@ -12,10 +12,18 @@ defmodule IEx.MixListener do @doc """ Unloads all modules invalidated by external compilations. + + Returns `:noop` if there is no module to purge or if + the listener is not running (it may happen when connecting + via --remsh to a node that was started without IEx). """ @spec purge :: :ok | :noop def purge do - GenServer.call(@name, :purge, :infinity) + if Process.whereis(@name) do + GenServer.call(@name, :purge, :infinity) + else + :noop + end end @impl true diff --git a/lib/iex/test/iex/autocomplete_test.exs b/lib/iex/test/iex/autocomplete_test.exs index 0827daf82e9..e4d14196306 100644 --- a/lib/iex/test/iex/autocomplete_test.exs +++ b/lib/iex/test/iex/autocomplete_test.exs @@ -416,6 +416,9 @@ defmodule IEx.AutocompleteTest do assert {:yes, ~c"ry: ", []} = expand(~c"%URI{path: \"foo\", que") assert {:no, [], []} = expand(~c"%URI{path: \"foo\", unkno") assert {:no, [], []} = expand(~c"%Unknown{path: \"foo\", unkno") + + assert {:yes, [], _} = expand(~c"%__MODULE__{") + assert {:yes, [], _} = expand(~c"%__MODULE__.Some{") end test "completion for struct keys in update syntax" do @@ -430,6 +433,19 @@ defmodule IEx.AutocompleteTest do assert {:yes, ~c"ry: ", []} = expand(~c"%URI{var | path: \"foo\", que") assert {:no, [], []} = expand(~c"%URI{var | path: \"foo\", unkno") assert {:no, [], []} = expand(~c"%Unknown{var | path: \"foo\", unkno") + + eval("var = %URI{}") + + assert {:yes, ~c"", entries} = expand(~c"%{var | ") + assert ~c"path:" in entries + assert ~c"query:" in entries + + assert {:yes, ~c"", entries} = expand(~c"%{var | path: \"foo\",") + assert ~c"path:" not in entries + assert ~c"query:" in entries + + assert {:yes, ~c"ry: ", []} = expand(~c"%{var | path: \"foo\", que") + assert {:no, [], []} = expand(~c"%URI{var | path: \"foo\", unkno") end test "completion for map keys in update syntax" do diff --git a/lib/iex/test/iex/helpers_test.exs b/lib/iex/test/iex/helpers_test.exs index 1af56114bae..490c97cb5bd 100644 --- a/lib/iex/test/iex/helpers_test.exs +++ b/lib/iex/test/iex/helpers_test.exs @@ -349,7 +349,7 @@ defmodule IEx.HelpersTest do assert captured =~ "-spec sleep(Time) -> ok when Time :: timeout()." else assert captured =~ "sleep(Time)" - assert captured =~ "@spec sleep(time) :: :ok when time: timeout()" + assert captured =~ "@spec sleep(time) :: :ok when time: " end end diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index 97f018e6fce..dc4181a33ab 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -352,6 +352,11 @@ defmodule Mix do * `MIX_INSTALL_DIR` *(since v1.12.0)* - specifies directory where `Mix.install/2` keeps install cache + * `MIX_OS_CONCURRENCY_LOCK` - when set to `0` or `false`, disables mix compilation locking. + While not recommended, this may be necessary in cases where hard links or TCP sockets are + not available. When opting for this behaviour, make sure to not start concurrent compilations + of the same project. + * `MIX_PATH` - appends extra code paths * `MIX_PROFILE` - a list of comma-separated Mix tasks to profile the time spent on diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index dcf04e4ed62..6dd57e45c1b 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -166,9 +166,6 @@ defmodule Mix.Compilers.Elixir do Mix.Utils.compiling_n(length(stale), :ex) Mix.Project.ensure_structure() - - # We don't want to cache this path as we will write to it - true = Code.prepend_path(dest) previous_opts = set_compiler_opts(opts) try do @@ -208,14 +205,18 @@ defmodule Mix.Compilers.Elixir do end Mix.Task.Compiler.notify_modules_compiled(lazy_modules_diff) - - unless_previous_warnings_as_errors(previous_warnings, opts, {:ok, all_warnings}) + unless_warnings_as_errors(opts, {:ok, all_warnings}) {:error, errors, %{runtime_warnings: r_warnings, compile_warnings: c_warnings}, state} -> + {errors, warnings} = + if opts[:warnings_as_errors], + do: {errors ++ r_warnings ++ c_warnings, []}, + else: {errors, r_warnings ++ c_warnings} + # In case of errors, we show all previous warnings and all new ones. {_, _, sources, _, _, _} = state errors = Enum.map(errors, &diagnostic/1) - warnings = Enum.map(r_warnings ++ c_warnings, &diagnostic/1) + warnings = Enum.map(warnings, &diagnostic/1) all_warnings = Keyword.get(opts, :all_warnings, errors == []) {:error, previous_warnings(sources, all_warnings) ++ warnings ++ errors} after @@ -254,8 +255,7 @@ defmodule Mix.Compilers.Elixir do all_warnings = Keyword.get(opts, :all_warnings, true) previous_warnings = previous_warnings(sources, all_warnings) - - unless_previous_warnings_as_errors(previous_warnings, opts, {status, previous_warnings}) + unless_warnings_as_errors(opts, {status, previous_warnings}) end end @@ -735,16 +735,22 @@ defmodule Mix.Compilers.Elixir do defp remove_and_purge(beam, module) do _ = File.rm(beam) - :code.purge(module) - :code.delete(module) + + if Code.loaded?(module) do + :code.purge(module) + :code.delete(module) + end end defp purge_modules_in_path(path) do with {:ok, beams} <- File.ls(path) do Enum.each(beams, fn beam -> module = beam |> Path.rootname() |> String.to_atom() - :code.purge(module) - :code.delete(module) + + if Code.loaded?(module) do + :code.purge(module) + :code.delete(module) + end end) end end @@ -922,9 +928,7 @@ defmodule Mix.Compilers.Elixir do end for {module, _} <- data do - File.rm(beam_path(compile_path, module)) - :code.purge(module) - :code.delete(module) + remove_and_purge(beam_path(compile_path, module), module) end rescue _ -> @@ -1012,8 +1016,8 @@ defmodule Mix.Compilers.Elixir do File.rm(manifest <> ".checkpoint") end - defp unless_previous_warnings_as_errors(previous_warnings, opts, {status, all_warnings}) do - if previous_warnings != [] and opts[:warnings_as_errors] do + defp unless_warnings_as_errors(opts, {status, all_warnings}) do + if all_warnings != [] and opts[:warnings_as_errors] do message = "Compilation failed due to warnings while using the --warnings-as-errors option" IO.puts(:stderr, message) {:error, all_warnings} @@ -1049,7 +1053,6 @@ defmodule Mix.Compilers.Elixir do threshold = opts[:long_compilation_threshold] || 10 profile = opts[:profile] verbose = opts[:verbose] || false - warnings_as_errors = opts[:warnings_as_errors] || false pid = spawn_link(fn -> @@ -1071,8 +1074,7 @@ defmodule Mix.Compilers.Elixir do long_compilation_threshold: threshold, profile: profile, beam_timestamp: timestamp, - return_diagnostics: true, - warnings_as_errors: warnings_as_errors + return_diagnostics: true ] response = Kernel.ParallelCompiler.compile_to_path(stale, dest, compile_opts) diff --git a/lib/mix/lib/mix/project.ex b/lib/mix/lib/mix/project.ex index 6f946b7e27d..157badc96b2 100644 --- a/lib/mix/lib/mix/project.ex +++ b/lib/mix/lib/mix/project.ex @@ -917,7 +917,10 @@ defmodule Mix.Project do build_path = build_path(config) on_taken = fn os_pid -> - Mix.shell().info("Waiting for lock on the build directory (held by process #{os_pid})") + Mix.shell().error([ + IO.ANSI.reset(), + "Waiting for lock on the build directory (held by process #{os_pid})" + ]) end Mix.Sync.Lock.with_lock(build_path, fun, on_taken: on_taken) @@ -931,7 +934,10 @@ defmodule Mix.Project do deps_path = deps_path(config) on_taken = fn os_pid -> - Mix.shell().info("Waiting for lock on the deps directory (held by process #{os_pid})") + Mix.shell().error([ + IO.ANSI.reset(), + "Waiting for lock on the deps directory (held by process #{os_pid})" + ]) end Mix.Sync.Lock.with_lock(deps_path, fun, on_taken: on_taken) diff --git a/lib/mix/lib/mix/scm/git.ex b/lib/mix/lib/mix/scm/git.ex index 4ff914dc7ee..95fcff44caa 100644 --- a/lib/mix/lib/mix/scm/git.ex +++ b/lib/mix/lib/mix/scm/git.ex @@ -126,17 +126,17 @@ defmodule Mix.SCM.Git do update_origin(opts[:git]) # Fetch external data - rev = get_lock_rev(opts[:lock], opts) || get_opts_rev(opts) + lock_rev = get_lock_rev(opts[:lock], opts) ["--git-dir=.git", "fetch", "--force", "--quiet"] |> Kernel.++(progress_switch(git_version())) |> Kernel.++(tags_switch(opts[:tag])) |> Kernel.++(depth_switch(opts[:depth])) - |> Kernel.++(refspec_switch(opts, rev)) + |> Kernel.++(refspec_switch(opts, lock_rev || get_opts_rev(opts))) |> git!() # Migrate the Git repo - rev = rev || default_branch() + rev = lock_rev || get_origin_opts_rev(opts) || default_branch() git!(["--git-dir=.git", "checkout", "--quiet", rev]) if opts[:submodules] do @@ -314,6 +314,14 @@ defmodule Mix.SCM.Git do opts[:branch] || opts[:ref] || opts[:tag] end + defp get_origin_opts_rev(opts) do + if branch = opts[:branch] do + "origin/#{branch}" + else + opts[:ref] || opts[:tag] + end + end + defp redact_uri(git) do case URI.parse(git) do %{userinfo: nil} -> git diff --git a/lib/mix/lib/mix/shell/io.ex b/lib/mix/lib/mix/shell/io.ex index 44d1e11e312..ca558030a66 100644 --- a/lib/mix/lib/mix/shell/io.ex +++ b/lib/mix/lib/mix/shell/io.ex @@ -100,12 +100,12 @@ defmodule Mix.Shell.IO do """ def cmd(command, opts \\ []) do print_app? = Keyword.get(opts, :print_app, true) + windows? = match?({:win32, _}, :os.type()) Mix.Shell.cmd(command, opts, fn data -> if print_app?, do: print_app() - # Due to encoding of shell command on Windows, - # let's write the data as is - IO.binwrite(data) + # Due to encoding of shell command on Windows, write the data as is. + if windows?, do: IO.binwrite(data), else: IO.write(data) end) end end diff --git a/lib/mix/lib/mix/sync/lock.ex b/lib/mix/lib/mix/sync/lock.ex index 5253d610e09..89bc5826646 100644 --- a/lib/mix/lib/mix/sync/lock.ex +++ b/lib/mix/lib/mix/sync/lock.ex @@ -79,6 +79,10 @@ defmodule Mix.Sync.Lock do This function can also be called if this process already has the lock. In such case the function is executed immediately. + When the `MIX_OS_CONCURRENCY_LOCK` environment variable is set to + a falsy value, the lock is ignored and the function is executed + immediately. + ## Options * `:on_taken` - a one-arity function called if the lock is held @@ -93,12 +97,12 @@ defmodule Mix.Sync.Lock do opts = Keyword.validate!(opts, [:on_taken]) hash = key |> :erlang.md5() |> Base.url_encode64(padding: false) - path = Path.join([System.tmp_dir!(), "mix_lock", hash]) + path = Path.join(base_path(), hash) pdict_key = {__MODULE__, path} - has_lock? = Process.get(pdict_key) + has_lock? = Process.get(pdict_key, false) - if has_lock? do + if has_lock? or lock_disabled?() do fun.() else lock = lock(path, opts[:on_taken]) @@ -115,6 +119,14 @@ defmodule Mix.Sync.Lock do end end + defp base_path do + # We include user in the dir to avoid permission conflicts across users + user = System.get_env("USER", "default") + Path.join(System.tmp_dir!(), "mix_lock_#{Base.url_encode64(user, padding: false)}") + end + + defp lock_disabled?(), do: System.get_env("MIX_OS_CONCURRENCY_LOCK") in ~w(0 false) + defp lock(path, on_taken) do File.mkdir_p!(path) @@ -198,11 +210,12 @@ defmodule Mix.Sync.Lock do :invalidated {:error, reason} -> - raise File.LinkError, - reason: reason, - action: "create hard link", - existing: port_path, - new: lock_path + Mix.raise(""" + could not create hard link from #{port_path} to "#{lock_path}: #{:file.format_error(reason)}. + + Hard link support is required for Mix compilation locking. If your system \ + does not support hard links, set MIX_OS_CONCURRENCY_LOCK=0\ + """) end end diff --git a/lib/mix/lib/mix/sync/pubsub.ex b/lib/mix/lib/mix/sync/pubsub.ex index 8c72406e944..bf58a098af2 100644 --- a/lib/mix/lib/mix/sync/pubsub.ex +++ b/lib/mix/lib/mix/sync/pubsub.ex @@ -274,7 +274,13 @@ defmodule Mix.Sync.PubSub do defp path(hash) do hash = Base.url_encode64(hash, padding: false) - Path.join([System.tmp_dir!(), "mix_pubsub", hash]) + Path.join(base_path(), hash) + end + + defp base_path do + # We include user in the dir to avoid permission conflicts across users + user = System.get_env("USER", "default") + Path.join(System.tmp_dir!(), "mix_pubsub_#{Base.url_encode64(user, padding: false)}") end defp recv(socket, size, timeout \\ :infinity) do diff --git a/lib/mix/lib/mix/tasks/clean.ex b/lib/mix/lib/mix/tasks/clean.ex index 7c0964f97d7..2f73b08c26c 100644 --- a/lib/mix/lib/mix/tasks/clean.ex +++ b/lib/mix/lib/mix/tasks/clean.ex @@ -65,7 +65,13 @@ defmodule Mix.Tasks.Clean do # Loadpaths without checks because compilers may be defined in deps. defp loadpaths! do - options = ["--no-elixir-version-check", "--no-deps-check", "--no-archives-check"] + options = [ + "--no-elixir-version-check", + "--no-deps-check", + "--no-archives-check", + "--no-listeners" + ] + Mix.Task.run("loadpaths", options) Mix.Task.reenable("loadpaths") Mix.Task.reenable("deps.loadpaths") diff --git a/lib/mix/lib/mix/tasks/cmd.ex b/lib/mix/lib/mix/tasks/cmd.ex index cfe43466db2..b4e3001e527 100644 --- a/lib/mix/lib/mix/tasks/cmd.ex +++ b/lib/mix/lib/mix/tasks/cmd.ex @@ -63,7 +63,7 @@ defmodule Mix.Tasks.Cmd do |> Enum.map(&String.to_atom/1) if apps != [] do - IO.warn("the --app in mix cmd is deprecated") + IO.warn("the --app in mix cmd is deprecated. Use mix do --app instead.") end if apps == [] or Mix.Project.config()[:app] in apps do diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index 61fcb907783..78c51706fcf 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -189,6 +189,13 @@ defmodule Mix.Tasks.Compile.App do Mix.Project.ensure_structure() File.write!(target, IO.chardata_to_string(contents)) 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") {:ok, []} else diff --git a/lib/mix/lib/mix/tasks/compile.ex b/lib/mix/lib/mix/tasks/compile.ex index b007653b4d1..148a9ef5268 100644 --- a/lib/mix/lib/mix/tasks/compile.ex +++ b/lib/mix/lib/mix/tasks/compile.ex @@ -86,7 +86,13 @@ defmodule Mix.Tasks.Compile do @impl true def run(["--list"]) do # Loadpaths without checks because compilers may be defined in deps. - args = ["--no-elixir-version-check", "--no-deps-check", "--no-archives-check"] + args = [ + "--no-elixir-version-check", + "--no-deps-check", + "--no-archives-check", + "--no-listeners" + ] + Mix.Task.run("loadpaths", args) Mix.Task.reenable("loadpaths") Mix.Task.reenable("deps.loadpaths") diff --git a/lib/mix/lib/mix/tasks/deps.loadpaths.ex b/lib/mix/lib/mix/tasks/deps.loadpaths.ex index 1f6566216a9..b8bba13e019 100644 --- a/lib/mix/lib/mix/tasks/deps.loadpaths.ex +++ b/lib/mix/lib/mix/tasks/deps.loadpaths.ex @@ -23,6 +23,7 @@ defmodule Mix.Tasks.Deps.Loadpaths do * `--no-compile` - does not compile even if files require compilation * `--no-deps-check` - does not check or compile deps, only load available ones * `--no-elixir-version-check` - does not check Elixir version + * `--no-listeners` - does not start Mix listeners * `--no-optional-deps` - does not compile or load optional deps """ @@ -48,6 +49,12 @@ defmodule Mix.Tasks.Deps.Loadpaths do Mix.Task.run("archive.check", args) 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 = @@ -57,21 +64,17 @@ defmodule Mix.Tasks.Deps.Loadpaths do all 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 - deps_check(all, "--no-compile" in args) + deps_check(config, all, "--no-compile" in args) end Code.prepend_paths(Enum.flat_map(all, &Mix.Dep.load_paths/1), cache: true) # For now we only allow listeners defined in dependencies, so # we start them right after adding adding deps to the path - Mix.PubSub.start_listeners() + if "--no-listeners" not in args do + Mix.PubSub.start_listeners() + end :ok end @@ -93,25 +96,38 @@ defmodule Mix.Tasks.Deps.Loadpaths do 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 all = Enum.map(all, &check_lock/1) - {not_ok, compile} = partition(all, [], []) + {not_ok, to_compile} = partition(all, [], []) cond do not_ok != [] -> show_not_ok!(not_ok) - compile == [] or no_compile? -> + to_compile == [] or no_compile? -> :ok true -> - Mix.Tasks.Deps.Compile.compile(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!() + {:compile, to_compile} end end @@ -136,6 +152,12 @@ defmodule Mix.Tasks.Deps.Loadpaths do {Enum.reverse(not_ok), Enum.reverse(compile)} 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. defp from_umbrella?(dep) do dep.opts[:from_umbrella] diff --git a/lib/mix/lib/mix/tasks/do.ex b/lib/mix/lib/mix/tasks/do.ex index c68fcb1c236..c46656f4a75 100644 --- a/lib/mix/lib/mix/tasks/do.ex +++ b/lib/mix/lib/mix/tasks/do.ex @@ -58,12 +58,25 @@ defmodule Mix.Tasks.Do do {apps, args} = extract_apps_from_args(args) show_forgotten_apps_warning(apps) - Enum.each(gather_commands(args), fn [task | args] -> - if apps == [] do - Mix.Task.run(task, args) - else - Mix.Task.run_in_apps(task, apps, args) - end + Enum.each(gather_commands(args), fn + [task | args] -> + if apps == [] do + Mix.Task.run(task, args) + else + 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 diff --git a/lib/mix/lib/mix/tasks/escript.build.ex b/lib/mix/lib/mix/tasks/escript.build.ex index 36fce7e6759..7a3cebfac0a 100644 --- a/lib/mix/lib/mix/tasks/escript.build.ex +++ b/lib/mix/lib/mix/tasks/escript.build.ex @@ -342,7 +342,7 @@ defmodule Mix.Tasks.Escript.Build do {zip_path, consolidated[Path.basename(path)] || path} end else - [] + files end end diff --git a/lib/mix/lib/mix/tasks/help.ex b/lib/mix/lib/mix/tasks/help.ex index f52df49b33e..0b1913ad46a 100644 --- a/lib/mix/lib/mix/tasks/help.ex +++ b/lib/mix/lib/mix/tasks/help.ex @@ -119,7 +119,13 @@ defmodule Mix.Tasks.Help do # Loadpaths without checks because tasks may be defined in deps. defp loadpaths! do - args = ["--no-elixir-version-check", "--no-deps-check", "--no-archives-check"] + args = [ + "--no-elixir-version-check", + "--no-deps-check", + "--no-archives-check", + "--no-listeners" + ] + Mix.Task.run("loadpaths", args) Mix.Task.reenable("loadpaths") Mix.Task.reenable("deps.loadpaths") diff --git a/lib/mix/lib/mix/tasks/loadpaths.ex b/lib/mix/lib/mix/tasks/loadpaths.ex index fffe930f5fc..5d0cc2bc4d8 100644 --- a/lib/mix/lib/mix/tasks/loadpaths.ex +++ b/lib/mix/lib/mix/tasks/loadpaths.ex @@ -21,6 +21,7 @@ defmodule Mix.Tasks.Loadpaths do * `--no-compile` - does not compile dependencies, only check and load them * `--no-deps-check` - does not check dependencies, only load available ones * `--no-elixir-version-check` - does not check Elixir version + * `--no-listeners` - does not start Mix listeners * `--no-optional-deps` - does not compile or load optional deps """ diff --git a/lib/mix/lib/mix/tasks/test.ex b/lib/mix/lib/mix/tasks/test.ex index ec3a0b60c5f..e41bac222ed 100644 --- a/lib/mix/lib/mix/tasks/test.ex +++ b/lib/mix/lib/mix/tasks/test.ex @@ -113,7 +113,7 @@ defmodule Mix.Tasks.Test do and imports (but not local functions). You can press `n` for the next line and `c` for the next test. This automatically sets `--trace` - * `--color` - enables color in the output + * `--color` - enables color in ExUnit formatting results * `--cover` - runs coverage tool. See "Coverage" section below diff --git a/lib/mix/test/mix/tasks/compile.app_test.exs b/lib/mix/test/mix/tasks/compile.app_test.exs index 53909254607..3674bfcfb25 100644 --- a/lib/mix/test/mix/tasks/compile.app_test.exs +++ b/lib/mix/test/mix/tasks/compile.app_test.exs @@ -66,8 +66,6 @@ defmodule Mix.Tasks.Compile.AppTest do test "generates .app file when changes happen" do in_fixture("no_mixfile", fn -> 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([]) assert Mix.Tasks.Compile.App.run([]) == {:ok, []} diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index 5d31f6e224b..8b130b74f02 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -1045,12 +1045,15 @@ defmodule Mix.Tasks.Compile.ElixirTest do # Compiles with missing external resources file = Path.absname("lib/a.eex") + source = Path.absname("lib/a.ex") 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([]) - 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"]) end) =~ "oops" diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index 2ecf29cbeb7..088887403d1 100644 --- a/lib/mix/test/mix/tasks/compile_test.exs +++ b/lib/mix/test/mix/tasks/compile_test.exs @@ -389,6 +389,10 @@ defmodule Mix.Tasks.CompileTest do File.write!("src/b.erl", "-module(b).") File.write!("src/c.erl", "-module(c).") + # Ensure we can boot with compilation and listeners if desired + assert mix(["loadpaths", "--no-compile", "--no-listeners"]) == "" + + # Now setup dependencies mix(["deps.compile"]) parent = self() diff --git a/lib/mix/test/mix/tasks/do_test.exs b/lib/mix/test/mix/tasks/do_test.exs index 23e540f1d15..dab90c4907c 100644 --- a/lib/mix/test/mix/tasks/do_test.exs +++ b/lib/mix/test/mix/tasks/do_test.exs @@ -13,6 +13,14 @@ defmodule Mix.Tasks.DoTest do 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 assert gather_commands(["help", "+", "compile"]) == [["help"], ["compile"]] diff --git a/lib/mix/test/mix/tasks/escript_test.exs b/lib/mix/test/mix/tasks/escript_test.exs index 5b284e9f08f..4e25a9b0b49 100644 --- a/lib/mix/test/mix/tasks/escript_test.exs +++ b/lib/mix/test/mix/tasks/escript_test.exs @@ -50,6 +50,20 @@ defmodule Mix.Tasks.EscriptTest do 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 in_fixture("escript_test", fn -> push_project_with_config(Escript)