Compare commits

...
117 Commits
Author SHA1 Message Date
Wojtek Mach 7df646d960 Backport building docs_config.js (#12620)
Cherry-picked from https://github.com/elixir-lang/elixir/commit/6f2c669a98c33d3c98a50c023b0434bb79041c89
2023-05-30 10:21:42 +02:00
José ValimandEric Meadows-Jönsson 74fe558c17 Fix generated docs_config.js on CI (#12618) (#12619)
Co-authored-by: Eric Meadows-Jönsson <eric.meadows.jonsson@gmail.com>
2023-05-30 09:47:54 +02:00
José Valim 7918d64700 Add branch docs publishing 2023-05-30 08:20:24 +02:00
José Valim 81d6007410 Release v1.14.5 2023-05-22 16:39:51 +02:00
José Valim be9fd499e6 Update Ubuntu instance 2023-05-22 16:39:25 +02:00
José Valim a5958e31a1 Release v1.14.5 2023-05-22 16:23:17 +02:00
Thanabodee Charoenpiriyakij 29067a0a7d Bump ubuntu to 20.04 (#12062)
The ubuntu-18.04 nows deprecated today [1]. This changes start moving it
to version 20.04 instead.

[1] https://github.com/actions/virtual-environments/issues/6002
2023-05-22 16:22:39 +02:00
José Valim ce8f0ac7d0 Release v1.14.5 2023-05-22 15:04:16 +02:00
José Valim 2d71e7144b Ensure conversion of maps to erlang ast is deterministic 2023-05-19 23:32:31 +02:00
José Valim 0c1395c215 Do not set encoding on Erlang/OTP 26 2023-05-18 11:55:24 +02:00
José Valim 1b3f9f8d5c Use httpc:ssl_verify_host_options/1 2023-04-07 11:28:02 +02:00
markovicmarko dbfdedf9b3 Update CHANGELOG.md (#12509)
Fix typo
2023-04-03 10:59:05 +02:00
José Valim 6f96693b35 Release v1.14.4 2023-04-03 09:33:52 +02:00
José Valim 4e5a8d48ba Update CHANGELOG 2023-04-03 09:31:59 +02:00
sabiwara acb5832c69 Fix dialyzer tests in OTP26 (#12486) 2023-04-03 09:28:11 +02:00
Zach Allaun f026cb3756 Fix column count when tokenizing escaped interpolations (#12504) 2023-03-30 23:04:21 +02:00
joepstender 73d58ec01b Fix typo in documentation (#12496) 2023-03-25 14:18:54 +01:00
José Valim e9a6c9e3b3 No need to revert trap exits 2023-03-24 23:17:23 +01:00
Kevin 237f3bc566 Backport test fixes (#12493) 2023-03-24 10:51:12 +01:00
Kevin 1a7545655c Update tests to new OTP start behaviour (#12490) 2023-03-23 15:23:52 +01:00
sabiwara 22fa810200 Fix logger formatting in OTP26 (#12434) 2023-03-23 14:12:40 +01:00
José Valim c3e0908c52 Clarify error message on missing certificates 2023-03-23 11:50:53 +01:00
Erick Dennis 3d0273afb3 Don't call formatter on directories (#12488) 2023-03-23 11:50:53 +01:00
Kevin 2b9d8b720e Add explicit :verify_none for httpc (#12485) 2023-03-23 11:50:22 +01:00
Andrea Leopardi 4687556f8f Add missing entry to CHANGELOG for v1.14.3 (#12468) 2023-03-15 09:20:07 +01:00
Alexandre a8824bab09 Fix Stream.zip/1 hanging on empty list (#12461) 2023-03-10 21:49:30 +01:00
José Valim 476b4fdf6e Bump timeout to infinity 2023-03-02 19:19:16 +01:00
Kevin 2229ed7924 Sort metadata before printing (#12440) 2023-03-02 11:34:46 +01:00
sabiwara f9c053d60c Make mix test reports deterministic in OTP26 (#12438)
* Make mix test reports deterministic in OTP26

The reports for the following were both relying on map keys ordering:
- mix test
- mix test.cover

* Fix non-deterministic test
2023-03-02 07:59:53 +01:00
sabiwara 1108a6c3d2 Fix more map inspection tests (#12433) 2023-02-28 12:56:27 +01:00
Shilov Konstantin 32c929b339 fix(supervisor): update documentation by call instead of cast (#12426) 2023-02-26 20:11:21 +01:00
José Valim 00e7245715 Update CHANGELOG 2023-02-23 15:09:03 +01:00
José Valim f596cf1629 Use get_until in IEx.Server
This change is necessary for forward compatibility
with Erlang/OTP 26+.
2023-02-23 15:08:03 +01:00
José Valim 12927b8840 Do not use private APIs in StringIO 2023-02-23 15:08:03 +01:00
José Valim 130ed60f4d Use infinity timeouts in parallel checker, closes #12421 2023-02-21 13:47:40 +01:00
José Valim 3eb94a482e Make sure protocols are loaded before dispatching
Closes https://github.com/hexpm/hex/issues/983.
2023-02-20 11:19:04 +01:00
sabiwara 6eca976752 Deterministic struct expansion (#12418) 2023-02-19 11:36:34 +01:00
sabiwara e977f023a0 Remove badfunc test on OTP26 (#12415) 2023-02-18 09:34:26 +01:00
sabiwara dc471cd876 Add :sort_maps to Inspect.Opts.custom_options (#12404) 2023-02-16 08:47:21 +01:00
sabiwara 6c31a6c8bb Fix Enum tests for maps in OTP26 (#12405) 2023-02-16 08:37:26 +01:00
José Valim 7535c0c403 Store complete path on quote keep, closes #12393 2023-02-13 12:56:31 +01:00
Zach Allaun 9fd9bceeaa Code.Normalizer: Use correct meta fields in normalize_call/2 (#12389) 2023-02-10 16:14:07 +01:00
José Valim 2ca0f1a99c Remove misplaced temporary files 2023-02-06 20:16:18 +01:00
Bram Verburg 6a1c5a449f Fix elixir_erl:debug_info/4 returning core_v1 (#12376) 2023-02-02 22:01:11 +01:00
José Valim c31d1b3eb0 Optimize application tracer 2023-01-28 11:31:07 +01:00
José Valim 6730d669fb Release v1.14.3 2023-01-14 16:22:53 +01:00
José Valim fb4ccfa656 Properly handle Macro inputs and output types in expansion
Closes #12299
Closes #12324
2023-01-14 16:00:45 +01:00
sabiwara ff2ea69710 Fix Macro.to_string/1 for large negative integers (#12326) 2023-01-10 10:50:51 +01:00
José Valim 2146bb449f Remove unecessary :beam_lib.info/1 call, closes #12317 2023-01-06 12:12:36 +01:00
José Valim 5a583c753b Consistently merge precisions in Calendar subsecond operations, closes #12303 2022-12-18 12:18:23 +01:00
José Valim ec31cc2331 Escape maps when expanding __ENV__ 2022-12-12 19:31:13 +01:00
Alex NaserandJosé Valim 6549d209d9 Precompile runtime module paths (#12294)
Co-authored-by: José Valim <jose.valim@dashbit.co>
2022-12-08 19:37:01 +01:00
José Valim 71d27919ad Revert "Ensure all runtime_modules loaded before mapping their paths (#12287)"
We don't want to load all modules because some may be
specified as `@compile {:autoload, false}`.

This reverts commit a8aea1c763.
2022-12-08 06:38:31 +01:00
Alex NaserandAlex Naser a8aea1c763 Ensure all runtime_modules loaded before mapping their paths (#12287)
Co-authored-by: Alex Naser <alex.naser@remote.com>
2022-12-05 20:46:27 +01:00
José Valim a72f18b38b Fix Range.disjoint?/2 2022-12-05 16:20:44 +01:00
José Valim a1a37c67b6 Optimize range membership implementation, closes #12282 2022-12-04 13:02:56 +01:00
José Valim ff73830103 Return values from running doctests and order tests 2022-12-01 13:22:03 +01:00
Thanabodee Charoenpiriyakij 950bcc0704 Fix Calendar.strftime/2 format negative year incorrectly (#12268)
Closes #12266
2022-11-24 09:37:01 +01:00
José Valim 80564fc954 Do not quote doctest excerpt 2022-11-23 18:01:31 +01:00
José Valim 94e0e386ce Do not crash for missing line info on type warnings
Closes #12263.
2022-11-23 05:35:09 +01:00
José Valim bfa799b31f Do not warn on captured underscored vars from defmodule
Closes #12262
2022-11-23 05:19:58 +01:00
Marcus Kruse abe2af477b Fix wildcard edge case (#12255) 2022-11-20 11:06:15 +01:00
sabiwara 2a82c35e39 Fix unused for optimization (#12252) 2022-11-19 10:27:40 +01:00
José Valim 1f14211a5e Handle false positive on List.to_charlist/1 formatting, closes #12248 2022-11-17 20:12:59 +01:00
José Valim 1ae36fa079 Keep expression when normalizing is_struct in blame
Closes #12243.
2022-11-15 23:03:53 +01:00
José Valim e3fc947b92 Pass the relevant module when generating unique vars for guards 2022-11-11 12:59:36 +01:00
José Valim 0909940b04 Release v1.14.2 2022-11-11 12:32:30 +01:00
José Valim d061cb96ef Improve docs for tracer events 2022-11-09 19:13:29 +01:00
José Valim d1f7457907 Document register 2022-11-09 19:13:29 +01:00
José Valim 123034569f Generate unique vars in defguard 2022-11-09 13:02:04 +01:00
José Valim bdea27c1e3 Include [if_undefined: :warn] on var metadata 2022-11-08 13:37:34 +01:00
José Valim 7f70f64a01 Do not include module variables in prune_binding 2022-11-08 11:36:38 +01:00
José Valim 2acd0949dc Improvements to prune binding 2022-11-08 00:33:19 +01:00
José Valim 7996c0a966 mix format 2022-11-03 11:57:31 +01:00
José Valim c01658b12d Add doctest metadata as tags 2022-11-03 11:56:44 +01:00
José Valim fdf96ad48a Prune binding from env and on literals 2022-11-03 09:57:33 +01:00
José Valim c80f8b6698 Ensure proper error message on mix test when there is no test directory 2022-11-01 18:38:18 +01:00
José Valim e65dae9f54 Improvements to ExUnit
* Do not run duplicate cases on ExUnit.run/1
* Allow test cases to not be registered on use
* Expose ExUnit.Formatter.format_assertion_diff/4
2022-10-31 15:31:25 +01:00
José Valim 6c5d7bfe3f Fix backported test 2022-10-29 21:14:32 +02:00
José Valim 804d03431a Add prune_binding to Code.eval_quoted_with_env/4 2022-10-29 20:43:59 +02:00
José Valim 29a2bb49d0 Add a test for hygienic vars on eval 2022-10-29 20:43:18 +02:00
sabiwara eb43a6a444 Mix.install accepts atoms as paths (#12204) 2022-10-16 11:32:01 +02:00
sabiwara fcb7b69cb5 Fix formatter: size*unit shortcut in bitstring (#12201) 2022-10-14 21:49:48 +02:00
José Valim 32c2a2db62 Expand :for in protocols with the appropriate env 2022-10-11 14:51:27 +02:00
José Valim a285e5876b Release v1.14.1 2022-10-10 16:28:46 +02:00
José Valim bc936f77c7 Add note about after_compiler, see #12159 2022-10-10 16:19:39 +02:00
José Valim ee602cd33e Skip unreliable stdin test on Windows 2022-10-08 22:21:12 +02:00
Michał Łępicki b5941d2f6b Expand default compile_env argument in expand literals (#12181) 2022-10-08 22:21:12 +02:00
José Valim 5f20a94895 expand_literal -> expand_literals 2022-10-07 11:57:32 +02:00
José Valim fd97c2f00f Update CHANGELOG 2022-10-07 11:54:45 +02:00
José Valim 3d2c3101c3 Add Macro.expand_literals/2 and do partial literal expansion in @
Closes #12174.
2022-10-07 11:44:10 +02:00
sabiwara 278eba5767 Handle unused :uniq comprehensions (#12116) 2022-10-05 10:43:17 +02:00
Sam Lin b2130136fb Fix range first and last description in module doc (#12169) 2022-10-04 16:23:19 +02:00
José Valim 7e76039434 More tests for malformed sequences 2022-10-04 16:14:26 +02:00
José Valim 375f2bd1cd Don't warn if :user is not defined in Erlang/OTP master (#12168) 2022-10-04 14:58:29 +02:00
José Valim f6f4f817f9 Improve coverage of malformed sequences in String 2022-10-03 12:42:21 +02:00
José Valim c2426ce98a Ignore malformed pictographic sequences, closes #12165 2022-10-03 12:42:06 +02:00
sabiwara e62a2d74ca Fix algebra edge case on :erlang.binary_to_atom/2 (#12163) 2022-10-02 23:32:57 +02:00
José Valim 61b2601488 mix format 2022-09-30 18:54:39 +02:00
José Valim 87dc7929fd Properly handle list output of :unicode_util.gc/1, closes #12160 2022-09-30 18:48:52 +02:00
Simon Prévost 78d6b88e33 Add complete list of time units on DateTime.add/3 argument error (#12152) 2022-09-23 17:26:15 +02:00
José Valim 7f494e6293 Fix signature order in type checkings messages, closes #11744 2022-09-23 13:38:25 +02:00
José Valim 41fa1db031 Do not recursively traverse vars to simplify checker, closes #12149 2022-09-23 13:38:25 +02:00
José Valim 887e2182dc Fix checker on missing debug info, closes #12140 2022-09-16 10:06:57 +02:00
José Valim 1c391417d4 Raise error message on missing metadata 2022-09-15 20:18:45 +02:00
José Valim 3f2ded08a5 Handle :port :undefined on URI.new 2022-09-15 19:40:12 +02:00
José Valim d60040636e Add :close_stdin to System.shell/2
It defaults to false as that was the behaviour
in past Elixir versions.

Closes #12135.
2022-09-15 00:09:56 +02:00
José Valim d84f88954c Fix bootstrap 2022-09-09 09:36:48 +02:00
José Valim d11d619c20 Do not delete enforce_keys on defstruct, closes #12126 2022-09-09 09:24:51 +02:00
José Valim f0943e74a7 Accept --all-warnings on mix test 2022-09-07 23:01:36 +02:00
José Valim d1055330c2 Do not crash when moduledoc and other functions are listed in only, closes #12120 2022-09-07 16:20:00 +02:00
José Valim efcb90bf10 Document noproc behaviour in start_link_supervised!, see #11737 2022-09-07 11:43:03 +02:00
José Valim 1457728e54 Make shell compatible with OTP 26 (#12114) 2022-09-06 16:33:31 +02:00
José Valim bd63670765 Do not cache inputs from .formatter.exs 2022-09-05 13:46:43 +02:00
José Valim b78b480587 Raise on missing struct for module conflict, closes #12113 2022-09-04 16:02:58 +02:00
José Valim 62ec71a489 Fix argument count for --no-pry 2022-09-03 00:31:41 +02:00
José Valim cfb51e3be9 Fix date 2022-09-01 20:18:19 +02:00
126 changed files with 2426 additions and 1136 deletions
+87
View File
@@ -0,0 +1,87 @@
name: builds.hex.pm
on:
push:
branches:
- main
- v*.*
tags:
- v*
env:
ELIXIR_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
concurrency: builds_txt
jobs:
release_pre_built:
strategy:
fail-fast: true
max-parallel: 1
matrix:
include:
- otp: 25
otp_version: '25.3'
build_docs: build_docs
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 50
- name: Get tags
run: git fetch --tags origin
- uses: ./.github/workflows/release_pre_built
with:
otp_version: ${{ matrix.otp_version }}
otp: ${{ matrix.otp }}
build_docs: ${{ matrix.build_docs }}
- name: Utils.sh
run: |
cat << 'EOF' > utils.sh
function purge_key() {
curl \
-X POST \
-H "Fastly-Key: ${FASTLY_KEY}" \
-H "Accept: application/json" \
-H "Content-Length: 0" \
"https://api.fastly.com/service/$1/purge/$2"
}
function purge() {
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
}
EOF
chmod +x utils.sh
- name: Upload Docs to S3
if: ${{ matrix.build_docs }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
run: |
source utils.sh
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
for f in doc/*; do
if [ -d "$f" ]; then
app=`echo $f | sed s/"doc\/"//`
tarball="${app}-${version}.tar.gz"
surrogate_key="docs/${app}-${version}"
tar -czf "${tarball}" -C "doc/${app}" .
aws s3 cp "${tarball}" "s3://${{ env.AWS_S3_BUCKET }}/docs/${tarball}" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
purge "${surrogate_key}"
fi
done
+4 -4
View File
@@ -13,7 +13,7 @@ permissions:
jobs:
test_linux:
name: Linux, ${{ matrix.otp_release }}, Ubuntu 18.04
name: Linux, ${{ matrix.otp_release }}, Ubuntu 20.04
strategy:
fail-fast: false
matrix:
@@ -28,7 +28,7 @@ jobs:
development: true
- otp_release: maint
development: true
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
with:
@@ -36,7 +36,7 @@ jobs:
- name: Install Erlang/OTP
run: |
cd $RUNNER_TEMP
wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-18.04/${{ matrix.otp_release }}.tar.gz
wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-20.04/${{ matrix.otp_release }}.tar.gz
mkdir -p otp
tar zxf otp.tar.gz -C otp --strip-components=1
otp/Install -minimal $(pwd)/otp
@@ -113,7 +113,7 @@ jobs:
check_posix_compliant:
name: Check POSIX-compliant
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
with:
+3 -3
View File
@@ -10,7 +10,7 @@ permissions:
jobs:
notify:
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
name: Notify
steps:
- uses: actions/checkout@v3
@@ -18,8 +18,8 @@ jobs:
fetch-depth: 50
- uses: erlef/setup-beam@v1
with:
otp-version: 24.3
elixir-version: 1.13.4
otp-version: '25.0'
elixir-version: '1.14.0'
- name: Run Elixir script
env:
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
+2 -2
View File
@@ -14,7 +14,7 @@ jobs:
create_draft_release:
permissions:
contents: write
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
@@ -39,7 +39,7 @@ jobs:
- otp: 25
otp_version: 25.0
build_docs: build_docs
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
with:
@@ -0,0 +1,51 @@
name: "Release pre built"
description: "Builds elixir release, ExDoc and generates docs"
inputs:
otp:
description: "The major OTP version"
otp_version:
description: "The exact OTP version (major.minor[.patch])"
build_docs:
description: "If docs have to be built or not"
runs:
using: "composite"
steps:
- uses: erlef/setup-beam@v1
with:
otp-version: ${{ inputs.otp_version }}
version-type: strict
- name: Build Elixir Release
shell: bash
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: Get latest stable ExDoc version
if: ${{ inputs.build_docs }}
shell: bash
run: |
EX_DOC_LATEST_STABLE_VERSION=$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
echo "EX_DOC_LATEST_STABLE_VERSION=${EX_DOC_LATEST_STABLE_VERSION}" >> $GITHUB_ENV
- uses: actions/checkout@v3
if: ${{ inputs.build_docs }}
with:
repository: elixir-lang/ex_doc
ref: v${{ env.EX_DOC_LATEST_STABLE_VERSION }}
path: ex_doc
- name: Build ex_doc
if: ${{ inputs.build_docs }}
shell: bash
run: |
mv ex_doc ../ex_doc
cd ../ex_doc
../elixir/bin/mix do local.rebar --force + local.hex --force + deps.get + compile
cd ../elixir
- name: Build Docs
if: ${{ inputs.build_docs }}
shell: bash
run: |
make Docs.zip
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
+164 -3
View File
@@ -241,7 +241,168 @@ protocol, giving developers more control over the struct representation.
See the updated documentation for `Inspect` for a general rundown on
the approaches and options available.
## v1.14.0 (2022-08-31)
## v1.14.5 (2023-05-22)
This release contains fixes for Erlang/OTP 26.
### Bug fixes
#### Elixir
* [CLI] Fix a bug where stdout would block when there was no attached terminal on Windows when running on Erlang/OTP 26
#### Mix
* [Mix] Properly set SSL configuration for Mix downloads when running on Erlang/OTP 26
## v1.14.4 (2023-04-03)
This release adds basic support for Erlang/OTP 26. When migrating
to Erlang/OTP 26, keep it mind it changes how maps are stored
internally and they will be printed and traversed in a different
order (note maps never provided a guarantee of their order).
To aid migration, this release adds `:sort_maps` to `inspect`
custom options, in case you want to sort them before inspection:
inspect(map, custom_options: [sort_maps: true])
### Enhancements
#### Elixir
* [Inspect] Add `:sort_maps` to `Inspect.Opts.custom_options`
#### IEx
* [IEx] Support shell history in Erlang/OTP 26+
#### Mix
* [mix compile.elixir] Optimize application tracer
### Bug fixes
#### Elixir
* [Code] Properly handle blocks with comments in all cases in `Code.quoted_to_string_with_comments/2`
* [Kernel] Fix `debug_info/4` when returning core_v1
* [Kernel] Store complete path on `quote keep: true` to avoid invalid stacktraces
* [Kernel] Fix column count when tokenizing escaped interpolations
* [Stream] Fix `Stream.zip/1` hanging on empty list
#### Mix
* [mix format] Don't call formatter on directories
## v1.14.3 (2023-01-14)
### 1. Enhancements
#### Elixir
* [Kernel] Speed up loading of runtime modules in the parallel compiler
* [Range] Optimize range membership implementation
#### ExUnit
* [ExUnit] Return values from running doctests and make their order consistent
### 2. Bug fixes
#### Elixir
* [Calendar] Fix handling of negative years in `Calendar.strftime/2`
* [DateTime] Consistently merge precision of subsecond operations (the bug was that subsecond precision was lost in functions like `DateTime.add/3`)
* [Exception] Improve blaming of FunctionClauseError with `is_struct/2` guards
* [Kernel] Fix invalid variable scoping in `defguard` expansion
* [Kernel] Do not warn on captured underscored vars from `defmodule`
* [Kernel] Do not crash for missing line info on type warnings
* [NaiveDateTime] Consistently merge precision of subsecond operations (the bug was that subsecond precision was lost in functions like `NaiveDateTime.add/3`)
* [Macro] Fix `Macro.to_string/1` for large negative integers
* [Macro] Properly type and escape expansion of `__ENV__` in macros
* [Path] Make sure `Path.wildcard/2` expands `..` symlinks accordingly
* [Range] Address corner cases in `Range.disjoint?/2` implementation
* [Time] Consistently merge precision of subsecond operations (the bug was that subsecond precision was lost in functions like `Time.add/3`)
#### ExUnit
* [ExUnit.DocTest] Remove unnecessary literal quotes from error message on reports
## v1.14.2 (2022-11-11)
### 1. Enhancements
#### Elixir
* [Code] Add `Code.eval_quoted_with_env/4` with support for the `:prune_binding` option
#### ExUnit
* [ExUnit.Case] Allow test cases to not be registered on use
* [ExUnit.DocTest] Include `:doctest` and `:doctest_line` as meta tags
* [ExUnit.Formatter] Expose `ExUnit.Formatter.format_assertion_diff/4`
#### Mix
* [Mix] `Mix.install/2` accepts atoms as paths
### 2. Bug fixes
#### Elixir
* [Code.Formatter] Fix `size*unit` shortcut in bitstring
* [Kernel] Generate unique variables for macro expansion of `defguard`
* [Protocol] Expand `:for` in protocols with the appropriate env
#### ExUnit
* [ExUnit] Do not run duplicate cases on `ExUnit.run/1`
#### Mix
* [mix test] Ensure proper error message when there is no test directory
## v1.14.1 (2022-10-10)
### 1. Enhancements
#### Elixir
* [Kernel] Perform partial expansion of literals in module attributes
* [Kernel] Do not add compile-time dependencies for literals as defaults in `Application.compile_env/3` inside module attributes
* [Macro] Add `Macro.expand_literals/2` and `Macro.expand_literals/3`
* [System] Add `:close_stdin` to `System.shell/2`
#### Mix
* [mix test] Accept `--all-warnings` option
## 2. Bug fixes
#### Elixir
* [Kernel] Fix misleading warning when `:uniq` is given in `for` comprehensions and the result is unused
* [Kernel] Improve error message for when there is a conflicting struct and ignoring module conflict
* [Kernel] Do not delete `@enforce_keys` attribute after `defstruct` declaration
* [Kernel] Do not crash the checker on modules with missing `:debug_info` chunk
* [Macro] Fix error in `Macro.to_string/2` when converting an AST with `:erlang.binary_to_atom/2`
* [String] Fix `String.split/3` and `String.next_grapheme/1` returning invalid results on invalid UTF-8 encoding
* [System] Do not close stdin by default in `System.shell/2`
* [URI] Do not return `uri.port` as `:undefined` in certain cases in `URI.new/1`
#### ExUnit
* [ExUnit.DocTest] Do not crash when both `:moduledoc` and functions are specified in `:only`
#### IEx
* [CLI] Fix invalid argument handling when `--no-pry` is given
#### Mix
* [mix format] Do not cache inputs from `.formatter.exs` so they are properly re-evaluted on every call
## v1.14.0 (2022-09-01)
### 1. Enhancements
@@ -287,7 +448,7 @@ the approaches and options available.
* [Keyword] Add `Keyword.from_keys/2` and `Keyword.replace_lazy/3`
* [List] Add `List.keysort/3` with support for a `sorter` function
* [Macro] Add `Macro.classify_atom/1` and `Macro.inspect_atom/2`
* [Macro] Add `Macro.expand_literal/2` and `Macro.path/2`
* [Macro] Add `Macro.path/2`
* [Macro.Env] Add `Macro.Env.prune_compile_info/1`
* [Map] Add `Map.from_keys/2` and `Map.replace_lazy/3`
* [MapSet] Add `MapSet.filter/2`, `MapSet.reject/2`, and `MapSet.symmetric_difference/2`
@@ -361,7 +522,7 @@ the approaches and options available.
* [Module] Fix loop while unifying type variables
* [Protocol] Warn if a protocol has no definitions
* [Regex] Show list options when inspecting a Regex manually defined with `Regex.compile/2`
* [String] Allow slices to overflow on both starting and ending positions
* [String] Allow slices to overflow on both starting and ending positions
* [System] Raise non-generic exception on missing env in `System.fetch_env!/1` to mirror map operations
#### ExUnit
+15 -8
View File
@@ -181,39 +181,46 @@ clean_residual_files:
LOGO_PATH = $(shell test -f ../docs/logo.png && echo "--logo ../docs/logo.png")
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
COMPILE_DOCS = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" --formatter "$(DOCS_FORMAT)" $(4)
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" --formatter "$(DOCS_FORMAT)" $(4)
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)"
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
docs_elixir: compile ../ex_doc/bin/ex_doc
@ echo "==> ex_doc (elixir)"
$(Q) rm -rf doc/elixir
$(call COMPILE_DOCS,Elixir,elixir,Kernel,--config "lib/elixir/docs.exs")
$(call DOCS_COMPILE,Elixir,elixir,Kernel,--config "lib/elixir/scripts/elixir_docs.exs")
$(call DOCS_CONFIG,elixir)
docs_eex: compile ../ex_doc/bin/ex_doc
@ echo "==> ex_doc (eex)"
$(Q) rm -rf doc/eex
$(call COMPILE_DOCS,EEx,eex,EEx,--config "lib/mix/docs.exs")
$(call DOCS_COMPILE,EEx,eex,EEx,--config "lib/elixir/scripts/mix_docs.exs")
$(call DOCS_CONFIG,eex)
docs_mix: compile ../ex_doc/bin/ex_doc
@ echo "==> ex_doc (mix)"
$(Q) rm -rf doc/mix
$(call COMPILE_DOCS,Mix,mix,Mix,--config "lib/mix/docs.exs")
$(call DOCS_COMPILE,Mix,mix,Mix,--config "lib/elixir/scripts/mix_docs.exs")
$(call DOCS_CONFIG,mix)
docs_iex: compile ../ex_doc/bin/ex_doc
@ echo "==> ex_doc (iex)"
$(Q) rm -rf doc/iex
$(call COMPILE_DOCS,IEx,iex,IEx,--config "lib/mix/docs.exs")
$(call DOCS_COMPILE,IEx,iex,IEx,--config "lib/elixir/scripts/mix_docs.exs")
$(call DOCS_CONFIG,iex)
docs_ex_unit: compile ../ex_doc/bin/ex_doc
@ echo "==> ex_doc (ex_unit)"
$(Q) rm -rf doc/ex_unit
$(call COMPILE_DOCS,ExUnit,ex_unit,ExUnit,--config "lib/mix/docs.exs")
$(call DOCS_COMPILE,ExUnit,ex_unit,ExUnit,--config "lib/elixir/scripts/mix_docs.exs")
$(call DOCS_CONFIG,ex_unit)
docs_logger: compile ../ex_doc/bin/ex_doc
@ echo "==> ex_doc (logger)"
$(Q) rm -rf doc/logger
$(call COMPILE_DOCS,Logger,logger,Logger,--config "lib/mix/docs.exs")
$(call DOCS_COMPILE,Logger,logger,Logger,--config "lib/elixir/scripts/mix_docs.exs")
$(call DOCS_CONFIG,logger)
../ex_doc/bin/ex_doc:
@ echo "ex_doc is not found in ../ex_doc as expected. See README for more information."
@@ -228,7 +235,7 @@ Docs.zip: docs
Precompiled.zip: build_man compile
rm -f Precompiled.zip
zip -9 -r Precompiled.zip bin CHANGELOG.md lib/*/ebin lib/*/lib LICENSE man NOTICE README.md VERSION
zip -9 -r Precompiled.zip bin CHANGELOG.md lib/*/ebin lib/*/lib LICENSE Makefile man NOTICE README.md VERSION
@ echo "Precompiled file created $(CURDIR)/Precompiled.zip"
#==> Test tasks
+1 -1
View File
@@ -1 +1 @@
1.14.0
1.14.5
+3 -3
View File
@@ -1,7 +1,7 @@
#!/bin/sh
set -e
ELIXIR_VERSION=1.14.0
ELIXIR_VERSION=1.14.5
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
@@ -112,10 +112,10 @@ while [ $I -le $LENGTH ]; do
C=1
MODE="elixirc"
;;
-v|--no-halt)
-v|--no-halt|--no-pry)
C=1
;;
-e|-r|-pr|-pa|-pz|--app|--eval|--remsh|--dot-iex|--no-pry)
-e|-r|-pr|-pa|-pz|--app|--eval|--remsh|--dot-iex)
C=2
;;
--rpc-eval)
+1 -1
View File
@@ -1,6 +1,6 @@
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
set ELIXIR_VERSION=1.14.0
set ELIXIR_VERSION=1.14.5
setlocal enabledelayedexpansion
if ""%1""=="""" if ""%2""=="""" goto documentation
+2 -2
View File
@@ -532,7 +532,7 @@ defmodule Application do
raise "Application.compile_env/3 cannot be called inside functions, only in the module body"
end
key_or_path = Macro.expand_literal(key_or_path, %{__CALLER__ | function: {:__info__, 1}})
key_or_path = Macro.expand_literals(key_or_path, %{__CALLER__ | function: {:__info__, 1}})
quote do
Application.compile_env(__ENV__, unquote(app), unquote(key_or_path), unquote(default))
@@ -572,7 +572,7 @@ defmodule Application do
raise "Application.compile_env!/2 cannot be called inside functions, only in the module body"
end
key_or_path = Macro.expand_literal(key_or_path, %{__CALLER__ | function: {:__info__, 1}})
key_or_path = Macro.expand_literals(key_or_path, %{__CALLER__ | function: {:__info__, 1}})
quote do
Application.compile_env!(__ENV__, unquote(app), unquote(key_or_path))
+8 -1
View File
@@ -791,7 +791,14 @@ defmodule Calendar do
# Year
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.year |> Integer.to_string() |> pad_leading(width, pad)
{sign, year} =
if datetime.year < 0 do
{?-, -datetime.year}
else
{[], datetime.year}
end
result = [sign | year |> Integer.to_string() |> pad_leading(width, pad)]
parse(rest, datetime, format_options, [result | acc])
end
+14
View File
@@ -1493,6 +1493,13 @@ defmodule DateTime do
iex> dt |> DateTime.add(1, :day, FakeTimeZoneDatabase)
#DateTime<2019-04-01 02:00:00+02:00 CEST Europe/Copenhagen>
This operation merges the precision of the naive date time with the given unit:
iex> result = DateTime.add(~U[2014-10-02 00:29:10Z], 21, :millisecond)
~U[2014-10-02 00:29:10.021Z]
iex> result.microsecond
{21000, 3}
"""
@doc since: "1.8.0"
@spec add(
@@ -1529,8 +1536,15 @@ defmodule DateTime do
microsecond: {_, precision}
} = datetime
if not is_integer(unit) and
unit not in ~w(second millisecond microsecond nanosecond)a do
raise ArgumentError,
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end
ppd = System.convert_time_unit(86400, :second, unit)
total_offset = System.convert_time_unit(utc_offset + std_offset, :second, unit)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
result =
datetime
+11
View File
@@ -247,6 +247,17 @@ defmodule Calendar.ISO do
defguardp is_utc_offset(offset) when is_integer(offset)
defguardp is_std_offset(offset) when is_integer(offset)
@doc """
Converts a `t:System.time_unit/0` to precision.
Integer-based time units always get maximum precision.
"""
def time_unit_to_precision(:nanosecond), do: 6
def time_unit_to_precision(:microsecond), do: 6
def time_unit_to_precision(:millisecond), do: 3
def time_unit_to_precision(:second), do: 0
def time_unit_to_precision(_), do: 6
@doc """
Parses a time `string` in the `:extended` format.
+8 -12
View File
@@ -361,8 +361,7 @@ defmodule NaiveDateTime do
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function always consider the unit to be computed according
to the `Calendar.ISO`.
This function always considers the unit to be computed according to the `Calendar.ISO`.
## Examples
@@ -379,7 +378,7 @@ defmodule NaiveDateTime do
It can also work with subsecond precisions:
iex> NaiveDateTime.add(~N[2014-10-02 00:29:10], 2_000, :millisecond)
~N[2014-10-02 00:29:12]
~N[2014-10-02 00:29:12.000]
As well as days/hours/minutes:
@@ -390,16 +389,12 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.add(~N[2015-02-28 00:29:10], 60, :minute)
~N[2015-02-28 01:29:10]
This operation keeps the precision of the naive date time:
This operation merges the precision of the naive date time with the given unit:
iex> NaiveDateTime.add(~N[2014-10-02 00:29:10.021], 21, :second)
~N[2014-10-02 00:29:31.021]
And ignores any changes below the precision:
iex> hidden = NaiveDateTime.add(~N[2014-10-02 00:29:10], 21, :millisecond)
iex> hidden.microsecond # ~N[2014-10-02 00:29:10]
{21000, 0}
iex> result = NaiveDateTime.add(~N[2014-10-02 00:29:10], 21, :millisecond)
~N[2014-10-02 00:29:10.021]
iex> result.microsecond
{21000, 3}
Operations on top of gregorian seconds or the Unix epoch are optimized:
@@ -440,6 +435,7 @@ defmodule NaiveDateTime do
)
when is_integer(amount_to_add) do
ppd = System.convert_time_unit(86400, :second, unit)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
naive_datetime
|> to_iso_days()
+10 -2
View File
@@ -493,6 +493,13 @@ defmodule Time do
iex> Time.add(~T[17:10:05], 30, :minute)
~T[17:40:05]
This operation merges the precision of the time with the given unit:
iex> result = Time.add(~T[00:29:10], 21, :millisecond)
~T[00:29:10.021]
iex> result.microsecond
{21000, 3}
"""
@doc since: "1.6.0"
@spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t
@@ -511,6 +518,7 @@ defmodule Time do
amount_to_add = System.convert_time_unit(amount_to_add, unit, :microsecond)
total = time_to_microseconds(time) + amount_to_add
parts = Integer.mod(total, @parts_per_day)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
{hour, minute, second, {microsecond, _}} =
calendar.time_from_day_fraction({parts, @parts_per_day})
@@ -736,14 +744,14 @@ defmodule Time do
hour: hour1,
minute: minute1,
second: second1,
microsecond: {microsecond1, @parts_per_day}
microsecond: {microsecond1, _}
},
%{
calendar: Calendar.ISO,
hour: hour2,
minute: minute2,
second: second2,
microsecond: {microsecond2, @parts_per_day}
microsecond: {microsecond2, _}
},
unit
) do
+43 -24
View File
@@ -100,10 +100,15 @@ defmodule Code do
following events are available to tracers:
* `:start` - (since v1.11.0) invoked whenever the compiler starts to trace
a new lexical context, such as a new file. Keep in mind the compiler runs
in parallel, so multiple files may invoke `:start` and run at the same
time. The value of the `lexical_tracker` of the macro environment, albeit
opaque, can be used to uniquely identify the environment.
a new lexical context. A lexical context is started when compiling a new
file or when defining a module within a function. Note evaluated code
does not start a new lexical context (because they don't track unused
aliases, imports, etc) but defining a module inside evaluated code will.
Note this event may be emitted in parallel, where multiple files/modules
invoke `:start` and run at the same time. The value of the `lexical_tracker`
of the macro environment, albeit opaque, can be used to uniquely identify
the environment.
* `:stop` - (since v1.11.0) invoked whenever the compiler stops tracing a
new lexical context, such as a new file.
@@ -181,7 +186,7 @@ defmodule Code do
"""
@typedoc """
A list with all variable bindings.
A list with all variables and their values.
The binding keys are usually atoms, but they may be a tuple for variables
defined in a different context.
@@ -344,7 +349,7 @@ defmodule Code do
@doc """
Evaluates the contents given by `string`.
The `binding` argument is a list of variable bindings.
The `binding` argument is a list of all variables and their values.
The `opts` argument is a keyword list of environment options.
**Warning**: `string` can be any Elixir code and will be executed with
@@ -362,17 +367,15 @@ defmodule Code do
* `:line` - the line on which the script starts
Additionally, you may also pass an environment as second argument,
so the evaluation happens within that environment. However, if the evaluated
code requires or compiles another file, the environment given to this function
will not apply to said files.
so the evaluation happens within that environment.
Returns a tuple of the form `{value, binding}`, where `value` is the value
returned from evaluating `string`. If an error occurs while evaluating
`string` an exception will be raised.
`string`, an exception will be raised.
`binding` is a list with all variable bindings after evaluating `string`.
The binding keys are usually atoms, but they may be a tuple for variables
defined in a different context.
`binding` is a list with all variable names and their values after evaluating
`string`. The binding keys are usually atoms, but they may be a tuple for variables
defined in a different context. The names are in no particular order.
## Examples
@@ -419,13 +422,13 @@ defmodule Code do
defp validated_eval_string(string, binding, opts_or_env) do
%{line: line, file: file} = env = env_for_eval(opts_or_env)
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
{value, binding, _env} = eval_verify(:eval_forms, forms, binding, env)
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env])
{value, binding}
end
defp eval_verify(fun, forms, binding, env) do
defp eval_verify(fun, args) do
Module.ParallelChecker.verify(fn ->
apply(:elixir, fun, [forms, binding, env])
apply(:elixir, fun, args)
end)
end
@@ -822,7 +825,9 @@ defmodule Code do
"""
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
{value, binding, _env} = eval_verify(:eval_quoted, quoted, binding, env_for_eval(env_or_opts))
{value, binding, _env} =
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
{value, binding}
end
@@ -859,10 +864,20 @@ defmodule Code do
Therefore, the first time you call this function, you must compute
the initial environment with `env_for_eval/1`. The remaining calls
must pass the environment that was returned by this function.
## Options
* `:prune_binding` - (since v1.14.2) prune binding to keep only
variables read or written by the evaluated code. Note that
variables used by modules are always pruned, even if later used
by the modules. You can submit to the `:on_module` tracer event
and access the variables used by the module from its environment.
"""
@doc since: "1.14.0"
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env) when is_list(binding) do
eval_verify(:eval_forms, quoted, binding, env)
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
when is_list(binding) do
eval_verify(:eval_quoted, [quoted, binding, env, opts])
end
@doc ~S"""
@@ -1334,12 +1349,16 @@ defmodule Code do
Available options are:
* `:docs` - when `true`, retain documentation in the compiled module.
* `:docs` - when `true`, retains documentation in the compiled module.
Defaults to `true`.
* `:debug_info` - when `true`, retain debug information in the compiled
module. This allows a developer to reconstruct the original source
code. Defaults to `true`.
* `:debug_info` - when `true`, retains debug information in the compiled
module. This enables static analysis tools as it allows developers to
partially reconstruct the original source code. Therefore, disabling
`:debug_info` is not recommended as it removes the ability of the
Elixir compiler and other tools to provide feedback. If you want to
remove the `:debug_info` while deploying, tools like `mix release`
already do such by default.
* `:ignore_already_consolidated` - when `true`, does not warn when a protocol
has already been consolidated and a new implementation is added. Defaults
@@ -1348,7 +1367,7 @@ defmodule Code do
* `:ignore_module_conflict` - when `true`, does not warn when a module has
already been defined. Defaults to `false`.
* `:relative_paths` - when `true`, use relative paths in quoted nodes,
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
warnings, and errors generated by the compiler. Note disabling this option
won't affect runtime warnings and errors. Defaults to `true`.
+15 -8
View File
@@ -1411,7 +1411,7 @@ defmodule Code.Formatter do
defp bitstring_segment_to_algebra({{:"::", _, [segment, spec]}, i}, state, last) do
{doc, state} = quoted_to_algebra(segment, :parens_arg, state)
{spec, state} = bitstring_spec_to_algebra(spec, state)
{spec, state} = bitstring_spec_to_algebra(spec, state, state.normalize_bitstring_modifiers)
spec = wrap_in_parens_if_inspected_atom(spec)
spec = if i == last, do: bitstring_wrap_parens(spec, i, last), else: spec
@@ -1430,26 +1430,29 @@ defmodule Code.Formatter do
{bitstring_wrap_parens(doc, i, last), state}
end
defp bitstring_spec_to_algebra({op, _, [left, right]}, state) when op in [:-, :*] do
{left, state} = bitstring_spec_to_algebra(left, state)
{right, state} = bitstring_spec_element_to_algebra(right, state)
defp bitstring_spec_to_algebra({op, _, [left, right]}, state, normalize_modifiers)
when op in [:-, :*] do
normalize_modifiers = normalize_modifiers && op != :*
{left, state} = bitstring_spec_to_algebra(left, state, normalize_modifiers)
{right, state} = bitstring_spec_element_to_algebra(right, state, normalize_modifiers)
{concat(concat(left, Atom.to_string(op)), right), state}
end
defp bitstring_spec_to_algebra(spec, state) do
bitstring_spec_element_to_algebra(spec, state)
defp bitstring_spec_to_algebra(spec, state, normalize_modifiers) do
bitstring_spec_element_to_algebra(spec, state, normalize_modifiers)
end
defp bitstring_spec_element_to_algebra(
{atom, meta, empty_args},
state = %{normalize_bitstring_modifiers: true}
state,
_normalize_modifiers = true
)
when is_atom(atom) and empty_args in [nil, []] do
empty_args = bitstring_spec_normalize_empty_args(atom)
quoted_to_algebra_with_parens_if_operator({atom, meta, empty_args}, :parens_arg, state)
end
defp bitstring_spec_element_to_algebra(spec_element, state) do
defp bitstring_spec_element_to_algebra(spec_element, state, _normalize_modifiers) do
quoted_to_algebra_with_parens_if_operator(spec_element, :parens_arg, state)
end
@@ -1580,6 +1583,10 @@ defmodule Code.Formatter do
insert_underscores(int_part) <> "." <> decimal_part
end
defp insert_underscores("-" <> digits) do
"-" <> insert_underscores(digits)
end
defp insert_underscores(digits) do
cond do
digits =~ "_" ->
+40 -25
View File
@@ -70,13 +70,21 @@ defmodule Code.Normalizer do
# Atoms with interpolations
defp do_normalize(
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta,
[{:<<>>, _, args} = string, :utf8]},
[{:<<>>, _, parts} = string, :utf8]},
state
)
when is_list(args) do
when is_list(parts) do
dot_meta = patch_meta_line(dot_meta, state.parent_meta)
call_meta = patch_meta_line(call_meta, dot_meta)
utf8 =
if parts == [] or binary_interpolated?(parts) do
# a non-normalized :utf8 atom signals an atom interpolation
:utf8
else
normalize_literal(:utf8, [], state)
end
string =
if state.escape do
normalize_bitstring(string, state, true)
@@ -84,27 +92,31 @@ defmodule Code.Normalizer do
normalize_bitstring(string, state)
end
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, :utf8]}
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
end
# Charlists with interpolations
defp do_normalize({{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}, state) do
parts =
Enum.map(parts, fn
{{:., part_dot_meta, [Kernel, :to_string]}, part_call_meta, args} ->
args = normalize_args(args, state)
defp do_normalize({{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]} = quoted, state) do
if list_interpolated?(parts) do
parts =
Enum.map(parts, fn
{{:., part_dot_meta, [Kernel, :to_string]}, part_call_meta, args} ->
args = normalize_args(args, state)
{{:., part_dot_meta, [Kernel, :to_string]}, part_call_meta, args}
{{:., part_dot_meta, [Kernel, :to_string]}, part_call_meta, args}
part ->
if state.escape do
maybe_escape_literal(part, state)
else
part
end
end)
part when is_binary(part) ->
if state.escape do
maybe_escape_literal(part, state)
else
part
end
end)
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
else
normalize_call(quoted, state)
end
end
# Don't normalize the `Access` atom in access syntax
@@ -350,7 +362,7 @@ defmodule Code.Normalizer do
normalize_kw_blocks(form, meta, args, state)
allow_keyword?(form, arity) ->
args = normalize_args(args, %{state | parent_meta: state.parent_meta})
args = normalize_args(args, %{state | parent_meta: meta})
{last_arg, leading_args} = List.pop_at(args, -1, [])
last_args =
@@ -372,7 +384,7 @@ defmodule Code.Normalizer do
{form, meta, leading_args ++ last_args}
true ->
args = normalize_args(args, %{state | parent_meta: state.parent_meta})
args = normalize_args(args, %{state | parent_meta: meta})
{form, meta, args}
end
end
@@ -381,11 +393,11 @@ defmodule Code.Normalizer do
defp allow_keyword?(:{}, _), do: false
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
defp normalize_bitstring({:<<>>, meta, parts} = quoted, state, escape_interpolation \\ false) do
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation \\ false) do
meta = patch_meta_line(meta, state.parent_meta)
parts =
if interpolated?(quoted) do
if binary_interpolated?(parts) do
normalize_interpolation_parts(parts, %{state | parent_meta: meta}, escape_interpolation)
else
state = %{state | parent_meta: meta}
@@ -535,8 +547,7 @@ defmodule Code.Normalizer do
term
end
# Check if we have an interpolated string.
defp interpolated?({:<<>>, _, [_ | _] = parts}) do
defp binary_interpolated?(parts) do
Enum.all?(parts, fn
{:"::", _, [{{:., _, [Kernel, :to_string]}, _, [_]}, {:binary, _, _}]} -> true
binary when is_binary(binary) -> true
@@ -544,8 +555,12 @@ defmodule Code.Normalizer do
end)
end
defp interpolated?(_) do
false
defp list_interpolated?(parts) do
Enum.all?(parts, fn
{{:., _, [Kernel, :to_string]}, _, [_]} -> true
binary when is_binary(binary) -> true
_ -> false
end)
end
defp patch_meta_line(meta, parent_meta) do
+3 -3
View File
@@ -2276,13 +2276,13 @@ defmodule Enum do
iex> Enum.split_with([5, 4, 3, 2, 1, 0], fn x -> rem(x, 2) == 0 end)
{[4, 2, 0], [5, 3, 1]}
iex> Enum.split_with(%{a: 1, b: -2, c: 1, d: -3}, fn {_k, v} -> v < 0 end)
iex> Enum.split_with([a: 1, b: -2, c: 1, d: -3], fn {_k, v} -> v < 0 end)
{[b: -2, d: -3], [a: 1, c: 1]}
iex> Enum.split_with(%{a: 1, b: -2, c: 1, d: -3}, fn {_k, v} -> v > 50 end)
iex> Enum.split_with([a: 1, b: -2, c: 1, d: -3], fn {_k, v} -> v > 50 end)
{[], [a: 1, b: -2, c: 1, d: -3]}
iex> Enum.split_with(%{}, fn {_k, v} -> v > 50 end)
iex> Enum.split_with([], fn {_k, v} -> v > 50 end)
{[], []}
"""
+3 -5
View File
@@ -294,13 +294,11 @@ defmodule Exception do
end
end
defp undo_is_struct_guard(
{:and, meta, [_, %{node: {_, _, [{_, _, [_, {struct, _, _}]} | optional]}}]}
) do
defp undo_is_struct_guard({:and, meta, [_, %{node: {_, _, [{_, _, [_, arg]} | optional]}}]}) do
args =
case optional do
[] -> [{struct, meta, nil}]
[module] -> [{struct, meta, nil}, module]
[] -> [arg]
[module] -> [arg, module]
end
%{match?: meta[:value], node: {:is_struct, meta, args}}
+6 -1
View File
@@ -302,7 +302,12 @@ end
defimpl Inspect, for: Map do
def inspect(map, opts) do
list = Map.to_list(map)
list =
if Keyword.get(opts.custom_options, :sort_maps) do
map |> Map.to_list() |> :lists.sort()
else
Map.to_list(map)
end
fun =
if Inspect.List.keyword?(list) do
+5
View File
@@ -31,6 +31,11 @@ defmodule Inspect.Opts do
options. Useful when implementing the `Inspect` protocol for nested structs
to pass the custom options through.
It supports some pre-defined keys:
- `:sort_maps` (since v1.15.0) - if set to `true`, sorts key-value pairs in maps.
This can be helpful to make map inspection deterministic for testing,
especially since key order is random since OTP 26.
* `:inspect_fun` (since v1.9.0) - a function to build algebra documents.
Defaults to `Inspect.Opts.default_inspect_fun/0`.
+3 -1
View File
@@ -78,7 +78,9 @@ defmodule IO.ANSI.Docs do
@metadata_filter [:deprecated, :guard, :since]
defp print_each_metadata(metadata, options) do
Enum.reduce(metadata, false, fn
metadata
|> Enum.sort()
|> Enum.reduce(false, fn
{key, value}, _printed when is_binary(value) and key in @metadata_filter ->
label = metadata_label(key, options)
indent = String.duplicate(" ", length_without_escape(label, 0) + 1)
+40 -26
View File
@@ -3590,33 +3590,31 @@ defmodule Kernel do
defp collect_traces(:after_verify, arg, _env), do: {arg, []}
defp collect_traces(:on_definition, arg, _env), do: {arg, []}
defp collect_traces(_name, arg, env) do
case is_pid(env.lexical_tracker) and Macro.quoted_literal?(arg) do
true ->
env = %{env | function: {:__info__, 1}}
defp collect_traces(_name, arg, %{lexical_tracker: pid} = env) when is_pid(pid) do
env = %{env | function: {:__info__, 1}}
{arg, aliases} =
Macro.prewalk(arg, %{}, fn
{:__aliases__, _, _} = alias, acc ->
case Macro.expand(alias, env) do
atom when is_atom(atom) -> {atom, Map.put(acc, atom, [])}
_ -> {alias, acc}
end
{arg, aliases} =
Macro.expand_literals(arg, %{}, fn
{:__aliases__, _, _} = alias, acc ->
case Macro.expand(alias, env) do
atom when is_atom(atom) -> {atom, Map.put(acc, atom, [])}
_ -> {alias, acc}
end
node, acc ->
{node, acc}
end)
node, acc ->
{Macro.expand(node, env), acc}
end)
case map_size(aliases) do
0 -> {arg, []}
_ -> {arg, [{env.line, env.lexical_tracker, env.tracers, :maps.keys(aliases)}]}
end
false ->
{arg, []}
case map_size(aliases) do
0 -> {arg, []}
_ -> {arg, [{env.line, env.lexical_tracker, env.tracers, :maps.keys(aliases)}]}
end
end
defp collect_traces(_name, arg, _env) do
{arg, []}
end
defp typespec?(:type), do: true
defp typespec?(:typep), do: true
defp typespec?(:opaque), do: true
@@ -4802,11 +4800,27 @@ defmodule Kernel do
:elixir_quote.escape(block, :none, false)
end
module_vars = :lists.map(&module_var/1, :maps.keys(env.versioned_vars))
versioned_vars = env.versioned_vars
prune = :erlang.is_map_key({:elixir, :prune_binding}, versioned_vars)
var_meta =
case prune do
true -> [generated: true, keep_unused: true]
false -> [generated: true]
end
module_vars = :lists.map(&module_var(&1, var_meta), :maps.keys(versioned_vars))
quote do
unquote(with_alias)
:elixir_module.compile(unquote(expanded), unquote(escaped), unquote(module_vars), __ENV__)
:elixir_module.compile(
unquote(expanded),
unquote(escaped),
unquote(module_vars),
unquote(prune),
__ENV__
)
end
end
@@ -4855,8 +4869,8 @@ defmodule Kernel do
{module, module, nil}
end
defp module_var({name, kind}) when is_atom(kind), do: {name, [generated: true], kind}
defp module_var({name, kind}), do: {name, [counter: kind, generated: true], nil}
defp module_var({name, kind}, meta) when is_atom(kind), do: {name, meta, kind}
defp module_var({name, kind}, meta), do: {name, [counter: kind] ++ meta, nil}
@doc ~S"""
Defines a public function with the given name and body.
@@ -5746,7 +5760,7 @@ defmodule Kernel do
"""
defmacro defdelegate(funs, opts) do
funs = Macro.escape(funs, unquote: true)
opts = Macro.expand_literal(opts, %{__CALLER__ | function: {:__info__, 1}})
opts = Macro.expand_literals(opts, %{__CALLER__ | function: {:__info__, 1}})
quote bind_quoted: [funs: funs, opts: opts] do
target = Kernel.Utils.defdelegate_all(funs, opts, __ENV__)
@@ -271,12 +271,6 @@ defmodule Kernel.ParallelCompiler do
for {{:module, module}, _} <- result,
do: module
runtime_modules =
for module <- runtime_modules,
path = :code.which(module),
is_list(path) and path != [],
do: {module, path}
profile_checker(profile, compiled_modules, runtime_modules, fn ->
Module.ParallelChecker.verify(checker, runtime_modules)
end)
+14 -7
View File
@@ -138,10 +138,17 @@ defmodule Kernel.Utils do
fields = :lists.map(mapper, fields)
enforce_keys =
case :ets.take(set, :enforce_keys) do
[{_, enforce_keys, _, _}] when is_list(enforce_keys) -> enforce_keys
[{_, enforce_key, _, _}] -> [enforce_key]
[] -> []
case :ets.lookup(set, :enforce_keys) do
[{_, enforce_keys, _, _}] when is_list(enforce_keys) ->
:ets.update_element(set, :enforce_keys, {3, :used})
enforce_keys
[{_, enforce_key, _, _}] ->
:ets.update_element(set, :enforce_keys, {3, :used})
[enforce_key]
[] ->
[]
end
# TODO: Make it raise on v2.0
@@ -324,7 +331,7 @@ defmodule Kernel.Utils do
quote do
case Macro.Env.in_guard?(__CALLER__) do
true -> unquote(literal_quote(unquote_every_ref(expr, vars)))
false -> unquote(literal_quote(unquote_refs_once(expr, vars)))
false -> unquote(literal_quote(unquote_refs_once(expr, vars, env.module)))
end
end
end
@@ -354,7 +361,7 @@ defmodule Kernel.Utils do
end
# Prefaces `guard` with unquoted versions of `refs`.
defp unquote_refs_once(guard, refs) do
defp unquote_refs_once(guard, refs, module) do
{guard, used_refs} =
Macro.postwalk(guard, %{}, fn
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
@@ -368,7 +375,7 @@ defmodule Kernel.Utils do
%{} ->
generated = String.to_atom("arg" <> Integer.to_string(map_size(acc) + 1))
new_var = Macro.var(generated, Elixir)
new_var = Macro.unique_var(generated, module)
{new_var, Map.put(acc, pair, {new_var, var})}
end
+92 -24
View File
@@ -1763,7 +1763,7 @@ defmodule Macro do
end
"""
@spec expand_once(t(), Macro.Env.t()) :: t()
@spec expand_once(input(), Macro.Env.t()) :: output()
def expand_once(ast, env) do
elem(do_expand_once(ast, env), 0)
end
@@ -1795,13 +1795,15 @@ defmodule Macro do
defp do_expand_once({:__DIR__, _, atom}, env) when is_atom(atom),
do: {:filename.dirname(env.file), true}
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom),
do: {{:%{}, [], Map.to_list(env)}, true}
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) do
env = update_in(env.versioned_vars, &maybe_escape_map/1)
{maybe_escape_map(env), true}
end
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env)
when is_atom(atom) and is_atom(field) do
if Map.has_key?(env, field) do
{Map.get(env, field), true}
{maybe_escape_map(Map.get(env, field)), true}
else
{original, false}
end
@@ -1884,6 +1886,9 @@ defmodule Macro do
# Anything else is just returned
defp do_expand_once(other, _env), do: {other, false}
defp maybe_escape_map(map) when is_map(map), do: {:%{}, [], Map.to_list(map)}
defp maybe_escape_map(other), do: other
@doc """
Returns `true` if the given name and arity is a special form.
"""
@@ -1966,42 +1971,105 @@ defmodule Macro do
def quoted_literal?({:%{}, _, args}), do: quoted_literal?(args)
def quoted_literal?({:{}, _, args}), do: quoted_literal?(args)
def quoted_literal?({:__MODULE__, _, ctx}) when is_atom(ctx), do: true
def quoted_literal?({left, right}), do: quoted_literal?(left) and quoted_literal?(right)
def quoted_literal?(list) when is_list(list), do: :lists.all(&quoted_literal?/1, list)
def quoted_literal?(term), do: is_atom(term) or is_number(term) or is_binary(term)
@doc """
Expands the `ast` representing a quoted literal within the given `env`.
@doc false
@deprecated "Use Macro.expand_literals/2 instead"
def expand_literal(ast, env) do
expand_literals(ast, env)
end
This function checks if the given AST represents a quoted literal
(using `quoted_literal?/1`) and then expands all relevant nodes.
If a literal node is not given, then it returns the AST as is.
At the moment, the only expandable literal nodes in an AST are
aliases, so this function only expands aliases.
@doc """
Expands all literals in `ast` with the given `env`.
This function is mostly used to remove compile-time dependencies
from AST nodes. In such cases, the given environment is usually
manipulate to represent a function:
manipulated to represent a function:
Macro.expand_literal(ast, %{env | function: {:my_code, 1}})
Macro.expand_literals(ast, %{env | function: {:my_code, 1}})
At the moment, the only expandable literal nodes in an AST are
aliases, so this function only expands aliases.
However, be careful when removing compile-time dependencies between
modules. If you remove them but you still invoke the module at
compile-time, you may end-up with broken behaviour.
compile-time, Elixir will be unable to properly recompile modules
when they change.
"""
@doc since: "1.14.0"
@spec expand_literal(t(), Macro.Env.t()) :: t()
def expand_literal(ast, env) do
if quoted_literal?(ast) do
prewalk(ast, fn
{:__aliases__, _, _} = alias -> expand(alias, env)
other -> other
end)
@doc since: "1.14.1"
@spec expand_literals(input(), Macro.Env.t()) :: output()
def expand_literals(ast, env) do
{ast, :ok} = expand_literals(ast, :ok, fn node, :ok -> {expand(node, env), :ok} end)
ast
end
@doc """
Expands all literals in `ast` with the given `acc` and `fun`.
`fun` will be invoked with an expandable AST node and `acc` and
must return a new node with `acc`. This is a general version of
`expand_literals/2` which supports a custom expansion function.
Please check `expand_literals/2` for use cases and pitfalls.
"""
@doc since: "1.14.1"
@spec expand_literals(t(), acc, (t(), acc -> {t(), acc})) :: t() when acc: term()
def expand_literals(ast, acc, fun)
def expand_literals({:__aliases__, meta, args}, acc, fun) do
{args, acc} = expand_literals(args, acc, fun)
if :lists.all(&is_atom/1, args) do
fun.({:__aliases__, meta, args}, acc)
else
ast
{{:__aliases__, meta, args}, acc}
end
end
def expand_literals({:__MODULE__, _meta, ctx} = node, acc, fun) when is_atom(ctx) do
fun.(node, acc)
end
def expand_literals({:%, meta, [left, right]}, acc, fun) do
{left, acc} = expand_literals(left, acc, fun)
{right, acc} = expand_literals(right, acc, fun)
{{:%, meta, [left, right]}, acc}
end
def expand_literals({:%{}, meta, args}, acc, fun) do
{args, acc} = expand_literals(args, acc, fun)
{{:%{}, meta, args}, acc}
end
def expand_literals({:{}, meta, args}, acc, fun) do
{args, acc} = expand_literals(args, acc, fun)
{{:{}, meta, args}, acc}
end
def expand_literals({left, right}, acc, fun) do
{left, acc} = expand_literals(left, acc, fun)
{right, acc} = expand_literals(right, acc, fun)
{{left, right}, acc}
end
def expand_literals(list, acc, fun) when is_list(list) do
:lists.mapfoldl(&expand_literals(&1, &2, fun), acc, list)
end
def expand_literals(
{{:., _, [{:__aliases__, _, [:Application]}, :compile_env]} = node, meta,
[app, key, default]},
acc,
fun
) do
{default, acc} = expand_literals(default, acc, fun)
{{node, meta, [app, key, default]}, acc}
end
def expand_literals(term, acc, _fun), do: {term, acc}
@doc """
Receives an AST node and expands it until it can no longer
be expanded.
@@ -2012,7 +2080,7 @@ defmodule Macro do
This function uses `expand_once/2` under the hood. Check
it out for more information and examples.
"""
@spec expand(t(), Macro.Env.t()) :: t()
@spec expand(input(), Macro.Env.t()) :: output()
def expand(ast, env) do
expand_until({ast, true}, env)
end
+6 -2
View File
@@ -532,7 +532,11 @@ defmodule Module do
corresponding setting in `Code.get_compiler_option/1`
* `@compile {:debug_info, false}` - disables `:debug_info` regardless
of the corresponding setting in `Code.get_compiler_option/1`
of the corresponding setting in `Code.get_compiler_option/1`. Note
disabling `:debug_info` is not recommended as it removes the ability
of the Elixir compiler and other tools to static analyse the code.
If you want to remove the `:debug_info` while deploying, tools like
`mix release` already do such by default.
* `@compile {:inline, some_fun: 2, other_fun: 3}` - inlines the given
name/arity pairs. Inlining is applied locally, calls from another
@@ -818,7 +822,7 @@ defmodule Module do
next = :elixir_module.next_counter(nil)
meta = Keyword.take(opts, [:line, :generated])
quoted = :elixir_quote.linify_with_context_counter(meta, {module, next}, quoted)
:elixir_module.compile(module, quoted, [], :elixir.env_for_eval(opts))
:elixir_module.compile(module, quoted, [], false, :elixir.env_for_eval(opts))
end
@doc """
+27 -13
View File
@@ -62,17 +62,27 @@ defmodule Module.ParallelChecker do
if is_map(info) do
info
else
info |> File.read!() |> fetch_module_map!(module)
case File.read(info) do
{:ok, binary} -> maybe_module_map(binary, module)
{:error, _} -> nil
end
end
cache_from_module_map(ets, module_map)
module_map && cache_from_module_map(ets, module_map)
send(checker, {ref, :cached})
receive do
{^ref, :check} ->
# Set the compiler info so we can collect warnings
:erlang.put(:elixir_compiler_info, {pid, self()})
warnings = check_module(module_map, {checker, ets})
warnings =
if module_map do
check_module(module_map, {checker, ets})
else
[]
end
send(pid, {__MODULE__, module, warnings})
send(checker, {__MODULE__, :done})
end
@@ -129,13 +139,13 @@ defmodule Module.ParallelChecker do
the modules and adds the ExCk chunk to the binaries. Returns the updated
list of warnings from the verification.
"""
@spec verify(pid(), [{module(), binary()}]) :: [warning()]
@spec verify(pid(), [{module(), Path.t()}]) :: [warning()]
def verify(checker, runtime_files) do
for {module, file} <- runtime_files do
spawn({self(), checker}, module, file)
end
modules = :gen_server.call(checker, :start)
modules = :gen_server.call(checker, :start, :infinity)
collect_results(modules, [])
end
@@ -161,7 +171,7 @@ defmodule Module.ParallelChecker do
"""
def test_cache do
{:ok, checker} = start_link()
{checker, :gen_server.call(checker, :ets)}
{checker, :gen_server.call(checker, :ets, :infinity)}
end
@doc """
@@ -317,8 +327,7 @@ defmodule Module.ParallelChecker do
defp cache_from_chunk(ets, module) do
with {^module, binary, _filename} <- :code.get_object_code(module),
{:ok, ^module} <- binary |> :beam_lib.info() |> Keyword.fetch(:module),
{:ok, {_, [{'ExCk', chunk}]}} <- :beam_lib.chunks(binary, ['ExCk']),
{:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
cache_chunk(ets, module, contents.exports)
true
@@ -357,11 +366,16 @@ defmodule Module.ParallelChecker do
_ -> %{}
end
defp fetch_module_map!(binary, module) when is_binary(binary) do
{:ok, {_, [debug_info: chunk]}} = :beam_lib.chunks(binary, [:debug_info])
{:debug_info_v1, backend, data} = chunk
{:ok, module_map} = backend.debug_info(:elixir_v1, module, data, [])
module_map
defp maybe_module_map(binary, module) when is_binary(binary) do
# If a module was compiled without debug_info,
# then there is no module_map for further verification.
with {:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
{:debug_info_v1, backend, data} = chunk,
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
module_map
else
_ -> nil
end
end
defp cache_from_module_map(ets, map) do
+16 -12
View File
@@ -135,8 +135,7 @@ defmodule Module.Types do
# Collect relevant information from context and traces to report error
def error_to_warning(:unable_apply, {mfa, args, expected, signature, stack}, context) do
{fun, arity} = context.function
line = get_meta(stack.last_expr)[:line]
location = {context.file, line, {context.module, fun, arity}}
location = {context.file, get_line(stack), {context.module, fun, arity}}
traces = type_traces(stack, context)
{[signature | args], traces} = lift_all_types([signature | args], traces, context)
@@ -146,8 +145,7 @@ defmodule Module.Types do
def error_to_warning(:unable_unify, {left, right, stack}, context) do
{fun, arity} = context.function
line = get_meta(stack.last_expr)[:line]
location = {context.file, line, {context.module, fun, arity}}
location = {context.file, get_line(stack), {context.module, fun, arity}}
traces = type_traces(stack, context)
{[left, right], traces} = lift_all_types([left, right], traces, context)
@@ -155,6 +153,8 @@ defmodule Module.Types do
{Module.Types, error, location}
end
defp get_line(stack), do: stack.last_expr |> get_meta() |> Keyword.get(:line, 0)
# Collect relevant traces from context.traces using stack.unify_stack
defp type_traces(stack, context) do
# TODO: Do we need the unify_stack or is enough to only get the last variable
@@ -211,17 +211,21 @@ defmodule Module.Types do
## FORMAT WARNINGS
def format_warning({:unable_apply, mfa, args, expected, signature, {location, expr, traces}}) do
{module, function, arity} = mfa
mfa_args = Macro.generate_arguments(arity, __MODULE__)
{module, function, ^arity} = call_to_mfa(erl_to_ex(module, function, mfa_args, []))
{original_module, original_function, arity} = mfa
{_, _, args} = mfa_or_fa = erl_to_ex(original_module, original_function, args, [])
{module, function, ^arity} = call_to_mfa(mfa_or_fa)
format_mfa = Exception.format_mfa(module, function, arity)
{traces, [] = _hints} = format_traces(traces, [], false)
clauses =
Enum.map(
signature,
&String.slice(IO.iodata_to_binary(Unify.format_type({:fun, [&1]}, false)), 1..-2)
)
Enum.map(signature, fn {ins, out} ->
{_, _, ins} = erl_to_ex(original_module, original_function, ins, [])
{:fun, [{ins, out}]}
|> Unify.format_type(false)
|> IO.iodata_to_binary()
|> binary_slice(1..-2//1)
end)
[
"expected #{format_mfa} to have signature:\n\n ",
@@ -248,7 +252,7 @@ defmodule Module.Types do
"undefined field \"#{atom}\" ",
format_expr(expr, location),
"expected one of the following fields: ",
Enum.map_join(Enum.sort(known_atoms), ", ", & &1),
Enum.join(Enum.sort(known_atoms), ", "),
"\n\n",
traces,
format_message_hints(hints),
+10 -6
View File
@@ -153,14 +153,18 @@ defmodule Module.Types.Expr do
args_type = {:map, dynamic_value_pairs ++ [{:optional, :dynamic, :dynamic}]},
{:ok, type, context} <- unify(map_type, args_type, stack, context) do
# Retrieve map type and overwrite with the new value types from the map update
{:map, pairs} = resolve_var(type, context)
case resolve_var(type, context) do
{:map, pairs} ->
updated_pairs =
Enum.reduce(arg_pairs, pairs, fn {:required, key, value}, pairs ->
List.keyreplace(pairs, key, 1, {:required, key, value})
end)
updated_pairs =
Enum.reduce(arg_pairs, pairs, fn {:required, key, value}, pairs ->
List.keyreplace(pairs, key, 1, {:required, key, value})
end)
{:ok, {:map, updated_pairs}, context}
{:ok, {:map, updated_pairs}, context}
_ ->
{:ok, :dynamic, context}
end
end
end
+6 -31
View File
@@ -128,35 +128,10 @@ defmodule Module.Types.Unify do
{:ok, {:var, var}, context}
end
%{^var => {:var, new_var} = var_type} ->
unify_result =
cond do
recursive_type?(var_type, [], context) ->
{:ok, var_type, put_in(context.types[var], var_type)}
var_source? ->
unify(var_type, type, stack, context)
true ->
unify(type, var_type, stack, context)
end
case unify_result do
{:ok, type, context} ->
{:ok, type, context}
{:error, {type, reason, %{traces: error_traces} = error_context}} ->
old_var_traces = Map.get(context.traces, new_var, [])
new_var_traces = Map.get(error_traces, new_var, [])
add_var_traces = Enum.drop(new_var_traces, -length(old_var_traces))
error_traces =
error_traces
|> Map.update(var, add_var_traces, &(add_var_traces ++ &1))
|> Map.put(new_var, old_var_traces)
{:error, {type, reason, %{error_context | traces: error_traces}}}
end
%{^var => {:var, _} = var_type} ->
# Do not recursively traverse type vars for now
# to avoid pathological cases related to performance.
{:ok, var_type, context}
%{^var => var_type} ->
# Only add trace if the variable wasn't already "expanded"
@@ -384,11 +359,11 @@ defmodule Module.Types.Unify do
end
@doc """
Resolves a variable raising if it is unbound.
Maybe resolves a variable.
"""
def resolve_var({:var, var}, context) do
case context.types do
%{^var => :unbound} -> raise "cannot resolve unbound var"
%{^var => :unbound} -> {:var, var}
%{^var => type} -> resolve_var(type, context)
end
end
+4
View File
@@ -603,6 +603,10 @@ defmodule Path do
:file.read_link_info(file)
end
def read_file_info(file) do
:file.read_file_info(file)
end
def list_dir(dir) do
case :file.list_dir(dir) do
{:ok, files} -> {:ok, for(file <- files, hd(file) != ?., do: file)}
+2 -1
View File
@@ -931,7 +931,8 @@ defmodule Protocol do
raise ArgumentError, "defimpl/3 expects a :for option when declared outside a module"
end)
for = Macro.expand_literal(for, %{env | module: Kernel, function: {:defimpl, 3}})
for =
Macro.expand_literals(for, %{env | module: env.module || Elixir, function: {:__impl__, 1}})
case opts do
[] -> raise ArgumentError, "defimpl expects a do-end block"
+32 -28
View File
@@ -24,7 +24,7 @@ defmodule Range do
When defining a range without a step, the step will be
defined based on the first and last position of the
range, If `first >= last`, it will be an increasing range
range, If `last >= first`, it will be an increasing range
with a step of 1. Otherwise, it is a decreasing range.
Note however implicit decreasing ranges are deprecated.
Therefore, if you need a decreasing range from `3` to `1`,
@@ -294,22 +294,24 @@ defmodule Range do
iex> Range.disjoint?(1..10//2, 2..10//2)
true
# First element in common in all below is 29
iex> Range.disjoint?(2..100//3, 9..100//5)
false
iex> Range.disjoint?(101..2//-3, 99..9//-5)
false
# First element in common is 29
iex> Range.disjoint?(1..100//14, 8..100//21)
false
iex> Range.disjoint?(57..-1//-14, 8..100//21)
false
iex> Range.disjoint?(1..100//14, 51..8//-21)
iex> Range.disjoint?(1..100//14, 50..8//-21)
false
# If 29 is out of range
iex> Range.disjoint?(1..28//14, 8..28//21)
true
# First element in common is 14
iex> Range.disjoint?(2..28//3, 9..28//5)
false
iex> Range.disjoint?(26..2//-3, 29..9//-5)
false
# Starting from the back without alignment
iex> Range.disjoint?(27..11//-3, 30..0//-7)
true
"""
@@ -334,21 +336,28 @@ defmodule Range do
# progressions and see if they belong within the ranges
# https://math.stackexchange.com/questions/1656120/formula-to-find-the-first-intersection-of-two-arithmetic-progressions
{gcd, u, v} = Integer.extended_gcd(-step1, step2)
c = first1 - first2 + step2 - step1
t1 = -c / step1 * u
t2 = -c / step2 * v
t = max(floor(t1) + 1, floor(t2) + 1)
x = div(c * u + t * step2, gcd) - 1
y = div(c * v + t * step1, gcd) - 1
x < 0 or first1 + x * step1 > last1 or
y < 0 or first2 + y * step2 > last2
if rem(first2 - first1, gcd) == 0 do
c = first1 - first2 + step2 - step1
t1 = -c / step2 * u
t2 = -c / step1 * v
t = max(floor(t1) + 1, floor(t2) + 1)
x = div(c * u + t * step2, gcd) - 1
y = div(c * v + t * step1, gcd) - 1
x < 0 or first1 + x * step1 > last1 or
y < 0 or first2 + y * step2 > last2
else
true
end
end
end
end
@compile inline: [normalize: 3]
defp normalize(first, last, step) when first > last, do: {last, first, -step}
defp normalize(first, last, step) when first > last,
do: {first - abs(div(first - last, step) * step), first, -step}
defp normalize(first, last, step), do: {first, last, step}
@doc false
@@ -387,16 +396,11 @@ defimpl Enumerable, for: Range do
{:done, acc}
end
def member?(first..last//step = range, value) when is_integer(value) do
cond do
Range.size(range) == 0 ->
{:ok, false}
first <= last ->
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
true ->
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
def member?(first..last//step, value) when is_integer(value) do
if step > 0 do
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
else
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
end
end
+2
View File
@@ -1315,6 +1315,8 @@ defmodule Stream do
{:suspended, acc, &zip_list(enumerables, &1, fun, zip_fun)}
end
defp zip_list([], {:cont, acc}, _fun, _zip_fun), do: {:done, acc}
defp zip_list(enumerables, {:cont, acc}, fun, zip_fun) do
case zip_list_heads_tails(enumerables, [], []) do
{heads, tails} -> zip_list(tails, fun.(zip_fun.(heads), acc), fun, zip_fun)
+3 -2
View File
@@ -524,11 +524,11 @@ defmodule String do
defp parts_to_index(n) when is_integer(n) and n > 0, do: n
defp split_empty("", true, 1), do: []
defp split_empty(string, _, 1), do: [string]
defp split_empty(string, _, 1), do: [IO.iodata_to_binary(string)]
defp split_empty(string, trim, count) do
case :unicode_util.gc(string) do
[gc] -> [grapheme_to_binary(gc) | split_empty(<<>>, trim, count - 1)]
[gc] -> [grapheme_to_binary(gc) | split_empty("", trim, 1)]
[gc | rest] -> [grapheme_to_binary(gc) | split_empty(rest, trim, count - 1)]
[] -> split_empty("", trim, 1)
{:error, <<byte, rest::bits>>} -> [<<byte>> | split_empty(rest, trim, count - 1)]
@@ -1930,6 +1930,7 @@ defmodule String do
def next_grapheme(string) when is_binary(string) do
case :unicode_util.gc(string) do
[gc] -> {grapheme_to_binary(gc), <<>>}
[gc, rest] -> {grapheme_to_binary(gc), rest}
[gc | rest] -> {grapheme_to_binary(gc), rest}
[] -> nil
{:error, <<byte, rest::bits>>} -> {<<byte>>, rest}
+2 -1
View File
@@ -366,6 +366,7 @@ defmodule StringIO do
defp get_until(encoding, prompt, mod, fun, args, %{input: input} = state) do
case get_until(input, encoding, mod, fun, args, [], 0) do
{result, input, count} ->
# Convert :eof to "" as they are both treated the same
input =
case input do
:eof -> ""
@@ -392,7 +393,7 @@ defmodule StringIO do
defp get_until(chars, encoding, mod, fun, args, continuation, count) do
case bytes_until_eol(chars, encoding, 0) do
{kind, size} when kind in [:split, :replace_split] ->
{line, rest} = :erlang.split_binary(chars, size)
<<line::binary-size(size), rest::binary>> = chars
case apply(mod, fun, [continuation, binary_to_list(line, encoding) | args]) do
{:done, result, :eof} ->
+1 -1
View File
@@ -82,7 +82,7 @@ defmodule Supervisor do
GenServer.call(Counter, :get)
#=> 0
GenServer.cast(Counter, {:bump, 3})
GenServer.call(Counter, {:bump, 3})
#=> 0
GenServer.call(Counter, :get)
+10 -3
View File
@@ -920,19 +920,26 @@ defmodule System do
## Options
It accepts the same options as `cmd/3`, except for `arg0`.
It accepts the same options as `cmd/3` (except for `arg0`).
It also accepts the following exclusive options:
* `:close_stdin` (since v1.14.1) - if the stdin should be closed
on Unix systems, forcing any command that waits on stdin to
immediately terminate. Defaults to false.
"""
@doc since: "1.12.0"
@spec shell(binary, keyword) :: {Collectable.t(), exit_status :: non_neg_integer}
def shell(command, opts \\ []) when is_binary(command) do
assert_no_null_byte!(command, "System.shell/2")
{close_stdin?, opts} = Keyword.pop(opts, :close_stdin, false)
# Finding shell command logic from :os.cmd in OTP
# https://github.com/erlang/otp/blob/8deb96fb1d017307e22d2ab88968b9ef9f1b71d0/lib/kernel/src/os.erl#L184
case :os.type() do
{:unix, _} ->
shell_path = :os.find_executable('sh') || :erlang.error(:enoent, [command, opts])
command = "(#{command}\n) </dev/null"
shell_path = :os.find_executable(~c"sh") || :erlang.error(:enoent, [command, opts])
close_stdin = if close_stdin?, do: " </dev/null", else: ""
command = IO.iodata_to_binary(["(", command, "\n)", close_stdin])
do_cmd({:spawn_executable, shell_path}, [args: ["-c", command]], opts)
{:win32, osname} ->
+1 -1
View File
@@ -648,7 +648,7 @@ defmodule URI do
scheme = String.downcase(scheme, :ascii)
case map do
%{port: _} ->
%{port: port} when port != :undefined ->
%{uri | scheme: scheme}
%{} ->
@@ -42,7 +42,7 @@ Erlang/OTP versioning is independent from the versioning of Elixir. Erlang relea
Elixir version | Supported Erlang/OTP versions
:------------- | :-------------------------------
1.14 | 23 - 25
1.14 | 23 - 25 (and Erlang/OTP 26 from v1.14.5)
1.13 | 22 - 24 (and Erlang/OTP 25 from v1.13.4)
1.12 | 22 - 24
1.11 | 21 - 23 (and Erlang/OTP 24 from v1.11.4)
+19
View File
@@ -0,0 +1,19 @@
# Generate docs_config.js for version chooser in ExDoc
[app] = System.argv()
{text_tags, 0} = System.cmd("git", ["tag"])
skipped = Version.parse!("1.0.3")
list_contents =
text_tags
|> String.split()
|> Enum.map(fn "v" <> rest -> Version.parse!(rest) end)
|> Enum.filter(&(Version.compare(&1, skipped) == :gt))
|> Enum.sort({:desc, Version})
|> Enum.map_intersperse(", ", fn version ->
version_string = Version.to_string(version)
~s[{"version":"v#{version_string}", "url":"https://hexdocs.pm/#{app}/#{version_string}"}]
end)
File.mkdir_p!("doc/#{app}")
File.write!("doc/#{app}/docs_config.js", ["var versionNodes = [", list_contents, "];\n"])
+38 -23
View File
@@ -4,7 +4,8 @@
-behaviour(application).
-export([start_cli/0,
string_to_tokens/5, tokens_to_quoted/3, 'string_to_quoted!'/5,
env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_quoted/3]).
env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_quoted/3,
eval_quoted/4]).
-include("elixir.hrl").
-define(system, 'Elixir.System').
@@ -24,9 +25,9 @@
-export([start/2, stop/1, config_change/3]).
start(_Type, _Args) ->
_ = parse_otp_release(),
OTP = parse_otp_release(),
preload_common_modules(),
set_stdio_and_stderr_to_binary_and_maybe_utf8(),
set_stdio_and_stderr_to_binary_and_maybe_utf8(OTP),
check_file_encoding(file:native_name_encoding()),
case application:get_env(elixir, check_endianness, true) of
@@ -86,7 +87,10 @@ stop(Tab) ->
config_change(_Changed, _New, _Remove) ->
ok.
set_stdio_and_stderr_to_binary_and_maybe_utf8() ->
set_stdio_and_stderr_to_binary_and_maybe_utf8(OTP) when OTP >= 26 ->
ok = io:setopts(standard_io, [binary]),
ok;
set_stdio_and_stderr_to_binary_and_maybe_utf8(_OTP) ->
%% In case there is a shell, we can't really change its
%% encoding, so we just set binary to true. Otherwise
%% we must set the encoding as the user with no shell
@@ -260,22 +264,27 @@ env_for_eval(Opts) when is_list(Opts) ->
%% Quoted evaluation
eval_quoted(Tree, Binding, Opts) when is_list(Opts) ->
eval_quoted(Tree, Binding, env_for_eval(Opts));
eval_quoted(Tree, Binding, #{line := Line} = E) ->
eval_forms(elixir_quote:linify(Line, line, Tree), Binding, E).
eval_quoted(Tree, Binding, E) ->
eval_quoted(Tree, Binding, E, []).
eval_quoted(Tree, Binding, #{line := Line} = E, Opts) ->
eval_forms(elixir_quote:linify(Line, line, Tree), Binding, E, Opts).
eval_forms(Tree, Binding, Opts) when is_list(Opts) ->
eval_forms(Tree, Binding, env_for_eval(Opts));
eval_forms(Tree, Binding, OrigE) ->
{ExVars, ErlVars, ErlBinding} = elixir_erl_var:load_binding(Binding),
eval_forms(Tree, Binding, OrigE, []).
eval_forms(Tree, Binding, OrigE, Opts) ->
Prune = proplists:get_value(prune_binding, Opts, false),
{ExVars, ErlVars, ErlBinding} = elixir_erl_var:load_binding(Binding, Prune),
E = elixir_env:with_vars(OrigE, ExVars),
S = elixir_erl_var:from_env(E, ErlVars),
{Erl, NewErlS, NewExS, NewE} = quoted_to_erl(Tree, E, S),
ExS = elixir_env:env_to_ex(E),
ErlS = elixir_erl_var:from_env(E, ErlVars),
{Erl, NewErlS, NewExS, NewE} = quoted_to_erl(Tree, ErlS, ExS, E),
case Erl of
{atom, _, Atom} ->
{Atom, Binding, NewE};
{Literal, _, Value} when Literal == atom; Literal == float; Literal == integer ->
if
Prune -> {Value, [], NewE#{versioned_vars := #{}}};
true -> {Value, Binding, NewE}
end;
_ ->
Exprs =
@@ -286,7 +295,12 @@ eval_forms(Tree, Binding, OrigE) ->
ExternalHandler = eval_external_handler(NewE),
{value, Value, NewBinding} = erl_eval:exprs(Exprs, ErlBinding, none, ExternalHandler),
{Value, elixir_erl_var:dump_binding(NewBinding, NewExS, NewErlS), NewE}
PruneBefore = if Prune -> length(Binding); true -> -1 end,
{DumpedBinding, DumpedVars} =
elixir_erl_var:dump_binding(NewBinding, NewErlS, NewExS, PruneBefore),
{Value, DumpedBinding, NewE#{versioned_vars := DumpedVars}}
end.
%% TODO: Remove conditional once we require Erlang/OTP 25+.
@@ -356,14 +370,15 @@ eval_external_handler(_Env) ->
%% Converts a quoted expression to Erlang abstract format
quoted_to_erl(Quoted, E) ->
{_, S} = elixir_erl_var:from_env(E),
quoted_to_erl(Quoted, E, S).
{_, ErlS} = elixir_erl_var:from_env(E),
ExS = elixir_env:env_to_ex(E),
quoted_to_erl(Quoted, ErlS, ExS, E).
quoted_to_erl(Quoted, Env, Scope) ->
{Expanded, #elixir_ex{vars={ReadVars, _}} = NewExS, NewEnv} =
elixir_expand:expand(Quoted, elixir_env:env_to_ex(Env), Env),
{Erl, NewErlS} = elixir_erl_pass:translate(Expanded, erl_anno:new(?key(Env, line)), Scope),
{Erl, NewErlS, NewExS, NewEnv#{versioned_vars := ReadVars}}.
quoted_to_erl(Quoted, ErlS, ExS, Env) ->
{Expanded, NewExS, NewEnv} =
elixir_expand:expand(Quoted, ExS, Env),
{Erl, NewErlS} = elixir_erl_pass:translate(Expanded, erl_anno:new(?key(Env, line)), ErlS),
{Erl, NewErlS, NewExS, NewEnv}.
%% Converts a given string (charlist) into quote expression
+1 -1
View File
@@ -20,7 +20,7 @@
'MACRO-defmodule'(_Caller, Alias, [{do, Block}]) ->
Escaped = elixir_quote:escape(Block, none, false),
Args = [Alias, Escaped, [], env()],
Args = [Alias, Escaped, [], false, env()],
{{'.', [], [elixir_module, compile]}, [], Args}.
'__info__'(functions) ->
+4 -3
View File
@@ -37,7 +37,8 @@ eval_or_compile(Forms, Args, E) ->
(not elixir_config:is_bootstrap()) of
true -> fast_compile(Forms, E);
false -> compile(Forms, Args, E)
end.
end,
ok.
compile(Quoted, ArgsList, E) ->
{Expanded, SE, EE} = elixir_expand:expand(Quoted, elixir_env:env_to_ex(E), E),
@@ -47,7 +48,7 @@ compile(Quoted, ArgsList, E) ->
elixir_erl_compiler:spawn(fun() -> spawned_compile(Expanded, E) end),
Args = list_to_tuple(ArgsList),
{dispatch(Module, Fun, Args, Purgeable), EE}.
{dispatch(Module, Fun, Args, Purgeable), SE, EE}.
spawned_compile(ExExprs, #{line := Line, file := File} = E) ->
{Vars, S} = elixir_erl_var:from_env(E),
@@ -126,7 +127,7 @@ fast_compile({defmodule, Meta, [Mod, [{do, TailBlock}]]}, NoLineE) ->
end,
ContextModules = [Expanded | ?key(E, context_modules)],
elixir_module:compile(Expanded, Block, [], E#{context_modules := ContextModules}).
elixir_module:compile(Expanded, Block, [], false, E#{context_modules := ContextModules}).
%% Bootstrapper
+5 -7
View File
@@ -83,7 +83,7 @@ reset_unused_vars(#elixir_ex{unused={_Unused, Version}} = S) ->
check_unused_vars(#elixir_ex{unused={Unused, _Version}}, E) ->
[elixir_errors:form_warn([{line, Line}], E, ?MODULE, {unused_var, Name, Overridden}) ||
{{Name, _}, {Line, Overridden}} <- maps:to_list(Unused), is_unused_var(Name)],
{{{Name, nil}, _}, {Line, Overridden}} <- maps:to_list(Unused), is_unused_var(Name)],
E.
merge_and_check_unused_vars(S, #elixir_ex{vars={Read, Write}, unused={Unused, _Version}}, E) ->
@@ -93,20 +93,18 @@ merge_and_check_unused_vars(S, #elixir_ex{vars={Read, Write}, unused={Unused, _V
merge_and_check_unused_vars(Current, Unused, ClauseUnused, E) ->
maps:fold(fun
({Name, Count} = Key, false, Acc) ->
Var = {Name, nil},
%% The parent knows it, so we have to propagate it was used up.
({Var, Count} = Key, false, Acc) ->
case Current of
#{Var := CurrentCount} when Count =< CurrentCount ->
%% The parent knows it, so we have to propagate it was used up.
Acc#{Key => false};
#{} ->
Acc
end;
({Name, _Count}, {Line, Overridden}, Acc) ->
case is_unused_var(Name) of
({{Name, Kind}, _Count}, {Line, Overridden}, Acc) ->
case (Kind == nil) andalso is_unused_var(Name) of
true ->
Warn = {unused_var, Name, Overridden},
elixir_errors:form_warn([{line, Line}], E, ?MODULE, Warn);
+3 -2
View File
@@ -24,7 +24,7 @@ debug_info(core_v1, _Module, {elixir_v1, Map, Specs}, Opts) ->
%% warnings nor the other functionality provided there.
case elixir_erl_compiler:erl_to_core(Prefix ++ Specs ++ Forms, AllOpts) of
{ok, CoreForms, _} ->
try compile:noenv_forms(CoreForms, [no_spawn_compiler_process, from_core, core, return | AllOpts]) of
try compile:noenv_forms(CoreForms, [no_spawn_compiler_process, from_core, to_core, return | AllOpts]) of
{ok, _, Core, _} -> {ok, Core};
_What -> {error, failed_conversion}
catch
@@ -65,7 +65,8 @@ elixir_to_erl([], Ann) ->
elixir_to_erl(<<>>, Ann) ->
{bin, Ann, []};
elixir_to_erl(#{} = Map, Ann) ->
Assocs = [{map_field_assoc, Ann, elixir_to_erl(K, Ann), elixir_to_erl(V, Ann)} || {K, V} <- maps:to_list(Map)],
Assocs = [{map_field_assoc, Ann, elixir_to_erl(K, Ann), elixir_to_erl(V, Ann)}
|| {K, V} <- lists:sort(maps:to_list(Map))],
{map, Ann, Assocs};
elixir_to_erl(Tree, Ann) when is_list(Tree) ->
elixir_to_erl_cons(Tree, Ann);
+4 -5
View File
@@ -1,13 +1,13 @@
-module(elixir_erl_for).
-export([translate/4]).
-export([translate/3]).
-include("elixir.hrl").
translate(Meta, Args, Return, S) ->
translate(Meta, Args, S) ->
{Cases, [{do, Expr} | Opts]} = elixir_utils:split_last(Args),
case lists:keyfind(reduce, 1, Opts) of
{reduce, Reduce} -> translate_reduce(Meta, Cases, Expr, Reduce, S);
false -> translate_into(Meta, Cases, Expr, Opts, Return, S)
false -> translate_into(Meta, Cases, Expr, Opts, S)
end.
translate_reduce(Meta, Cases, Expr, Reduce, S) ->
@@ -23,13 +23,12 @@ translate_reduce(Meta, Cases, Expr, Reduce, S) ->
build_reduce(Ann, TCases, InnerFun, TExpr, TReduce, false, SE).
translate_into(Meta, Cases, Expr, Opts, Return, S) ->
translate_into(Meta, Cases, Expr, Opts, S) ->
Ann = ?ann(Meta),
{TInto, SI} =
case lists:keyfind(into, 1, Opts) of
{into, Into} -> elixir_erl_pass:translate(Into, Ann, S);
false when Return -> {{nil, Ann}, S};
false -> {false, S}
end,
+1 -7
View File
@@ -182,7 +182,7 @@ translate({'receive', Meta, [Opts]}, _Ann, S) ->
%% Comprehensions and with
translate({for, Meta, [_ | _] = Args}, _Ann, S) ->
elixir_erl_for:translate(Meta, Args, true, S);
elixir_erl_for:translate(Meta, Args, S);
translate({with, Meta, [_ | _] = Args}, _Ann, S) ->
{Exprs, [{do, Do} | Opts]} = elixir_utils:split_last(Args),
@@ -348,12 +348,6 @@ translate_block([H], Ann, Acc, S) ->
translate_block([{'__block__', Meta, Args} | T], Ann, Acc, S) when is_list(Args) ->
{TAcc, SA} = translate_block(Args, ?ann(Meta), Acc, S),
translate_block(T, Ann, TAcc, SA);
translate_block([{for, Meta, [_ | _] = Args} | T], Ann, Acc, S) ->
{TH, TS} = elixir_erl_for:translate(Meta, Args, false, S),
translate_block(T, Ann, [TH | Acc], TS);
translate_block([{'=', _, [{'_', _, Ctx}, {for, Meta, [_ | _] = Args}]} | T], Ann, Acc, S) when is_atom(Ctx) ->
{TH, TS} = elixir_erl_for:translate(Meta, Args, false, S),
translate_block(T, Ann, [TH | Acc], TS);
translate_block([H | T], Ann, Acc, S) ->
{TH, TS} = translate(H, Ann, S),
translate_block(T, Ann, [TH | Acc], TS).
+24 -11
View File
@@ -2,7 +2,7 @@
-module(elixir_erl_var).
-export([
translate/4, assign/2, build/2,
load_binding/1, dump_binding/3,
load_binding/2, dump_binding/4,
from_env/1, from_env/2
]).
-include("elixir.hrl").
@@ -65,16 +65,16 @@ to_erl_vars([], _Counter) ->
to_erl_var(Counter) ->
list_to_atom("_@" ++ integer_to_list(Counter)).
load_binding(Binding) ->
load_binding(Binding, #{}, [], [], 0).
load_binding(Binding, Prune) ->
load_binding(Binding, #{}, [], [], 0, Prune).
load_binding([Binding | NextBindings], ExVars, ErlVars, Normalized, Counter) ->
load_binding([Binding | NextBindings], ExVars, ErlVars, Normalized, Counter, Prune) ->
{Pair, Value} = load_pair(Binding),
case ExVars of
#{Pair := VarCounter} ->
ErlVar = to_erl_var(VarCounter),
load_binding(NextBindings, ExVars, ErlVars, [{ErlVar, Value} | Normalized], Counter);
load_binding(NextBindings, ExVars, ErlVars, [{ErlVar, Value} | Normalized], Counter, Prune);
#{} ->
ErlVar = to_erl_var(Counter),
@@ -84,10 +84,13 @@ load_binding([Binding | NextBindings], ExVars, ErlVars, Normalized, Counter) ->
ExVars#{Pair => Counter},
[{Counter, ErlVar} | ErlVars],
[{ErlVar, Value} | Normalized],
Counter + 1
Counter + 1,
Prune
)
end;
load_binding([], ExVars, ErlVars, Normalized, _Counter) ->
load_binding([], ExVars, ErlVars, Normalized, Counter, true) ->
load_binding([{{elixir, prune_binding}, true}], ExVars, ErlVars, Normalized, Counter, false);
load_binding([], ExVars, ErlVars, Normalized, _Counter, false) ->
%% TODO: Remove me once we require Erlang/OTP 24+
%% Also revisit dump_binding below and remove the vars field for simplicity.
Mod =
@@ -101,9 +104,16 @@ load_binding([], ExVars, ErlVars, Normalized, _Counter) ->
load_pair({Key, Value}) when is_atom(Key) -> {{Key, nil}, Value};
load_pair({Pair, Value}) -> {Pair, Value}.
dump_binding(Binding, #elixir_ex{vars={ExVars, _}}, #elixir_erl{var_names=ErlVars}) ->
dump_binding(Binding, ErlS, ExS, PruneBefore) ->
#elixir_erl{var_names=ErlVars} = ErlS,
#elixir_ex{vars={ExVars, _}, unused={Unused, _}} = ExS,
maps:fold(fun
({Var, Kind} = Pair, Version, Acc) when is_atom(Kind) ->
({Var, Kind} = Pair, Version, {B, V})
when is_atom(Kind),
%% If the variable is part of the pruning (usually the input binding)
%% and is unused, we removed it from vars.
Version > PruneBefore orelse is_map_key({Pair, Version}, Unused) ->
Key = case Kind of
nil -> Var;
_ -> Pair
@@ -111,11 +121,14 @@ dump_binding(Binding, #elixir_ex{vars={ExVars, _}}, #elixir_erl{var_names=ErlVar
ErlName = maps:get(Version, ErlVars),
Value = find_binding(ErlName, Binding),
[{Key, Value} | Acc];
{[{Key, Value} | B], V};
(Pair, _, {B, V}) when PruneBefore >= 0 ->
{B, maps:remove(Pair, V)};
(_, _, Acc) ->
Acc
end, [], ExVars).
end, {[], ExVars}, ExVars).
find_binding(ErlName, Binding = #{}) ->
case Binding of
+83 -62
View File
@@ -173,7 +173,7 @@ expand({quote, Meta, [Opts, Do]}, S, E) when is_list(Do) ->
{File, Line} = case lists:keyfind(location, 1, EOpts) of
{location, keep} ->
{elixir_utils:relative_to_cwd(?key(E, file)), false};
{?key(E, file), false};
false ->
{proplists:get_value(file, EOpts, nil), proplists:get_value(line, EOpts, false)}
end,
@@ -253,45 +253,8 @@ expand({'try', Meta, [Opts]}, S, E) ->
%% Comprehensions
expand({for, Meta, [_ | _] = Args}, S, E) ->
assert_no_match_or_guard_scope(Meta, "for", S, E),
{Cases, Block} =
case elixir_utils:split_last(Args) of
{OuterCases, OuterOpts} when is_list(OuterOpts) ->
case elixir_utils:split_last(OuterCases) of
{InnerCases, InnerOpts} when is_list(InnerOpts) ->
{InnerCases, InnerOpts ++ OuterOpts};
_ ->
{OuterCases, OuterOpts}
end;
_ ->
{Args, []}
end,
validate_opts(Meta, for, [do, into, uniq, reduce], Block, E),
{Expr, Opts} =
case lists:keytake(do, 1, Block) of
{value, {do, Do}, DoOpts} ->
{Do, DoOpts};
false ->
form_error(Meta, E, ?MODULE, {missing_option, for, [do]})
end,
{EOpts, SO, EO} = expand(Opts, elixir_env:reset_unused_vars(S), E),
{ECases, SC, EC} = mapfold(fun expand_for/3, SO, EO, Cases),
assert_generator_start(Meta, ECases, E),
{EExpr, SE, EE} =
case validate_for_options(EOpts, false, false, false) of
{ok, MaybeReduce} -> expand_for_do_block(Meta, Expr, SC, EC, MaybeReduce);
{error, Error} -> form_error(Meta, E, ?MODULE, Error)
end,
{{for, Meta, ECases ++ [[{do, EExpr} | EOpts]]},
elixir_env:merge_and_check_unused_vars(SE, S, EE),
E};
expand({for, _, [_ | _] } = Expr, S, E) ->
expand_for(Expr, S, E, true);
%% With
@@ -344,7 +307,7 @@ expand({Name, Meta, Kind}, S, #{context := match} = E) when is_atom(Name), is_at
%% Variable was already overridden
#{Pair := VarVersion} when VarVersion >= PrematchVersion ->
maybe_warn_underscored_var_repeat(Meta, Name, Kind, E),
NewUnused = var_used(Pair, VarVersion, Unused),
NewUnused = var_used(Meta, Pair, VarVersion, Unused),
Var = {Name, [{version, VarVersion} | Meta], Kind},
{Var, S#elixir_ex{unused={NewUnused, Version}}, E};
@@ -391,7 +354,7 @@ expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) ->
{ok, PairVersion} ->
maybe_warn_underscored_var_access(Meta, Name, Kind, E),
Var = {Name, [{version, PairVersion} | Meta], Kind},
{Var, S#elixir_ex{unused={var_used(Pair, PairVersion, Unused), Version}}, E};
{Var, S#elixir_ex{unused={var_used(Meta, Pair, PairVersion, Unused), Version}}, E};
Error ->
case lists:keyfind(if_undefined, 1, Meta) of
@@ -405,7 +368,7 @@ expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) ->
%% TODO: Remove this clause on v2.0
_ when Error == warn ->
elixir_errors:form_warn(Meta, E, ?MODULE, {unknown_variable, Name}),
expand({Name, Meta, []}, S, E);
expand({Name, [{if_undefined, warn} | Meta], []}, S, E);
_ when Error == pin ->
form_error(Meta, E, ?MODULE, {undefined_var_pin, Name, Kind});
@@ -563,6 +526,12 @@ expand_block([], Acc, _Meta, S, E) ->
expand_block([H], Acc, Meta, S, E) ->
{EH, SE, EE} = expand(H, S, E),
expand_block([], [EH | Acc], Meta, SE, EE);
expand_block([{for, _, [_ | _]} = H | T], Acc, Meta, S, E) ->
{EH, SE, EE} = expand_for(H, S, E, false),
expand_block(T, [EH | Acc], Meta, SE, EE);
expand_block([{'=', _, [{'_', _, Ctx}, {for, _, [_ | _]} = H]} | T], Acc, Meta, S, E) when is_atom(Ctx) ->
{EH, SE, EE} = expand_for(H, S, E, false),
expand_block(T, [EH | Acc], Meta, SE, EE);
expand_block([H | T], Acc, Meta, S, E) ->
{EH, SE, EE} = expand(H, S, E),
@@ -638,16 +607,19 @@ mapfold(_Fun, S, E, [], Acc) ->
%% Match/var helpers
var_unused({Name, Kind}, Meta, Version, Unused, Override) ->
var_unused({_, Kind} = Pair, Meta, Version, Unused, Override) ->
case (Kind == nil) andalso should_warn(Meta) of
true -> Unused#{{Name, Version} => {?line(Meta), Override}};
true -> Unused#{{Pair, Version} => {?line(Meta), Override}};
false -> Unused
end.
var_used({Name, Kind}, Version, Unused) ->
case Kind of
nil -> Unused#{{Name, Version} => false};
_ -> Unused
var_used(Meta, {_, Kind} = Pair, Version, Unused) ->
KeepUnused = lists:keymember(keep_unused, 1, Meta),
if
KeepUnused -> Unused;
is_atom(Kind) -> Unused#{{Pair, Version} => false};
true -> Unused
end.
maybe_warn_underscored_var_repeat(Meta, Name, Kind, E) ->
@@ -741,18 +713,65 @@ generated_case_clauses([{do, Clauses}]) ->
%% Comprehensions
validate_for_options([{into, _} = Pair | Opts], _Into, Uniq, Reduce) ->
validate_for_options(Opts, Pair, Uniq, Reduce);
validate_for_options([{uniq, Boolean} = Pair | Opts], Into, _Uniq, Reduce) when is_boolean(Boolean) ->
validate_for_options(Opts, Into, Pair, Reduce);
validate_for_options([{uniq, Value} | _], _, _, _) ->
expand_for({for, Meta, [_ | _] = Args}, S, E, Return) ->
assert_no_match_or_guard_scope(Meta, "for", S, E),
{Cases, Block} =
case elixir_utils:split_last(Args) of
{OuterCases, OuterOpts} when is_list(OuterOpts) ->
case elixir_utils:split_last(OuterCases) of
{InnerCases, InnerOpts} when is_list(InnerOpts) ->
{InnerCases, InnerOpts ++ OuterOpts};
_ ->
{OuterCases, OuterOpts}
end;
_ ->
{Args, []}
end,
validate_opts(Meta, for, [do, into, uniq, reduce], Block, E),
{Expr, Opts} =
case lists:keytake(do, 1, Block) of
{value, {do, Do}, DoOpts} ->
{Do, DoOpts};
false ->
form_error(Meta, E, ?MODULE, {missing_option, for, [do]})
end,
{EOpts, SO, EO} = expand(Opts, elixir_env:reset_unused_vars(S), E),
{ECases, SC, EC} = mapfold(fun expand_for_generator/3, SO, EO, Cases),
assert_generator_start(Meta, ECases, E),
{{EExpr, SE, EE}, NormalizedOpts} =
case validate_for_options(EOpts, false, false, false, Return, Meta, E, []) of
{ok, MaybeReduce, NOpts} -> {expand_for_do_block(Meta, Expr, SC, EC, MaybeReduce), NOpts};
{error, Error} -> {form_error(Meta, E, ?MODULE, Error), EOpts}
end,
{{for, Meta, ECases ++ [[{do, EExpr} | NormalizedOpts]]},
elixir_env:merge_and_check_unused_vars(SE, S, EE),
E}.
validate_for_options([{into, _} = Pair | Opts], _Into, Uniq, Reduce, Return, Meta, E, Acc) ->
validate_for_options(Opts, Pair, Uniq, Reduce, Return, Meta, E, [Pair | Acc]);
validate_for_options([{uniq, Boolean} = Pair | Opts], Into, _Uniq, Reduce, Return, Meta, E, Acc) when is_boolean(Boolean) ->
validate_for_options(Opts, Into, Pair, Reduce, Return, Meta, E, [Pair | Acc]);
validate_for_options([{uniq, Value} | _], _, _, _, _, _, _, _) ->
{error, {for_invalid_uniq, Value}};
validate_for_options([{reduce, _} = Pair | Opts], Into, Uniq, _Reduce) ->
validate_for_options(Opts, Into, Uniq, Pair);
validate_for_options([], Into, Uniq, {reduce, _}) when Into /= false; Uniq /= false ->
validate_for_options([{reduce, _} = Pair | Opts], Into, Uniq, _Reduce, Return, Meta, E, Acc) ->
validate_for_options(Opts, Into, Uniq, Pair, Return, Meta, E, [Pair | Acc]);
validate_for_options([], Into, Uniq, {reduce, _}, _Return, _Meta, _E, _Acc) when Into /= false; Uniq /= false ->
{error, for_conflicting_reduce_into_uniq};
validate_for_options([], _Into, _Uniq, Reduce) ->
{ok, Reduce}.
validate_for_options([], _Into = false, Uniq, Reduce = false, Return = true, Meta, E, Acc) ->
Pair = {into, []},
validate_for_options([Pair], Pair, Uniq, Reduce, Return, Meta, E, Acc);
validate_for_options([], Into = false, {uniq, true}, Reduce = false, Return = false, Meta, E, Acc) ->
elixir_errors:form_warn(Meta, E, ?MODULE, for_with_unused_uniq),
AccWithoutUniq = lists:keydelete(uniq, 1, Acc),
validate_for_options([], Into, false, Reduce, Return, Meta, E, AccWithoutUniq);
validate_for_options([], _Into, _Uniq, Reduce, _Return, _Meta, _E, Acc) ->
{ok, Reduce, lists:reverse(Acc)}.
expand_for_do_block(Meta, [{'->', _, _} | _], _S, E, false) ->
form_error(Meta, E, ?MODULE, for_without_reduce_bad_block);
@@ -1031,12 +1050,12 @@ expand_aliases({'__aliases__', Meta, _} = Alias, S, E, Report) ->
%% Comprehensions
expand_for({'<-', Meta, [Left, Right]}, S, E) ->
expand_for_generator({'<-', Meta, [Left, Right]}, S, E) ->
{ERight, SR, ER} = expand(Right, S, E),
SM = elixir_env:reset_read(SR, S),
{[ELeft], SL, EL} = elixir_clauses:head([Left], SM, ER),
{{'<-', Meta, [ELeft, ERight]}, SL, EL};
expand_for({'<<>>', Meta, Args} = X, S, E) when is_list(Args) ->
expand_for_generator({'<<>>', Meta, Args} = X, S, E) when is_list(Args) ->
case elixir_utils:split_last(Args) of
{LeftStart, {'<-', OpMeta, [LeftEnd, Right]}} ->
{ERight, SR, ER} = expand(Right, S, E),
@@ -1048,7 +1067,7 @@ expand_for({'<<>>', Meta, Args} = X, S, E) when is_list(Args) ->
_ ->
expand(X, S, E)
end;
expand_for(X, S, E) ->
expand_for_generator(X, S, E) ->
expand(X, S, E).
assert_generator_start(_, [{'<-', _, [_, _]} | _], _) ->
@@ -1170,6 +1189,8 @@ format_error(for_without_reduce_bad_block) ->
"the do block was written using acc -> expr clauses but the :reduce option was not given";
format_error(for_generator_start) ->
"for comprehensions must start with a generator";
format_error(for_with_unused_uniq) ->
"the :uniq option has no effect since the result of the for comprehension is not used";
format_error(unhandled_arrow_op) ->
"unhandled operator ->";
format_error(as_in_multi_alias_call) ->
+1 -1
View File
@@ -39,7 +39,7 @@ extract([$\\, Last, Last, Last | Rest], Buffer, Output, Line, Column, Scope, Int
extract(Rest, [Last, Last, Last | Buffer], Output, Line, Column+4, Scope, Interpol, All);
extract([$\\, $#, ${ | Rest], Buffer, Output, Line, Column, Scope, true, Last) ->
extract(Rest, [${, $#, $\\ | Buffer], Output, Line, Column+1, Scope, true, Last);
extract(Rest, [${, $#, $\\ | Buffer], Output, Line, Column+3, Scope, true, Last);
extract([$#, ${ | Rest], Buffer, Output, Line, Column, Scope, true, Last) ->
Output1 = build_string(Buffer, Output),
+8 -2
View File
@@ -25,7 +25,7 @@ expand_struct(Meta, Left, {'%{}', MapMeta, MapArgs}, S, #{context := Context} =
AssocKeys = [K || {K, _} <- Assocs],
Struct = load_struct(Meta, ELeft, [Assocs], AssocKeys, EE),
Keys = ['__struct__'] ++ AssocKeys,
WithoutKeys = maps:to_list(maps:without(Keys, Struct)),
WithoutKeys = lists:sort(maps:to_list(maps:without(Keys, Struct))),
StructAssocs = elixir_quote:escape(WithoutKeys, none, false),
{{'%', Meta, [ELeft, {'%{}', MapMeta, StructAssocs ++ Assocs}]}, SE, EE};
@@ -138,13 +138,19 @@ load_struct(Meta, Name, Args, Keys, E) ->
wrapped_load_struct(Meta, Name, Args, Keys, E) ->
%% We also include the current module because it won't be present
%% in context module in case the module name is defined dynamically.
InContext = lists:member(Name, [?key(E, module) | ?key(E, context_modules)]),
Module = ?key(E, module),
InContext = lists:member(Name, [Module | ?key(E, context_modules)]),
Arity = length(Args),
External = InContext orelse (not(ensure_loaded(Name)) andalso wait_for_struct(Name)),
try
case External andalso elixir_def:external_for(Meta, Name, '__struct__', Arity, [def]) of
%% If I am accessing myself and there is no __struct__ function,
%% don't invoke the fallback to avoid calling loaded code.
false when Module == Name ->
error(undef);
false ->
apply(Name, '__struct__', Args);
+27 -13
View File
@@ -1,6 +1,6 @@
-module(elixir_module).
-export([file/1, data_tables/1, is_open/1, mode/1, delete_definition_attributes/6,
compile/4, expand_callback/6, format_error/1, compiler_modules/0,
compile/5, expand_callback/6, format_error/1, compiler_modules/0,
write_cache/3, read_cache/2, next_counter/1]).
-include("elixir.hrl").
-define(counter_attr, {elixir, counter}).
@@ -64,7 +64,7 @@ next_counter(Module) ->
%% Compilation hook
compile(Module, Block, Vars, Env) when is_atom(Module) ->
compile(Module, Block, Vars, Prune, Env) when is_atom(Module) ->
#{line := Line, function := Function, versioned_vars := OldVerVars} = Env,
{VerVars, _} =
@@ -82,16 +82,16 @@ compile(Module, Block, Vars, Env) when is_atom(Module) ->
#{lexical_tracker := nil} ->
elixir_lexical:run(
MaybeLexEnv,
fun(LexEnv) -> compile(Line, Module, Block, Vars, LexEnv) end,
fun(LexEnv) -> compile(Line, Module, Block, Vars, Prune, LexEnv) end,
fun(_LexEnv) -> ok end
);
_ ->
compile(Line, Module, Block, Vars, MaybeLexEnv)
compile(Line, Module, Block, Vars, Prune, MaybeLexEnv)
end;
compile(Module, _Block, _Vars, #{line := Line, file := File}) ->
compile(Module, _Block, _Vars, _Prune, #{line := Line, file := File}) ->
elixir_errors:form_error([{line, Line}], File, ?MODULE, {invalid_module, Module}).
compile(Line, Module, Block, Vars, E) ->
compile(Line, Module, Block, Vars, Prune, E) ->
File = ?key(E, file),
check_module_availability(Line, File, Module),
ModuleAsCharlist = validate_module_name(Line, File, Module),
@@ -102,7 +102,7 @@ compile(Line, Module, Block, Vars, E) ->
try
put_compiler_modules([Module | CompilerModules]),
{Result, NE} = eval_form(Line, Module, DataBag, Block, Vars, E),
{Result, ModuleE, CallbackE} = eval_form(Line, Module, DataBag, Block, Vars, Prune, E),
CheckerInfo = checker_info(),
{Binary, PersistedAttributes, Autoload} =
@@ -133,7 +133,7 @@ compile(Line, Module, Block, Vars, E) ->
CompileOpts = validate_compile_opts(RawCompileOpts, AllDefinitions, Unreachable, File, Line),
AfterVerify = bag_lookup_element(DataBag, {accumulate, after_verify}, 2),
[elixir_env:trace({remote_function, [], VerifyMod, VerifyFun, 1}, E) ||
[elixir_env:trace({remote_function, [], VerifyMod, VerifyFun, 1}, CallbackE) ||
{VerifyMod, VerifyFun} <- AfterVerify],
ModuleMap = #{
@@ -158,8 +158,8 @@ compile(Line, Module, Block, Vars, E) ->
end),
Autoload andalso code:load_binary(Module, beam_location(ModuleAsCharlist), Binary),
eval_callbacks(Line, DataBag, after_compile, [NE, Binary], NE),
elixir_env:trace({on_module, Binary, none}, E),
eval_callbacks(Line, DataBag, after_compile, [CallbackE, Binary], CallbackE),
elixir_env:trace({on_module, Binary, none}, ModuleE),
warn_unused_attributes(File, DataSet, DataBag, PersistedAttributes),
make_module_available(Module, Binary),
(CheckerInfo == undefined) andalso
@@ -375,13 +375,27 @@ build(Line, File, Module) ->
%% Handles module and callback evaluations.
eval_form(Line, Module, DataBag, Block, Vars, E) ->
{Value, EE} = elixir_compiler:compile(Block, Vars, E),
eval_form(Line, Module, DataBag, Block, Vars, Prune, E) ->
{Value, ExS, EE} = elixir_compiler:compile(Block, Vars, E),
elixir_overridable:store_not_overridden(Module),
EV = (elixir_env:reset_vars(EE))#{line := Line},
EC = eval_callbacks(Line, DataBag, before_compile, [EV], EV),
elixir_overridable:store_not_overridden(Module),
{Value, EC}.
{Value, maybe_prune_versioned_vars(Prune, Vars, ExS, E), EC}.
maybe_prune_versioned_vars(false, _Vars, _Exs, E) ->
E;
maybe_prune_versioned_vars(true, Vars, ExS, E) ->
PruneBefore = length(Vars),
#elixir_ex{vars={ExVars, _}, unused={Unused, _}} = ExS,
VersionedVars =
maps:filter(fun
(Pair, Version) when Version < PruneBefore, not is_map_key({Pair, Version}, Unused) -> false;
(_, _) -> true
end, ExVars),
E#{versioned_vars := VersionedVars}.
eval_callbacks(Line, DataBag, Name, Args, E) ->
Callbacks = bag_lookup_element(DataBag, {accumulate, Name}, 2),
@@ -958,6 +958,15 @@ defmodule DateTimeTest do
end
describe "add" do
test "add with invalid time unit" do
dt = DateTime.utc_now()
message =
~r/unsupported time unit\. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got "day"/
assert_raise ArgumentError, message, fn -> DateTime.add(dt, 1, "day") end
end
test "add with non-struct map that conforms to Calendar.datetime" do
dt_map = DateTime.from_naive!(~N[2018-08-28 00:00:00], "Etc/UTC") |> Map.from_struct()
+7
View File
@@ -364,5 +364,12 @@ defmodule CalendarTest do
Calendar.strftime(~D[2019-08-15], "%D", unknown: "option")
end
end
test "zero padding for negative year" do
assert Calendar.strftime(Date.new!(-1, 1, 1), "%Y") == "-0001"
assert Calendar.strftime(Date.new!(-11, 1, 1), "%Y") == "-0011"
assert Calendar.strftime(Date.new!(-111, 1, 1), "%Y") == "-0111"
assert Calendar.strftime(Date.new!(-1111, 1, 1), "%Y") == "-1111"
end
end
end
@@ -299,6 +299,8 @@ defmodule Code.Formatter.ContainersTest do
assert_format "<<x::binary()-(13 * 6)-custom>>", "<<x::binary-(13 * 6)-custom()>>"
assert_same "<<x::binary-(13 * 6)-custom()>>"
assert_same "<<0::size*unit, bytes::binary>>"
assert_format "<<0::size*unit, bytes::custom>>", "<<0::size*unit, bytes::custom()>>"
assert_format "<<0, 1::2-integer() <- x>>", "<<0, 1::2-integer <- x>>"
assert_same "<<0, 1::2-integer <- x>>"
@@ -507,6 +507,28 @@ defmodule Code.Normalizer.FormatterASTTest do
# after
""",
literal_encoder: fn literal, _ -> {:ok, literal} end
assert_same """
block do
# before 1
1 + 1
# before 2
2 + 2
end
""",
literal_encoder: fn literal, _ -> {:ok, literal} end
assert_same """
block do
# before 1
Mix.install([1 + 1])
# before 2
Mix.install([2 + 2])
end
""",
literal_encoder: fn literal, _ -> {:ok, literal} end
end
test "before and after expressions with newlines" do
@@ -515,6 +515,17 @@ defmodule Code.Normalizer.QuotedASTTest do
test "charlist" do
assert quoted_to_string(quote(do: [])) == "[]"
assert quoted_to_string(quote(do: 'abc')) == "'abc'"
# False positive
assert quoted_to_string(
quote do
:"Elixir.List".to_charlist([
case var do
var -> var
end
])
end
) =~ "List.to_charlist([\n case var do\n var -> var\n end\n])"
end
test "string" do
@@ -656,6 +667,13 @@ defmodule Code.Normalizer.QuotedASTTest do
assert quoted_to_string(quote(do: :"one\n\"#{2}\"\nthree")) == ~S[:"one\n\"#{2}\"\nthree"]
end
test ":erlang.binary_to_atom/2 edge cases" do
assert quoted_to_string(quote(do: :erlang.binary_to_atom(<<>>, :utf8))) == ~S[:""]
assert quoted_to_string(quote(do: :erlang.binary_to_atom(<<1>>, :utf8))) ==
~S":erlang.binary_to_atom(<<1>>, :utf8)"
end
end
describe "quoted_to_algebra/2 does not escape" do
+76
View File
@@ -48,6 +48,16 @@ defmodule CodeTest do
assert Code.eval_string("var!(a, Sample) = 1") == {1, [{{:a, Sample}, 1}]}
end
defmacro hygiene_var do
quote do
a = 1
end
end
test "does not return bindings from macro hygiene" do
assert Code.eval_string("require CodeTest; CodeTest.hygiene_var()") == {1, []}
end
test "does not raise on duplicate bindings" do
# The order of which values win is not guaranteed, but it should evaluate successfully.
assert Code.eval_string("b = String.Chars.to_string(a)", a: 0, a: 1) ==
@@ -167,6 +177,72 @@ defmodule CodeTest do
assert Macro.Env.vars(env) == [{:x, nil}]
end
test "eval_quoted_with_env/3 with pruning" do
env = Code.env_for_eval(__ENV__)
fun = fn quoted, binding ->
{_, binding, env} = Code.eval_quoted_with_env(quoted, binding, env, prune_binding: true)
{binding, Macro.Env.vars(env)}
end
assert fun.(quote(do: 123), []) == {[], []}
assert fun.(quote(do: 123), x: 2, y: 3) == {[], []}
assert fun.(quote(do: var!(x) = 1), []) == {[x: 1], [x: nil]}
assert fun.(quote(do: var!(x) = 1), x: 2, y: 3) == {[x: 1], [x: nil]}
assert fun.(quote(do: var!(x, :foo) = 1), []) == {[{{:x, :foo}, 1}], [x: :foo]}
assert fun.(quote(do: var!(x, :foo) = 1), x: 2, y: 3) == {[{{:x, :foo}, 1}], [x: :foo]}
assert fun.(quote(do: var!(x, :foo) = 1), [{{:x, :foo}, 2}, {{:y, :foo}, 3}]) ==
{[{{:x, :foo}, 1}], [x: :foo]}
assert fun.(quote(do: fn -> var!(x, :foo) = 1 end), []) == {[], []}
assert fun.(quote(do: fn -> var!(x, :foo) = 1 end), x: 1, y: 2) == {[], []}
assert fun.(quote(do: fn -> var!(x) end), x: 2, y: 3) == {[x: 2], [x: nil]}
assert fun.(quote(do: fn -> var!(x, :foo) end), [{{:x, :foo}, 2}, {{:y, :foo}, 3}]) ==
{[{{:x, :foo}, 2}], [x: :foo]}
end
defmodule Tracer do
def trace(event, env) do
send(self(), {:trace, event, env})
:ok
end
end
test "eval_quoted_with_env/3 with tracing and pruning" do
env = %{Code.env_for_eval(__ENV__) | tracers: [Tracer], function: nil}
binding = [x: 1, y: 2, z: 3]
quoted =
quote do
defmodule Elixir.CodeTest.TracingPruning do
var!(y) = :updated
var!(y)
var!(x)
end
end
{_, binding, env} = Code.eval_quoted_with_env(quoted, binding, env, prune_binding: true)
assert Enum.sort(binding) == []
assert env.versioned_vars == %{}
assert_receive {:trace, {:on_module, _, _}, %{module: CodeTest.TracingPruning} = trace_env}
assert trace_env.versioned_vars == %{{:result, Kernel} => 5, {:x, nil} => 1, {:y, nil} => 4}
end
test "eval_quoted_with_env/3 with defguard" do
require Integer
env = Code.env_for_eval(__ENV__)
quoted = quote do: Integer.is_even(1)
{false, binding, env} = Code.eval_quoted_with_env(quoted, [], env, prune_binding: true)
assert binding == []
assert Macro.Env.vars(env) == []
end
test "compile_file/1" do
assert Code.compile_file(fixture_path("code_sample.exs")) == []
refute fixture_path("code_sample.exs") in Code.required_files()
+61 -64
View File
@@ -735,11 +735,11 @@ defmodule EnumTest do
assert Enum.split_with(%{}, fn x -> rem(x, 2) == 0 end) == {[], []}
assert Enum.split_with(%{a: 1, b: 2, c: 3}, fn {_k, v} -> rem(v, 2) == 0 end) ==
{[b: 2], [a: 1, c: 3]}
assert Enum.split_with(%{a: 1, b: 2}, fn {_k, v} -> rem(v, 2) == 0 end) ==
{[b: 2], [a: 1]}
assert Enum.split_with(%{b: 2, d: 4, f: 6}, fn {_k, v} -> rem(v, 2) == 0 end) ==
{[b: 2, d: 4, f: 6], []}
{Map.to_list(%{b: 2, d: 4, f: 6}), []}
end
test "random/1" do
@@ -1503,14 +1503,11 @@ defmodule EnumTest do
assert result == [2, 4, 6, 8, 10]
# Maps
result = Enum.zip_with(%{a: 7, c: 9}, 3..4, fn {key, value}, b -> {key, value + b} end)
assert result == [a: 10, c: 13]
result = Enum.zip_with(%{a: 7}, 3..4, fn {key, value}, b -> {key, value + b} end)
assert result == [a: 10]
colour_1 = %{r: 176, g: 175, b: 255}
colour_2 = %{r: 12, g: 176, b: 176}
result = Enum.zip_with(colour_1, colour_2, fn {k, left}, {k, right} -> {k, left + right} end)
assert result == [b: 431, g: 351, r: 188]
result = Enum.zip_with(3..4, %{a: 7}, fn a, {key, value} -> {key, value + a} end)
assert result == [a: 10]
end
test "zip_with/2" do
@@ -1518,8 +1515,10 @@ defmodule EnumTest do
result = Enum.zip_with([[:a, :b], [1, 2], ["foo", "bar"]], zip_fun)
assert result == [{:a, 1, "foo"}, {:b, 2, "bar"}]
lots = Enum.zip_with([[:a, :b], [1, 2], ["foo", "bar"], %{a: :b, c: :d}], zip_fun)
assert lots == [{:a, 1, "foo", {:a, :b}}, {:b, 2, "bar", {:c, :d}}]
map = %{a: :b, c: :d}
[x1, x2] = Map.to_list(map)
lots = Enum.zip_with([[:a, :b], [1, 2], ["foo", "bar"], map], zip_fun)
assert lots == [{:a, 1, "foo", x1}, {:b, 2, "bar", x2}]
assert Enum.zip_with([[:a, :b], [1, 2, 3, 4], ["foo", "bar", "baz", "qux"]], zip_fun) ==
[{:a, 1, "foo"}, {:b, 2, "bar"}]
@@ -1556,21 +1555,11 @@ defmodule EnumTest do
assert result == [2, 4, 6, 8, 10]
# Maps
result = Enum.zip_with([%{a: 7, c: 9}, 3..4], fn [{key, value}, b] -> {key, value + b} end)
assert result == [a: 10, c: 13]
result = Enum.zip_with([%{a: 7}, 3..4], fn [{key, value}, b] -> {key, value + b} end)
assert result == [a: 10]
colour_1 = %{r: 176, g: 175, b: 255}
colour_2 = %{r: 12, g: 176, b: 176}
result =
Enum.zip_with([colour_1, colour_2], fn [{k, left}, {k, right}] -> {k, left + right} end)
assert result == [b: 431, g: 351, r: 188]
assert Enum.zip_with([%{a: :b, c: :d}, %{e: :f, g: :h}], & &1) == [
[a: :b, e: :f],
[c: :d, g: :h]
]
result = Enum.zip_with([%{a: 7}, 3..4], fn [{key, value}, b] -> {key, value + b} end)
assert result == [a: 10]
end
end
@@ -2396,16 +2385,17 @@ defmodule EnumTest.Map do
test "random/1" do
map = %{a: 1, b: 2, c: 3}
[x1, x2, x3] = Map.to_list(map)
seed1 = {1406, 407_414, 139_258}
seed2 = {1406, 421_106, 567_597}
:rand.seed(:exsss, seed1)
assert Enum.random(map) == {:c, 3}
assert Enum.random(map) == {:a, 1}
assert Enum.random(map) == {:b, 2}
assert Enum.random(map) == x3
assert Enum.random(map) == x1
assert Enum.random(map) == x2
:rand.seed(:exsss, seed2)
assert Enum.random(map) == {:c, 3}
assert Enum.random(map) == {:b, 2}
assert Enum.random(map) == x3
assert Enum.random(map) == x2
end
test "take_random/2" do
@@ -2418,35 +2408,37 @@ defmodule EnumTest.Map do
# set a fixed seed so the test can be deterministic
# please note the order of following assertions is important
map = %{a: 1, b: 2, c: 3}
[x1, x2, x3] = Map.to_list(map)
seed1 = {1406, 407_414, 139_258}
seed2 = {1406, 421_106, 567_597}
:rand.seed(:exsss, seed1)
assert Enum.take_random(map, 1) == [c: 3]
assert Enum.take_random(map, 1) == [x3]
:rand.seed(:exsss, seed1)
assert Enum.take_random(map, 2) == [c: 3, a: 1]
assert Enum.take_random(map, 2) == [x3, x1]
:rand.seed(:exsss, seed1)
assert Enum.take_random(map, 3) == [c: 3, a: 1, b: 2]
assert Enum.take_random(map, 3) == [x3, x1, x2]
:rand.seed(:exsss, seed1)
assert Enum.take_random(map, 4) == [c: 3, a: 1, b: 2]
assert Enum.take_random(map, 4) == [x3, x1, x2]
:rand.seed(:exsss, seed2)
assert Enum.take_random(map, 1) == [a: 1]
assert Enum.take_random(map, 1) == [x1]
:rand.seed(:exsss, seed2)
assert Enum.take_random(map, 2) == [a: 1, c: 3]
assert Enum.take_random(map, 2) == [x1, x3]
:rand.seed(:exsss, seed2)
assert Enum.take_random(map, 3) == [a: 1, c: 3, b: 2]
assert Enum.take_random(map, 3) == [x1, x3, x2]
:rand.seed(:exsss, seed2)
assert Enum.take_random(map, 4) == [a: 1, c: 3, b: 2]
assert Enum.take_random(map, 4) == [x1, x3, x2]
end
test "reverse/1" do
assert Enum.reverse(%{}) == []
assert Enum.reverse(MapSet.new()) == []
assert Enum.reverse(%{a: 1, b: 2, c: 3}) == [c: 3, b: 2, a: 1]
map = %{a: 1, b: 2, c: 3}
assert Enum.reverse(map) == Map.to_list(map) |> Enum.reverse()
end
test "reverse/2" do
assert Enum.reverse([a: 1, b: 2, c: 3, a: 1], %{x: 1, y: 2, z: 3}) ==
[a: 1, c: 3, b: 2, a: 1, x: 1, y: 2, z: 3]
assert Enum.reverse([a: 1, b: 2, c: 3, a: 1], %{x: 1}) == [a: 1, c: 3, b: 2, a: 1, x: 1]
assert Enum.reverse([], %{a: 1}) == [a: 1]
assert Enum.reverse([], %{}) == []
@@ -2456,44 +2448,49 @@ defmodule EnumTest.Map do
test "fetch/2" do
map = %{a: 1, b: 2, c: 3, d: 4, e: 5}
assert Enum.fetch(map, 0) == {:ok, {:a, 1}}
assert Enum.fetch(map, -2) == {:ok, {:d, 4}}
[x1, _x2, _x3, x4, x5] = Map.to_list(map)
assert Enum.fetch(map, 0) == {:ok, x1}
assert Enum.fetch(map, -2) == {:ok, x4}
assert Enum.fetch(map, -6) == :error
assert Enum.fetch(map, 5) == :error
assert Enum.fetch(%{}, 0) == :error
assert Stream.take(map, 3) |> Enum.fetch(3) == :error
assert Stream.take(map, 5) |> Enum.fetch(4) == {:ok, {:e, 5}}
assert Stream.take(map, 5) |> Enum.fetch(4) == {:ok, x5}
end
test "map_intersperse/3" do
assert Enum.map_intersperse(%{}, :a, & &1) == []
assert Enum.map_intersperse(%{foo: :bar}, :a, & &1) == [{:foo, :bar}]
assert Enum.map_intersperse(%{foo: :bar, baz: :bat}, :a, & &1) ==
[{:baz, :bat}, :a, {:foo, :bar}]
map = %{foo: :bar, baz: :bat}
[x1, x2] = Map.to_list(map)
assert Enum.map_intersperse(map, :a, & &1) == [x1, :a, x2]
end
test "slice/2" do
map = %{a: 1, b: 2, c: 3, d: 4, e: 5}
assert Enum.slice(map, 0..0) == [a: 1]
assert Enum.slice(map, 0..1) == [a: 1, b: 2]
assert Enum.slice(map, 0..2) == [a: 1, b: 2, c: 3]
[x1, x2, x3 | _] = Map.to_list(map)
assert Enum.slice(map, 0..0) == [x1]
assert Enum.slice(map, 0..1) == [x1, x2]
assert Enum.slice(map, 0..2) == [x1, x2, x3]
end
test "slice/3" do
map = %{a: 1, b: 2, c: 3, d: 4, e: 5}
assert Enum.slice(map, 1, 2) == [b: 2, c: 3]
[x1, x2, x3, x4, x5] = Map.to_list(map)
assert Enum.slice(map, 1, 2) == [x2, x3]
assert Enum.slice(map, 1, 0) == []
assert Enum.slice(map, 2, 5) == [c: 3, d: 4, e: 5]
assert Enum.slice(map, 2, 6) == [c: 3, d: 4, e: 5]
assert Enum.slice(map, 2, 5) == [x3, x4, x5]
assert Enum.slice(map, 2, 6) == [x3, x4, x5]
assert Enum.slice(map, 5, 5) == []
assert Enum.slice(map, 6, 5) == []
assert Enum.slice(map, 6, 0) == []
assert Enum.slice(map, -6, 0) == []
assert Enum.slice(map, -6, 5) == [a: 1, b: 2, c: 3, d: 4, e: 5]
assert Enum.slice(map, -2, 5) == [d: 4, e: 5]
assert Enum.slice(map, -3, 1) == [c: 3]
assert Enum.slice(map, -6, 5) == [x1, x2, x3, x4, x5]
assert Enum.slice(map, -2, 5) == [x4, x5]
assert Enum.slice(map, -3, 1) == [x3]
assert_raise FunctionClauseError, fn ->
Enum.slice(map, 0, -1)
@@ -2508,19 +2505,19 @@ defmodule EnumTest.Map do
end
assert Enum.slice(map, 0, 0) == []
assert Enum.slice(map, 0, 1) == [a: 1]
assert Enum.slice(map, 0, 2) == [a: 1, b: 2]
assert Enum.slice(map, 1, 2) == [b: 2, c: 3]
assert Enum.slice(map, 0, 1) == [x1]
assert Enum.slice(map, 0, 2) == [x1, x2]
assert Enum.slice(map, 1, 2) == [x2, x3]
assert Enum.slice(map, 1, 0) == []
assert Enum.slice(map, 2, 5) == [c: 3, d: 4, e: 5]
assert Enum.slice(map, 2, 6) == [c: 3, d: 4, e: 5]
assert Enum.slice(map, 2, 5) == [x3, x4, x5]
assert Enum.slice(map, 2, 6) == [x3, x4, x5]
assert Enum.slice(map, 5, 5) == []
assert Enum.slice(map, 6, 5) == []
assert Enum.slice(map, 6, 0) == []
assert Enum.slice(map, -6, 0) == []
assert Enum.slice(map, -6, 5) == [a: 1, b: 2, c: 3, d: 4, e: 5]
assert Enum.slice(map, -2, 5) == [d: 4, e: 5]
assert Enum.slice(map, -3, 1) == [c: 3]
assert Enum.slice(map, -6, 5) == [x1, x2, x3, x4, x5]
assert Enum.slice(map, -2, 5) == [x4, x5]
assert Enum.slice(map, -3, 1) == [x3]
assert_raise FunctionClauseError, fn ->
Enum.slice(map, 0, -1)
+28 -6
View File
@@ -18,13 +18,20 @@ defmodule ExceptionTest do
end
end
assert Exception.message(%{__struct__: BadException, __exception__: true, raise: true}) =~
"got RuntimeError with message \"oops\" while retrieving Exception.message/1 " <>
"for %{__exception__: true, __struct__: ExceptionTest.BadException, raise: true}"
assert "got RuntimeError with message \"oops\" while retrieving Exception.message/1 for %{" <>
inspected =
Exception.message(%{__struct__: BadException, __exception__: true, raise: true})
assert Exception.message(%{__struct__: BadException, __exception__: true, raise: false}) =~
"got nil while retrieving Exception.message/1 " <>
"for %{__exception__: true, __struct__: ExceptionTest.BadException, raise: false}"
assert inspected =~ "raise: true"
assert inspected =~ "__exception__: true"
assert inspected =~ "__struct__: ExceptionTest.BadException"
assert "got nil while retrieving Exception.message/1 for %{" <> inspected =
Exception.message(%{__struct__: BadException, __exception__: true, raise: false})
assert inspected =~ "raise: false"
assert inspected =~ "__exception__: true"
assert inspected =~ "__struct__: ExceptionTest.BadException"
end
test "normalize/2" do
@@ -426,6 +433,8 @@ defmodule ExceptionTest do
when is_binary(url) or (is_struct(url) and is_struct(url, url_module) and false) do
url
end
def sub_get!(url) when is_struct(url.sub, URI), do: url.sub
end
)
@@ -475,6 +484,19 @@ defmodule ExceptionTest do
def get!(url) when -is_binary(url)- or is_struct(url) and is_struct(url, URI) and -false-
"""
assert blame_message(Req, & &1.sub_get!(%{})) =~ """
no function clause matching in ExceptionTest.Req.sub_get!/1
The following arguments were given to ExceptionTest.Req.sub_get!/1:
# 1
%{}
Attempted function clauses (showing 1 out of 1):
def sub_get!(url) when -is_struct(url.sub, URI)-
"""
end
test "annotates badarg on apply" do
+22 -11
View File
@@ -417,14 +417,20 @@ defmodule Inspect.MapTest do
test "basic" do
assert inspect(%{1 => "b"}) == "%{1 => \"b\"}"
assert inspect(%{1 => "b", 2 => "c"}, pretty: true, width: 1) ==
assert inspect(%{1 => "b", 2 => "c"},
pretty: true,
width: 1,
custom_options: [sort_maps: true]
) ==
"%{\n 1 => \"b\",\n 2 => \"c\"\n}"
end
test "keyword" do
assert inspect(%{a: 1}) == "%{a: 1}"
assert inspect(%{a: 1, b: 2}) == "%{a: 1, b: 2}"
assert inspect(%{a: 1, b: 2, c: 3}) == "%{a: 1, b: 2, c: 3}"
assert inspect(%{a: 1, b: 2}, custom_options: [sort_maps: true]) == "%{a: 1, b: 2}"
assert inspect(%{a: 1, b: 2, c: 3}, custom_options: [sort_maps: true]) ==
"%{a: 1, b: 2, c: 3}"
end
test "with limit" do
@@ -446,12 +452,12 @@ defmodule Inspect.MapTest do
test "public modified struct" do
public = %Public{key: 1}
assert inspect(Map.put(public, :foo, :bar)) ==
assert inspect(Map.put(public, :foo, :bar), custom_options: [sort_maps: true]) ==
"%{__struct__: Inspect.MapTest.Public, foo: :bar, key: 1}"
end
test "private struct" do
assert inspect(%{__struct__: Private, key: 1}) ==
assert inspect(%{__struct__: Private, key: 1}, custom_options: [sort_maps: true]) ==
"%{__struct__: Inspect.MapTest.Private, key: 1}"
end
@@ -486,7 +492,7 @@ defmodule Inspect.MapTest do
%{__struct__: Inspect.MapTest.Failing, name: "Foo"}
'''
assert inspect(%Failing{name: "Foo"}) =~ message
assert inspect(%Failing{name: "Foo"}, custom_options: [sort_maps: true]) =~ message
end
test "safely inspect bad implementation disables colors" do
@@ -505,7 +511,10 @@ defmodule Inspect.MapTest do
%{__struct__: Inspect.MapTest.Failing, name: "Foo"}
'''
assert inspect(%Failing{name: "Foo"}, syntax_colors: [atom: [:green]]) =~ message
assert inspect(%Failing{name: "Foo"},
syntax_colors: [atom: [:green]],
custom_options: [sort_maps: true]
) =~ message
end
test "unsafely inspect bad implementation" do
@@ -524,7 +533,7 @@ defmodule Inspect.MapTest do
'''
try do
inspect(%Failing{name: "Foo"}, safe: false)
inspect(%Failing{name: "Foo"}, safe: false, custom_options: [sort_maps: true])
rescue
exception in Inspect.Error ->
assert Exception.message(exception) =~ exception_message
@@ -552,14 +561,16 @@ defmodule Inspect.MapTest do
while inspecting:
%{__struct__: Inspect.MapTest.Failing, name: "Foo"}
'''
try do
Enum.to_list(%Failing{name: "Foo"})
rescue
exception in Protocol.UndefinedError ->
assert Exception.message(exception) =~ exception_message
message = Exception.message(exception)
assert message =~ exception_message
assert message =~ "__struct__: Inspect.MapTest.Failing"
assert message =~ "name: \"Foo\""
assert [
{Enumerable, :impl_for!, 1, _} | _
@@ -617,7 +628,7 @@ defmodule Inspect.MapTest do
)
)
assert inspect(%Failing{name: "Foo"}) =~ message
assert inspect(%Failing{name: "Foo"}, custom_options: [sort_maps: true]) =~ message
assert inspected =~ message
end
@@ -115,6 +115,36 @@ defmodule Kernel.ComprehensionTest do
assert for(x <- 1..3, nilly(), do: x * 2) == []
end
test "for comprehensions with unique option where value is not used" do
assert capture_io(:stderr, fn ->
assert capture_io(fn ->
Code.eval_quoted(
quote do
for x <- [1, 2, 1, 2], uniq: true, do: IO.puts(x)
nil
end
)
end) ==
"1\n2\n1\n2\n"
end) =~
"the :uniq option has no effect since the result of the for comprehension is not used"
end
test "for comprehensions with unique option where value is assigned to _" do
assert capture_io(:stderr, fn ->
assert capture_io(fn ->
Code.eval_quoted(
quote do
_ = for x <- [1, 2, 1, 2], uniq: true, do: IO.puts(x)
nil
end
)
end) ==
"1\n2\n1\n2\n"
end) =~
"the :uniq option has no effect since the result of the for comprehension is not used"
end
test "for comprehensions with errors on filters" do
assert_raise ArgumentError, fn ->
for x <- 1..3, hd(x), do: x * 2
@@ -22,7 +22,26 @@ defmodule Kernel.DialyzerTest do
end
# Add a few key Elixir modules for types and macro functions
mods = [:elixir, :elixir_env, Atom, Enum, Exception, Kernel, Macro, Macro.Env, String]
mods = [
:elixir,
:elixir_env,
ArgumentError,
Atom,
Enum,
Exception,
ExUnit.AssertionError,
ExUnit.Assertions,
IO,
Kernel,
Kernel.Utils,
List,
Macro,
Macro.Env,
Module,
String,
String.Chars
]
files = Enum.map(mods, &:code.which/1)
dialyzer_run(analysis_type: :plt_build, output_plt: plt, apps: [:erts], files: files)
@@ -102,6 +121,7 @@ defmodule Kernel.DialyzerTest do
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)
@@ -204,6 +204,23 @@ defmodule Kernel.ErrorsTest do
end
end
test "bad struct on module conflict" do
Code.put_compiler_option(:ignore_module_conflict, true)
assert_eval_raise CompileError, ~r'MissingStructOnReload\.__struct__/1 is undefined', ~c'''
defmodule MissingStructOnReload do
defstruct [:title]
def d(), do: %MissingStructOnReload{}
end
defmodule MissingStructOnReload do
def d(), do: %MissingStructOnReload{}
end
'''
after
Code.put_compiler_option(:ignore_module_conflict, false)
end
test "missing struct key" do
missing_struct_key_error =
~r"expected Kernel.ErrorsTest.MissingStructKey.__struct__/(0|1) to return a map.*, got: %\{\}"
+101 -84
View File
@@ -13,76 +13,6 @@ end
defmodule Kernel.ExpansionTest do
use ExUnit.Case, async: true
defmacrop var_ver(var, version) do
quote do
{unquote(var), [version: unquote(version)], __MODULE__}
end
end
test "tracks variable version" do
assert {:__block__, _, [{:=, _, [var_ver(:x, 0), 0]}, {:=, _, [_, var_ver(:x, 0)]}]} =
expand_with_version(
quote do
x = 0
_ = x
end
)
assert {:__block__, _,
[
{:=, _, [var_ver(:x, 0), 0]},
{:=, _, [_, var_ver(:x, 0)]},
{:=, _, [var_ver(:x, 1), 1]},
{:=, _, [_, var_ver(:x, 1)]}
]} =
expand_with_version(
quote do
x = 0
_ = x
x = 1
_ = x
end
)
assert {:__block__, _,
[
{:=, _, [var_ver(:x, 0), 0]},
{:fn, _, [{:->, _, [[var_ver(:x, 1)], {:=, _, [var_ver(:x, 2), 2]}]}]},
{:=, _, [_, var_ver(:x, 0)]},
{:=, _, [var_ver(:x, 3), 3]}
]} =
expand_with_version(
quote do
x = 0
fn x -> x = 2 end
_ = x
x = 3
end
)
assert {:__block__, _,
[
{:=, _, [var_ver(:x, 0), 0]},
{:case, _, [:foo, [do: [{:->, _, [[var_ver(:x, 1)], var_ver(:x, 1)]}]]]},
{:=, _, [_, var_ver(:x, 0)]},
{:=, _, [var_ver(:x, 2), 2]}
]} =
expand_with_version(
quote do
x = 0
case(:foo, do: (x -> x))
_ = x
x = 2
end
)
end
defp expand_with_version(expr) do
env = :elixir_env.reset_vars(__ENV__)
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
expr
end
describe "__block__" do
test "expands to nil when empty" do
assert expand(quote(do: unquote(:__block__)())) == nil
@@ -328,12 +258,30 @@ defmodule Kernel.ExpansionTest do
end
describe "vars" do
test "expand to local call" do
{output, env} = expand_env(quote(do: a), __ENV__)
assert output == quote(do: a())
test "expands vars to local call" do
{output, env} = expand_env({:a, [], nil}, __ENV__, [])
assert output == {:a, [if_undefined: :warn], []}
assert Macro.Env.vars(env) == []
end
test "expands vars to local call without warning" do
env = __ENV__
{output, _, env} =
:elixir_expand.expand({:a, [if_undefined: :apply], nil}, :elixir_env.env_to_ex(env), env)
assert output == {:a, [if_undefined: :apply], []}
assert Macro.Env.vars(env) == []
end
test "raises when expanding var to local call" do
env = __ENV__
assert_raise CompileError, ~r"undefined variable \"a\"", fn ->
:elixir_expand.expand({:a, [if_undefined: :raise], nil}, :elixir_env.env_to_ex(env), env)
end
end
test "forces variable to exist" do
code =
quote do
@@ -361,6 +309,76 @@ defmodule Kernel.ExpansionTest do
expand(quote(do: {1, 2, _}))
end
end
defmacrop var_ver(var, version) do
quote do
{unquote(var), [version: unquote(version)], __MODULE__}
end
end
defp expand_with_version(expr) do
env = :elixir_env.reset_vars(__ENV__)
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
expr
end
test "tracks variable version" do
assert {:__block__, _, [{:=, _, [var_ver(:x, 0), 0]}, {:=, _, [_, var_ver(:x, 0)]}]} =
expand_with_version(
quote do
x = 0
_ = x
end
)
assert {:__block__, _,
[
{:=, _, [var_ver(:x, 0), 0]},
{:=, _, [_, var_ver(:x, 0)]},
{:=, _, [var_ver(:x, 1), 1]},
{:=, _, [_, var_ver(:x, 1)]}
]} =
expand_with_version(
quote do
x = 0
_ = x
x = 1
_ = x
end
)
assert {:__block__, _,
[
{:=, _, [var_ver(:x, 0), 0]},
{:fn, _, [{:->, _, [[var_ver(:x, 1)], {:=, _, [var_ver(:x, 2), 2]}]}]},
{:=, _, [_, var_ver(:x, 0)]},
{:=, _, [var_ver(:x, 3), 3]}
]} =
expand_with_version(
quote do
x = 0
fn x -> x = 2 end
_ = x
x = 3
end
)
assert {:__block__, _,
[
{:=, _, [var_ver(:x, 0), 0]},
{:case, _, [:foo, [do: [{:->, _, [[var_ver(:x, 1)], var_ver(:x, 1)]}]]]},
{:=, _, [_, var_ver(:x, 0)]},
{:=, _, [var_ver(:x, 2), 2]}
]} =
expand_with_version(
quote do
x = 0
case(:foo, do: (x -> x))
_ = x
x = 2
end
)
end
end
describe "^" do
@@ -798,7 +816,8 @@ defmodule Kernel.ExpansionTest do
c = 3
[4]
),
do: {a(), b, c(), d}
do: {a(), b, c(), d},
into: []
)
end
@@ -807,7 +826,7 @@ defmodule Kernel.ExpansionTest do
test "variables inside filters are available in blocks" do
assert expand(quote(do: for(a <- b, c = a, do: c))) ==
quote(do: for(a <- b(), c = a, do: c))
quote(do: for(a <- b(), c = a, do: c, into: []))
end
test "variables inside options do not leak" do
@@ -2890,15 +2909,13 @@ defmodule Kernel.ExpansionTest do
expand_env(expr, __ENV__) |> elem(0)
end
defp expand_env(expr, env) do
ExUnit.CaptureIO.capture_io(:stderr, fn ->
send(self(), {:expand_env, :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)})
end)
defp expand_env(expr, env, to_clean \\ [:version, :inferred_bitstring_spec, :if_undefined]) do
{{expr, scope, env}, _capture} =
ExUnit.CaptureIO.with_io(:stderr, fn ->
:elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
end)
receive do
{:expand_env, {expr, scope, env}} ->
env = :elixir_env.to_caller({env.line, scope, env})
{clean_meta(expr, [:version, :inferred_bitstring_spec]), env}
end
env = :elixir_env.to_caller({env.line, scope, env})
{clean_meta(expr, to_clean), env}
end
end
@@ -162,7 +162,7 @@ defmodule Kernel.LexicalTrackerTest do
{{compile, _, _, _}, _binding} =
Code.eval_string("""
defmodule Kernel.LexicalTrackerTest.Attribute1 do
@example [String, Enum]
@example [String, Enum, 3 + 10]
def foo(atom) when atom in @example, do: atom
Kernel.LexicalTracker.references(__ENV__.lexical_tracker)
end |> elem(3)
@@ -226,6 +226,18 @@ defmodule Kernel.LexicalTrackerTest do
assert String in compile
assert Enum in compile
{{compile, _, _, _}, _binding} =
Code.eval_string("""
defmodule Kernel.LexicalTrackerTest.Attribute6 do
@example %{foo: Application.compile_env(:elixir, Enum, String)}
def foo(atom) when atom == @example.foo, do: atom
Kernel.LexicalTracker.references(__ENV__.lexical_tracker)
end |> elem(3)
""")
refute String in compile
refute Enum in compile
end
test "@compile adds a runtime dependency" do
+1 -1
View File
@@ -21,7 +21,7 @@ defmodule Kernel.QuoteTest do
line = __ENV__.line + 2
assert quote(location: :keep, do: bar(1, 2, 3)) ==
{:bar, [keep: {Path.relative_to_cwd(__ENV__.file), line}], [1, 2, 3]}
{:bar, [keep: {__ENV__.file, line}], [1, 2, 3]}
end
test "fixed line" do
@@ -411,6 +411,7 @@ defmodule Kernel.RaiseTest do
fun = BadFunction.Missing.fun()
:code.delete(BadFunction.Missing)
:code.purge(BadFunction.Missing)
defmodule BadFunction.Missing do
def fun, do: fn -> :another end
+40 -5
View File
@@ -172,10 +172,18 @@ defmodule MacroTest do
end
test "env" do
env = %{__ENV__ | line: 0}
assert Macro.expand_once(quote(do: __ENV__), env) == {:%{}, [], Map.to_list(env)}
env = %{__ENV__ | line: 0, lexical_tracker: self()}
expanded = Macro.expand_once(quote(do: __ENV__), env)
assert Macro.validate(expanded) == :ok
assert Code.eval_quoted(expanded) == {env, []}
assert Macro.expand_once(quote(do: __ENV__.file), env) == env.file
assert Macro.expand_once(quote(do: __ENV__.unknown), env) == quote(do: __ENV__.unknown)
expanded = Macro.expand_once(quote(do: __ENV__.versioned_vars), env)
assert Macro.validate(expanded) == :ok
assert Code.eval_quoted(expanded) == {env.versioned_vars, []}
end
defmacro local_macro(), do: raise("ignored")
@@ -269,9 +277,26 @@ defmodule MacroTest do
assert expand_and_clean(quote(do: oror(1, false)), __ENV__) == quoted
end
test "expand_literal" do
assert Macro.expand_literal(quote(do: Foo), __ENV__) == Foo
assert Macro.expand_literal(quote(do: Foo + Bar), __ENV__) == quote(do: Foo + Bar)
test "expand_literals/2" do
assert Macro.expand_literals(quote(do: Foo), __ENV__) == Foo
assert Macro.expand_literals(quote(do: Foo + Bar), __ENV__) == quote(do: Foo + Bar)
assert Macro.expand_literals(quote(do: __MODULE__), __ENV__) == __MODULE__
assert Macro.expand_literals(quote(do: __MODULE__.Foo), __ENV__) == __MODULE__.Foo
assert Macro.expand_literals(quote(do: [Foo, 1 + 2]), __ENV__) == [Foo, quote(do: 1 + 2)]
end
test "expand_literals/3" do
fun = fn node, acc ->
expanded = Macro.expand(node, __ENV__)
{expanded, [expanded | acc]}
end
assert Macro.expand_literals(quote(do: Foo), [], fun) == {Foo, [Foo]}
assert Macro.expand_literals(quote(do: Foo + Bar), [], fun) == {quote(do: Foo + Bar), []}
assert Macro.expand_literals(quote(do: __MODULE__), [], fun) == {__MODULE__, [__MODULE__]}
assert Macro.expand_literals(quote(do: __MODULE__.Foo), [], fun) ==
{__MODULE__.Foo, [__MODULE__.Foo, __MODULE__]}
end
test "var/2" do
@@ -404,6 +429,16 @@ defmodule MacroTest do
test "converts quoted to string" do
assert Macro.to_string(quote do: hello(world)) == "hello(world)"
end
test "large number literals" do
# with quote
assert Macro.to_string(quote do: 576_460_752_303_423_455) == "576_460_752_303_423_455"
assert Macro.to_string(quote do: -576_460_752_303_423_455) == "-576_460_752_303_423_455"
# without quote
assert Macro.to_string(576_460_752_303_423_455) == "576_460_752_303_423_455"
assert Macro.to_string(-576_460_752_303_423_455) == "-576_460_752_303_423_455"
end
end
describe "to_string/2" do
+5 -1
View File
@@ -206,7 +206,7 @@ defmodule MapTest do
assert Map.replace!(map, :b, 10) == %{c: 3, b: 10, a: 1}
assert Map.replace!(map, :a, 1) == map
assert_raise KeyError, "key :x not found in: %{a: 1, b: 2, c: 3}", fn ->
assert_raise KeyError, ~r/key :x not found in: %{.*a: 1.*}/, fn ->
Map.replace!(map, :x, 10)
end
@@ -393,6 +393,10 @@ defmodule MapTest do
defmodule TestMod do
@enforce_keys :foo
defstruct [:foo]
# Verify it remain set afterwards
:foo = @enforce_keys
def foo do
%TestMod{}
end
@@ -562,6 +562,22 @@ defmodule Module.Types.IntegrationTest do
end
end
describe "regressions" do
test "handle missing location info from quoted" do
assert capture_io(:stderr, fn ->
quote do
defmodule X do
def f() do
x = %{}
%{x | key: :value}
end
end
end
|> Code.compile_quoted()
end) =~ "warning:"
end
end
describe "after_verify" do
test "reports functions" do
files = %{
@@ -140,25 +140,25 @@ defmodule Module.Types.PatternTest do
end
test "struct" do
assert quoted_pattern(%:"Elixir.Module.Types.PatternTest.Struct"{}) ==
{:ok,
{:map,
[
{:required, {:atom, :__struct__}, {:atom, Module.Types.PatternTest.Struct}},
{:required, {:atom, :bar}, :dynamic},
{:required, {:atom, :baz}, :dynamic},
{:required, {:atom, :foo}, :dynamic}
]}}
assert {:ok, {:map, fields}} = quoted_pattern(%:"Elixir.Module.Types.PatternTest.Struct"{})
assert quoted_pattern(%:"Elixir.Module.Types.PatternTest.Struct"{foo: 123, bar: :atom}) ==
{:ok,
{:map,
[
{:required, {:atom, :foo}, :integer},
{:required, {:atom, :bar}, {:atom, :atom}},
{:required, {:atom, :__struct__}, {:atom, Module.Types.PatternTest.Struct}},
{:required, {:atom, :baz}, :dynamic}
]}}
assert Enum.sort(fields) == [
{:required, {:atom, :__struct__}, {:atom, Module.Types.PatternTest.Struct}},
{:required, {:atom, :bar}, :dynamic},
{:required, {:atom, :baz}, :dynamic},
{:required, {:atom, :foo}, :dynamic}
]
assert {:ok, {:map, fields}} =
quoted_pattern(%:"Elixir.Module.Types.PatternTest.Struct"{foo: 123, bar: :atom})
assert Enum.sort(fields) ==
[
{:required, {:atom, :__struct__}, {:atom, Module.Types.PatternTest.Struct}},
{:required, {:atom, :bar}, {:atom, :atom}},
{:required, {:atom, :baz}, :dynamic},
{:required, {:atom, :foo}, :integer}
]
end
test "struct var" do
@@ -222,7 +222,6 @@ defmodule Module.Types.PatternTest do
assert quoted_pattern(x = y = 123) == {:ok, :integer}
assert quoted_pattern(x = 123 = y) == {:ok, :integer}
assert quoted_pattern(123 = x = y) == {:ok, :integer}
assert {:error, {:unable_unify, {{:tuple, 1, [var: 0]}, {:var, 0}, _}}} =
quoted_pattern({x} = x)
@@ -296,15 +295,6 @@ defmodule Module.Types.PatternTest do
assert quoted_head([x], [is_atom(x) > :foo]) == {:ok, [var: 0]}
assert quoted_head([x, x = y, y = z], [is_atom(x)]) ==
{:ok, [:atom, :atom, :atom]}
assert quoted_head([x = y, y, y = z], [is_atom(y)]) ==
{:ok, [:atom, :atom, :atom]}
assert quoted_head([x = y, y = z, z], [is_atom(z)]) ==
{:ok, [:atom, :atom, :atom]}
assert quoted_head([x, y], [is_atom(x) or is_integer(y)]) ==
{:ok, [{:var, 0}, {:var, 1}]}
@@ -320,15 +310,6 @@ defmodule Module.Types.PatternTest do
assert quoted_head([x, y], [is_atom(y) or is_integer(y)]) ==
{:ok, [{:var, 0}, {:union, [:atom, :integer]}]}
assert quoted_head([x = y], [is_atom(y) or is_integer(y)]) ==
{:ok, [{:union, [:atom, :integer]}]}
assert quoted_head([x = y], [is_atom(x) or is_integer(x)]) ==
{:ok, [{:union, [:atom, :integer]}]}
assert quoted_head([x = y], [is_atom(x) or is_integer(x)]) ==
{:ok, [{:union, [:atom, :integer]}]}
assert quoted_head([x], [true == false or is_integer(x)]) ==
{:ok, [var: 0]}
@@ -187,36 +187,6 @@ defmodule Module.Types.TypesTest do
"""
end
test "warns on guards with multiple variables" do
string = warning([x = y], [is_integer(x) and is_binary(y)], {x, y})
assert string == """
incompatible types:
integer() !~ binary()
in expression:
# types_test.ex:1
is_binary(y)
where "x" was given the same type as "y" in:
# types_test.ex:1
x = y
where "y" was given the type integer() in:
# types_test.ex:1
is_integer(x)
where "y" was given the type binary() in:
# types_test.ex:1
is_binary(y)
"""
end
test "warns on guards from cases unless generated" do
string =
warning(
@@ -243,36 +213,6 @@ defmodule Module.Types.TypesTest do
assert string == :none
end
test "only show relevant traces in warning" do
string = warning([x = y, z], [is_integer(x) and is_binary(y) and is_boolean(z)], {x, y, z})
assert string == """
incompatible types:
integer() !~ binary()
in expression:
# types_test.ex:1
is_binary(y)
where "x" was given the same type as "y" in:
# types_test.ex:1
x = y
where "y" was given the type integer() in:
# types_test.ex:1
is_integer(x)
where "y" was given the type binary() in:
# types_test.ex:1
is_binary(y)
"""
end
test "check body" do
string = warning([x], [is_integer(x)], :foo = x)
@@ -417,6 +357,25 @@ defmodule Module.Types.TypesTest do
foo - :bar
"""
end
test "rewrite call" do
string = warning([foo], [is_map_key(1, foo)], foo)
assert string == """
expected Kernel.is_map_key/2 to have signature:
integer(), var1 -> dynamic()
but it has signature:
%{optional(dynamic()) => dynamic()}, dynamic() -> dynamic()
in expression:
# types_test.ex:1
is_map_key(1, foo)
"""
end
end
describe "map warnings" do
+7
View File
@@ -332,6 +332,13 @@ defmodule ModuleTest do
assert Elixir.ModuleTest.NonAtomAlias.hello() == :world
end
test "does not warn on captured underscored vars" do
_unused = 123
defmodule __MODULE__.NoVarWarning do
end
end
@compile {:no_warn_undefined, ModuleCreateSample}
test "create" do
+15
View File
@@ -38,6 +38,21 @@ defmodule PathTest do
File.rm_rf(Path.join(config.tmp_dir, "wildcard"))
end
@tag :tmp_dir
test "wildcard/2 follows ..", config do
hello = Path.join(config.tmp_dir, "wildcard/hello")
world = Path.join(config.tmp_dir, "wildcard/world")
File.mkdir_p(hello)
File.touch(world)
assert Path.wildcard(Path.join(config.tmp_dir, "wildcard/w*/../h*")) == []
assert Path.wildcard(Path.join(config.tmp_dir, "wildcard/h*/../w*")) ==
[Path.join(config.tmp_dir, "wildcard/hello/../world")]
after
File.rm_rf(Path.join(config.tmp_dir, "wildcard"))
end
test "wildcard/2 raises on null byte" do
assert_raise ArgumentError, ~r/null byte/, fn -> Path.wildcard("foo\0bar") end
end
+9
View File
@@ -74,6 +74,14 @@ defmodule ProtocolTest do
end
end
defmodule ImplStructExplicitFor do
defstruct a: 0, b: 0
defimpl Sample, for: __MODULE__ do
def ok(_struct), do: true
end
end
test "protocol implementations without any" do
assert is_nil(Sample.impl_for(:foo))
assert is_nil(Sample.impl_for(fn x -> x end))
@@ -91,6 +99,7 @@ defmodule ProtocolTest do
assert is_nil(Sample.impl_for(make_ref()))
assert Sample.impl_for(%ImplStruct{}) == Sample.ProtocolTest.ImplStruct
assert Sample.impl_for(%ImplStructExplicitFor{}) == Sample.ProtocolTest.ImplStructExplicitFor
assert Sample.impl_for(%NoImplStruct{}) == nil
end
+2
View File
@@ -1340,6 +1340,8 @@ defmodule StreamTest do
assert Stream.chunk_every([0, 1, 2, 3], 2) |> Stream.zip() |> Enum.to_list() ==
[{0, 2}, {1, 3}]
assert Stream.zip([]) |> Enum.to_list() == []
stream = %HaltAcc{acc: 1..3}
assert Stream.zip([1..3, stream]) |> Enum.to_list() == [{1, 1}, {2, 2}, {3, 3}]
+82 -1
View File
@@ -107,6 +107,20 @@ defmodule StringTest do
assert String.split("x-x-", pattern, parts: 3, trim: true) == ["x", "x"]
end
test "split/2,3 with malformed" do
assert String.split(<<225, 158, 128, 225, 158, 185, 225>>, "", parts: 1) ==
[<<225, 158, 128, 225, 158, 185, 225>>]
assert String.split(<<225, 158, 128, 225, 158, 185, 225>>, "", parts: 2) ==
["", <<225, 158, 128, 225, 158, 185, 225>>]
assert String.split(<<225, 158, 128, 225, 158, 185, 225>>, "", parts: 3) ==
["", "កឹ", <<225>>]
assert String.split(<<225, 158, 128, 225, 158, 185, 225>>, "", parts: 4) ==
["", "កឹ", <<225>>, ""]
end
test "splitter/2,3" do
assert String.splitter("a,b,c", ",") |> Enum.to_list() == ["a", "b", "c"]
assert String.splitter("a,b", ".") |> Enum.to_list() == ["a,b"]
@@ -155,9 +169,18 @@ defmodule StringTest do
end
end
test "split_at/2 with invalid guard" do
test "split_at/2 with malformed" do
assert String.split_at(<<?a, 195, 10, ?a>>, 2) == {<<?a, 195>>, <<10, ?a>>}
assert String.split_at(<<107, 205, 135, 184>>, 1) == {<<107, 205, 135>>, <<184>>}
assert String.split_at(<<225, 158, 128, 225, 158, 185, 225>>, 0) ==
{"", <<225, 158, 128, 225, 158, 185, 225>>}
assert String.split_at(<<225, 158, 128, 225, 158, 185, 225>>, 1) ==
{"កឹ", <<225>>}
assert String.split_at(<<225, 158, 128, 225, 158, 185, 225>>, 2) ==
{<<225, 158, 128, 225, 158, 185, 225>>, ""}
end
test "upcase/1" do
@@ -457,6 +480,10 @@ defmodule StringTest do
end
end
test "with empty string and string replacement with malformed" do
assert String.replace(<<225, 158, 128, 225, 158, 185, 225>>, "", ".") == ".កឹ.\xE1."
end
test "with empty pattern list" do
assert String.replace("elixir", [], "anything") == "elixir"
end
@@ -587,6 +614,60 @@ defmodule StringTest do
assert String.next_grapheme("") == nil
end
describe "randomized" do
test "next_grapheme" do
for _ <- 1..10 do
bin = :crypto.strong_rand_bytes(20)
try do
bin |> Stream.unfold(&String.next_grapheme/1) |> Enum.to_list()
rescue
# Ignore malformed pictographic sequences
_ -> :ok
else
list ->
assert Enum.all?(list, &is_binary/1), "cannot build graphemes for #{inspect(bin)}"
end
end
end
test "split empty" do
for _ <- 1..10 do
bin = :crypto.strong_rand_bytes(20)
try do
String.split(bin, "")
rescue
# Ignore malformed pictographic sequences
_ -> :ok
else
split ->
assert Enum.all?(split, &is_binary/1), "cannot split #{inspect(bin)}"
assert IO.iodata_to_binary(split) == bin
end
end
end
test "graphemes" do
for _ <- 1..10 do
bin = :crypto.strong_rand_bytes(20)
try do
String.graphemes(bin)
rescue
# Ignore malformed pictographic sequences
_ -> :ok
else
graphemes ->
assert Enum.all?(graphemes, &is_binary/1),
"cannot build graphemes for #{inspect(bin)}"
assert IO.iodata_to_binary(graphemes) == bin
end
end
end
end
test "first/1" do
assert String.first("elixir") == "e"
assert String.first("íelixr") == "í"
+1 -1
View File
@@ -203,7 +203,7 @@ defmodule SystemTest do
@tag timeout: 1_000
test "shell/1 returns when command awaits input" do
assert {"", 0} = System.shell("cat")
assert {"", 0} = System.shell("cat", close_stdin: true)
end
test "shell/1 with comment" do
+25
View File
@@ -279,6 +279,31 @@ defmodule URITest do
end
end
test "http://http://http://@http://http://?http://#http://" do
assert URI.parse("http://http://http://@http://http://?http://#http://") ==
%URI{
scheme: "http",
authority: "http:",
userinfo: nil,
host: "http",
port: 80,
path: "//http://@http://http://",
query: "http://",
fragment: "http://"
}
assert URI.new!("http://http://http://@http://http://?http://#http://") ==
%URI{
scheme: "http",
userinfo: nil,
host: "http",
port: 80,
path: "//http://@http://http://",
query: "http://",
fragment: "http://"
}
end
test "default_port/1,2" do
assert URI.default_port("http") == 80
+2 -2
View File
@@ -3,8 +3,8 @@
-include_lib("eunit/include/eunit.hrl").
eval(Content) ->
{Value, Binding, _} =
elixir:eval_forms(elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []), [], []),
Quoted = elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []),
{Value, Binding, _} = elixir:eval_forms(Quoted, [], elixir:env_for_eval([])),
{Value, Binding}.
kv([{Key, nil}]) -> Key.
+2 -2
View File
@@ -2,8 +2,8 @@
-include_lib("eunit/include/eunit.hrl").
eval(Content) ->
{Value, Binding, _} =
elixir:eval_forms(elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []), [], []),
Quoted = elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []),
{Value, Binding, _} = elixir:eval_forms(Quoted, [], elixir:env_for_eval([])),
{Value, lists:sort(Binding)}.
function_arg_do_end_test() ->
+3 -3
View File
@@ -3,8 +3,8 @@
-include_lib("eunit/include/eunit.hrl").
eval(Content) ->
{Value, Binding, _} =
elixir:eval_forms(elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []), [], []),
Quoted = elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []),
{Value, Binding, _} = elixir:eval_forms(Quoted, [], elixir:env_for_eval([])),
{Value, Binding}.
extract_interpolations(String) ->
@@ -22,7 +22,7 @@ extract_interpolations_without_interpolation_test() ->
extract_interpolations_with_escaped_interpolation_test() ->
["f\\#{o}o"] = extract_interpolations("f\\#{o}o"),
{1, 8, ["f\\#{o}o"], [], _} =
{1, 10, ["f\\#{o}o"], [], _} =
elixir_interpolation:extract(1, 2, #elixir_tokenizer{}, true, "f\\#{o}o\"", $").
extract_interpolations_with_interpolation_test() ->
@@ -199,6 +199,11 @@ interpolation_test() ->
{concat_op, {1, 10, nil}, '<>'},
{bin_string, {1, 13, nil}, [<<>>]}] = tokenize("\"f#{oo}\" <> \"\"").
escaped_interpolation_test() ->
[{bin_string, {1, 1, nil}, [<<"f#{oo}">>]},
{concat_op, {1, 11, nil}, '<>'},
{bin_string, {1, 14, nil}, [<<>>]}] = tokenize("\"f\\#{oo}\" <> \"\"").
capture_test() ->
% Parens precedence
[{capture_op, {1, 1, nil}, '&'},
+3 -3
View File
@@ -201,7 +201,7 @@ defmodule ExUnit do
System.at_exit(fn
0 ->
time = ExUnit.Server.modules_loaded()
time = ExUnit.Server.modules_loaded(false)
options = persist_defaults(configuration())
%{failures: failures} = ExUnit.Runner.run(options, time)
@@ -373,7 +373,7 @@ defmodule ExUnit do
end
end
_ = ExUnit.Server.modules_loaded()
_ = ExUnit.Server.modules_loaded(additional_modules != [])
options = persist_defaults(configuration())
ExUnit.Runner.run(options, nil)
end
@@ -399,7 +399,7 @@ defmodule ExUnit do
@doc since: "1.12.0"
@spec await_run(Task.t()) :: suite_result()
def await_run(task) do
ExUnit.Server.modules_loaded()
ExUnit.Server.modules_loaded(false)
Task.await(task, :infinity)
end
+6 -4
View File
@@ -260,19 +260,21 @@ defmodule ExUnit.Assertions do
quote do
left = unquote(left)
right = unquote(right)
expr = unquote(expr)
message = unquote(message)
if ExUnit.Assertions.__equal__?(left, right) do
ExUnit.Assertions.assert(false,
left: left,
expr: unquote(expr),
message: unquote(message <> ", both sides are exactly equal")
expr: expr,
message: message <> ", both sides are exactly equal"
)
else
ExUnit.Assertions.assert(unquote(call),
left: left,
right: right,
expr: unquote(expr),
message: unquote(message),
expr: expr,
message: message,
context: unquote(context)
)
end
+5 -2
View File
@@ -551,8 +551,11 @@ defmodule ExUnit.Callbacks do
@doc """
Same as `start_supervised!/2` but links the started process to the test process.
This means that if the process that was started crashes that crash is propagated to
the test process, failing the test and printing the cause of the crash.
If the process that was started crashes, the crash is propagated to the test process,
failing the test and printing the cause of the crash.
Note that if the started terminates before it is linked to the test process,
this function will exit with reason `:noproc`.
"""
@doc since: "1.14.0"
@spec start_link_supervised!(Supervisor.child_spec() | module | {module, term}, keyword) ::
+29 -15
View File
@@ -20,6 +20,9 @@ defmodule ExUnit.Case do
It should be enabled only if tests do not change any global state.
Defaults to `false`.
* `:register` - when `false`, does not register this module within
ExUnit server. This means the module won't run when ExUnit suite runs.
This module automatically includes all callbacks defined in
`ExUnit.Callbacks`. See that module for more information on `setup`,
`start_supervised`, `on_exit` and the test process life cycle.
@@ -156,6 +159,10 @@ defmodule ExUnit.Case do
* `:describe_line` - the line the describe block begins on
* `:doctest` - the module or the file being doctested (if a doctest)
* `:doctest_line` - the line the doctest was defined (if a doctest)
The following tags customize how tests behave:
* `:capture_log` - see the "Log Capture" section below
@@ -167,8 +174,8 @@ defmodule ExUnit.Case do
* `:tmp_dir` - (since v1.11.0) see the "Tmp Dir" section below
The `:test_type` tag is automatically set by ExUnit, but is **not** reserved.
This tag is available for users to customize if they desire.
* `:test_type` - the test type used when printing test results.
It is set by ExUnit to `:test`, `:doctest` and so on, but is customizable.
## Filters
@@ -284,7 +291,7 @@ defmodule ExUnit.Case do
raise "you must set @tag, @describetag, and @moduletag after the call to \"use ExUnit.Case\""
end
attributes = [
accumulate_attributes = [
:ex_unit_tests,
:tag,
:describetag,
@@ -294,24 +301,23 @@ defmodule ExUnit.Case do
:ex_unit_registered_module_attributes
]
Enum.each(attributes, &Module.register_attribute(module, &1, accumulate: true))
Enum.each(accumulate_attributes, &Module.register_attribute(module, &1, accumulate: true))
persisted_attributes = [:ex_unit_async]
Enum.each(persisted_attributes, &Module.register_attribute(module, &1, persist: true))
attributes = [
before_compile: ExUnit.Case,
after_compile: ExUnit.Case,
ex_unit_async: false
]
if Keyword.get(opts, :register, true) do
Module.put_attribute(module, :after_compile, ExUnit.Case)
end
Enum.each(attributes, fn {k, v} -> Module.put_attribute(module, k, v) end)
Module.put_attribute(module, :before_compile, ExUnit.Case)
end
async? = opts[:async]
if is_boolean(async?) do
Module.put_attribute(module, :ex_unit_async, async?)
if is_boolean(async?) or not registered? do
Module.put_attribute(module, :ex_unit_async, async? || false)
end
registered?
@@ -477,10 +483,16 @@ defmodule ExUnit.Case do
end
@doc false
defmacro __before_compile__(_) do
defmacro __before_compile__(env) do
tests =
env.module
|> Module.get_attribute(:ex_unit_tests)
|> Enum.reverse()
|> Macro.escape()
quote do
def __ex_unit__ do
%ExUnit.TestModule{file: __ENV__.file, name: __MODULE__, tests: @ex_unit_tests}
%ExUnit.TestModule{file: __ENV__.file, name: __MODULE__, tests: unquote(tests)}
end
end
end
@@ -582,6 +594,7 @@ defmodule ExUnit.Case do
This function is deprecated in favor of `register_test/6` which performs
better under tight loops by avoiding `__ENV__`.
"""
# TODO: Deprecate on Elixir v1.17
@doc deprecated: "Use register_test/6 instead"
def register_test(%{module: mod, file: file, line: line}, test_type, name, tags) do
register_test(mod, file, line, test_type, name, tags)
@@ -759,8 +772,9 @@ defmodule ExUnit.Case do
defp normalize_tags(tags) do
Enum.reduce(Enum.reverse(tags), %{}, fn
{key, value}, acc -> Map.put(acc, key, value)
tag, acc when is_atom(tag) -> Map.put(acc, tag, true)
tag, acc when is_list(tag) -> tag |> Enum.into(acc)
tag, acc when is_list(tag) -> Enum.into(tag, acc)
end)
end
end
+3 -1
View File
@@ -337,7 +337,9 @@ defmodule ExUnit.CLIFormatter do
end
defp format_test_type_counts(%{test_counter: test_counter} = _config) do
Enum.map(test_counter, fn {test_type, count} ->
test_counter
|> Enum.sort()
|> Enum.map(fn {test_type, count} ->
type_pluralized = pluralize(count, test_type, ExUnit.plural_rule(test_type |> to_string()))
"#{count} #{type_pluralized}, "
end)
+49 -56
View File
@@ -193,25 +193,27 @@ defmodule ExUnit.DocTest do
This macro is auto-imported with every `ExUnit.Case`.
"""
defmacro doctest(module, opts \\ []) do
caller = __CALLER__
require =
if is_atom(Macro.expand(module, __CALLER__)) do
if is_atom(Macro.expand(module, caller)) do
quote do
require unquote(module)
end
end
tests =
quote bind_quoted: [module: module, opts: opts] do
env = __ENV__
quote bind_quoted: [
module: module,
opts: opts,
env_line: caller.line,
env_file: caller.file
] do
file = ExUnit.DocTest.__file__(module)
for {name, test} <- ExUnit.DocTest.__doctests__(module, opts) do
if tags = Keyword.get(opts, :tags) do
@tag tags
end
for {name, test, tags} <- ExUnit.DocTest.__doctests__(module, opts) do
@file file
doc = ExUnit.Case.register_test(env, :doctest, name, [])
doc = ExUnit.Case.register_test(__MODULE__, env_file, env_line, :doctest, name, tags)
def unquote(doc)(_), do: unquote(test)
end
end
@@ -230,13 +232,15 @@ defmodule ExUnit.DocTest do
@doc false
def __doctests__(module, opts) do
do_import = Keyword.get(opts, :import, false)
tags = [doctest: module] ++ Keyword.get(opts, :tags, [])
import = Keyword.get(opts, :import, false)
file = module.module_info(:compile)[:source] |> Path.relative_to_cwd()
extract(module)
|> filter_by_opts(module, opts)
|> Stream.with_index()
|> Enum.map(fn {test, acc} ->
compile_test(test, module, do_import, acc + 1)
|> Enum.sort_by(& &1.line)
|> Enum.with_index(fn test, index ->
compile_test(test, module, import, index + 1, file, tags)
end)
end
@@ -244,14 +248,9 @@ defmodule ExUnit.DocTest do
except = Keyword.get(opts, :except, [])
case Keyword.fetch(opts, :only) do
{:ok, []} ->
[]
{:ok, only} ->
filter_tests(module, tests, except, only)
:error ->
Stream.reject(tests, &(&1.fun_arity in except))
{:ok, []} -> []
{:ok, only} -> filter_tests(module, tests, except, only)
:error -> Stream.reject(tests, &(&1.fun_arity in except))
end
end
@@ -264,7 +263,7 @@ defmodule ExUnit.DocTest do
{tests, fun_arities} -> {[test | tests], [test.fun_arity | fun_arities]}
end
case only -- fun_arities do
case only -- [:moduledoc | fun_arities] do
[] ->
filtered_tests
@@ -287,8 +286,9 @@ defmodule ExUnit.DocTest do
## Compilation of extracted tests
defp compile_test(test, module, do_import, n) do
{test_name(test, module, n), test_content(test, module, do_import)}
defp compile_test(test, module, do_import, n, file, tags) do
tags = [doctest_line: test.line] ++ tags
{test_name(test, module, n), test_content(test, module, do_import, file), tags}
end
defp test_name(%{fun_arity: :moduledoc}, m, n) do
@@ -299,8 +299,7 @@ defmodule ExUnit.DocTest do
"#{inspect(m)}.#{f}/#{a} (#{n})"
end
defp test_content(%{exprs: exprs, line: line}, module, do_import) do
file = module.module_info(:compile)[:source] |> Path.relative_to_cwd()
defp test_content(%{exprs: exprs, line: line}, module, do_import, file) do
location = [line: line, file: file]
stack = Macro.escape([{module, :__MODULE__, 0, location}])
@@ -314,8 +313,8 @@ defmodule ExUnit.DocTest do
end
tests =
Enum.map(exprs, fn {expr, expected, formatted} ->
test_case_content(expr, expected, location, stack, formatted)
Enum.map(exprs, fn {expr, expected, doctest} ->
test_case_content(expr, expected, location, stack, doctest)
end)
{:__block__, [], test_import(module, do_import) ++ tests}
@@ -328,12 +327,11 @@ defmodule ExUnit.DocTest do
end) > 1
end
defp test_case_content(expr, :test, location, stack, formatted) do
string_to_quoted(location, stack, expr, "\n" <> formatted) |> insert_assertions()
defp test_case_content(expr, :test, location, stack, doctest) do
string_to_quoted(location, stack, expr, doctest) |> insert_assertions()
end
defp test_case_content(expr, {:test, expected}, location, stack, formatted) do
doctest = "\n" <> formatted <> "\n" <> expected
defp test_case_content(expr, {:test, expected}, location, stack, doctest) do
expr_ast = string_to_quoted(location, stack, expr, doctest) |> insert_assertions()
expected_ast = string_to_quoted(location, stack, expected, doctest)
last_expr = Macro.to_string(last_expr(expr_ast))
@@ -341,56 +339,53 @@ defmodule ExUnit.DocTest do
quote do
value = unquote(expr_ast)
expected = unquote(expected_ast)
formatted = unquote(formatted)
doctest = unquote(doctest)
last_expr = unquote(last_expr)
expected_expr = unquote(expected)
stack = unquote(stack)
ExUnit.DocTest.__test__(value, expected, formatted, last_expr, expected_expr, stack)
ExUnit.DocTest.__test__(value, expected, doctest, last_expr, expected_expr, stack)
end
end
defp test_case_content(expr, {:inspect, expected}, location, stack, formatted) do
doctest = "\n" <> formatted <> "\n" <> expected
defp test_case_content(expr, {:inspect, expected}, location, stack, doctest) do
expr_ast = string_to_quoted(location, stack, expr, doctest) |> insert_assertions()
expected_ast = string_to_quoted(location, stack, expected, doctest)
last_expr = Macro.to_string(last_expr(expr_ast))
quote do
value = unquote(expr_ast)
expected = unquote(expected_ast)
formatted = unquote(formatted)
expected = unquote(expected)
doctest = unquote(doctest)
last_expr = unquote(last_expr)
expected_expr = unquote(expected)
expected_expr = unquote(inspect(expected))
stack = unquote(stack)
ExUnit.DocTest.__inspect__(value, expected, formatted, last_expr, expected_expr, stack)
ExUnit.DocTest.__inspect__(value, expected, doctest, last_expr, expected_expr, stack)
end
end
defp test_case_content(expr, {:error, exception, message}, location, stack, formatted) do
doctest = "\n" <> formatted <> "\n** (#{inspect(exception)}) #{inspect(message)}"
defp test_case_content(expr, {:error, exception, message}, location, stack, doctest) do
expr_ast = string_to_quoted(location, stack, expr, doctest)
quote do
stack = unquote(stack)
message = unquote(message)
formatted = unquote(formatted)
doctest = unquote(doctest)
exception = unquote(exception)
ExUnit.DocTest.__error__(fn -> unquote(expr_ast) end, message, exception, formatted, stack)
ExUnit.DocTest.__error__(fn -> unquote(expr_ast) end, message, exception, doctest, stack)
end
end
@doc false
def __test__(value, expected, formatted, last_expr, expected_expr, stack) do
def __test__(value, expected, doctest, last_expr, expected_expr, stack) do
case value do
^expected ->
:ok
{:ok, value}
_ ->
error = [
message: "Doctest failed",
doctest: "\n" <> formatted <> "\n" <> expected_expr,
doctest: doctest,
expr: "#{last_expr} === #{String.trim(expected_expr)}",
left: value,
right: expected
@@ -401,7 +396,7 @@ defmodule ExUnit.DocTest do
end
@doc false
def __inspect__(value, expected, formatted, last_expr, expected_expr, parent_stack) do
def __inspect__(value, expected, doctest, last_expr, expected_expr, parent_stack) do
result =
try do
inspect(value, safe: false)
@@ -416,10 +411,9 @@ defmodule ExUnit.DocTest do
case result do
:ok ->
:ok
{:ok, value}
{extra, stack} ->
doctest = "\n" <> formatted <> "\n" <> expected_expr
expr = "inspect(#{last_expr}) === #{String.trim(expected_expr)}"
error = [doctest: doctest, expr: expr] ++ extra
reraise ExUnit.AssertionError, error, stack ++ parent_stack
@@ -427,7 +421,7 @@ defmodule ExUnit.DocTest do
end
@doc false
def __error__(fun, message, exception, formatted, stack) do
def __error__(fun, message, exception, doctest, stack) do
try do
fun.()
rescue
@@ -452,12 +446,10 @@ defmodule ExUnit.DocTest do
end
if failed do
doctest = "\n" <> formatted <> "\n** (#{inspect(exception)}) #{inspect(message)}"
reraise ExUnit.AssertionError, [message: failed, doctest: doctest], stack
end
else
_ ->
doctest = "\n" <> formatted <> "\n** (#{inspect(exception)}) #{inspect(message)}"
failed = "Doctest failed: expected exception #{inspect(exception)} but nothing was raised"
error = [message: failed, doctest: doctest]
reraise ExUnit.AssertionError, error, stack
@@ -863,7 +855,8 @@ defmodule ExUnit.DocTest do
end
defp add_expr(%{exprs: exprs} = test, expr, expected, formatted) do
%{test | exprs: [{expr, tag_expected(expected), formatted} | exprs]}
doctest = "\n" <> formatted <> "\n" <> expected
%{test | exprs: [{expr, tag_expected(expected), doctest} | exprs]}
end
defp tag_expected(string) do
@@ -877,7 +870,7 @@ defmodule ExUnit.DocTest do
_ ->
if inspectable?(string) do
{:inspect, inspect(string)}
{:inspect, string}
else
{:test, string}
end
+177 -159
View File
@@ -39,9 +39,9 @@ defmodule ExUnit.Formatter do
* `{:case_finished, test_module}` -
a test module has finished. See `ExUnit.TestModule` for details.
The full ExUnit configuration is passed as the argument to `c:GenServer.init/1` callback when the
formatters are started. If you need to do runtime configuration of a
formatter, you can add any configuration needed by using `ExUnit.configure/1`
The full ExUnit configuration is passed as the argument to `c:GenServer.init/1`
callback when the formatters are started. If you need to do runtime configuration
of a formatter, you can add any configuration needed by using `ExUnit.configure/1`
or `ExUnit.start/1`, and this will then be included in the options passed to
the `c:GenServer.init/1` callback.
"""
@@ -126,7 +126,7 @@ defmodule ExUnit.Formatter do
end
end
# Deprecate me on Elixir v1.16
# TODO: Deprecate me on Elixir v1.16
@doc false
def format_time(run, load) do
format_times(%{run: run, load: load, async: nil})
@@ -165,43 +165,6 @@ defmodule ExUnit.Formatter do
end)
end
@doc false
def format_assertion_error(%ExUnit.AssertionError{} = struct) do
format_exception(%{}, struct, [], :infinity, fn _, msg -> msg end, "") |> elem(0)
end
defp format_exception(test, %ExUnit.AssertionError{} = struct, stack, width, formatter, pad) do
label_padding_size = if has_value?(struct.right), do: 7, else: 6
padding_size = label_padding_size + byte_size(@counter_padding)
code_multiline =
if struct.doctest != @no_value,
do: &pad_multiline(&1, padding_size),
else: &code_multiline(&1, padding_size)
formatted =
[
note: if_value(struct.message, &format_message(&1, formatter)),
doctest: if_value(struct.doctest, &pad_multiline(&1, 2 + byte_size(@counter_padding))),
code: if_value(struct.expr, code_multiline),
code: unless_value(struct.expr, fn -> get_code(test, stack) || @no_value end),
arguments: if_value(struct.args, &format_args(&1, width))
]
|> Kernel.++(format_context(struct, formatter, padding_size, width))
|> format_meta(formatter, pad, label_padding_size)
|> IO.iodata_to_binary()
{formatted, stack}
end
defp format_exception(test, %FunctionClauseError{} = struct, stack, _width, formatter, _pad) do
{blamed, stack} = Exception.blame(:error, struct, stack)
banner = Exception.format_banner(:error, struct)
blamed = FunctionClauseError.blame(blamed, &inspect/1, &blame_match(&1, formatter))
message = error_info(banner, formatter) <> "\n" <> pad(String.trim_leading(blamed, "\n"))
{message <> format_code(test, stack, formatter), stack}
end
@doc false
@deprecated "Use ExUnit.Formatter.format_test_all_failure/5 instead"
def format_test_case_failure(test_case, failures, counter, width, formatter) do
@@ -221,6 +184,8 @@ defmodule ExUnit.Formatter do
end)
end
## kind/reason formatting
defp format_kind_reason(test, :error, %mod{} = struct, stack, width, formatter)
when mod in @formatter_exceptions do
format_exception(test, struct, stack, width, formatter, @counter_padding)
@@ -263,129 +228,67 @@ defmodule ExUnit.Formatter do
defp linked_or_trapped_exit(_kind, _reason), do: :error
defp format_code(test, stack, formatter) do
if snippet = get_code(test, stack) do
" " <> formatter.(:extra_info, "code: ") <> snippet <> "\n"
else
""
end
defp format_exception(test, %ExUnit.AssertionError{} = struct, stack, width, formatter, pad) do
label_padding_size = if has_value?(struct.right), do: 7, else: 6
padding_size = label_padding_size + byte_size(@counter_padding)
code_multiline =
if struct.doctest != @no_value,
do: &pad_multiline(&1, padding_size),
else: &code_multiline(&1, padding_size)
formatted =
[
message: if_value(struct.message, &format_message(&1, formatter)),
doctest: if_value(struct.doctest, &pad_multiline(&1, 2 + byte_size(@counter_padding))),
code: if_value(struct.expr, code_multiline, fn -> get_code(test, stack) || @no_value end),
arguments: if_value(struct.args, &format_args(&1, width))
]
|> Kernel.++(format_assertion_diff(struct, padding_size, width, formatter))
|> format_meta(formatter, pad, label_padding_size)
|> IO.iodata_to_binary()
{formatted, stack}
end
defp get_code(%{module: module, name: name}, stack) do
info =
Enum.find_value(stack, fn
{^module, ^name, _, info} -> info
_ -> nil
end)
file = info[:file]
line = info[:line]
if line > 0 && file && File.exists?(file) do
file |> File.stream!() |> Enum.at(line - 1) |> String.trim()
end
rescue
_ -> nil
defp format_exception(test, %FunctionClauseError{} = struct, stack, _width, formatter, _pad) do
{blamed, stack} = Exception.blame(:error, struct, stack)
banner = Exception.format_banner(:error, struct)
blamed = FunctionClauseError.blame(blamed, &inspect/1, &blame_match(&1, formatter))
message = error_info(banner, formatter) <> "\n" <> pad(String.trim_leading(blamed, "\n"))
{message <> format_code(test, stack, formatter), stack}
end
defp get_code(%{}, _) do
nil
## Assertion error and diffing
@doc false
def format_assertion_error(%ExUnit.AssertionError{} = struct) do
format_exception(%{}, struct, [], :infinity, fn _, msg -> msg end, "") |> elem(0)
end
defp blame_match(%{match?: true, node: node}, _formatter),
do: Macro.to_string(node)
@doc """
Formats `ExUnit.AssertionError` diff.
defp blame_match(%{match?: false, node: node}, formatter),
do: formatter.(:blame_diff, Macro.to_string(node))
It returns a keyword list with diffing information
from the left and right side of the assertion, if
any exists.
defp format_meta(fields, formatter, padding, padding_size) do
for {label, value} <- fields, has_value?(value) do
[padding, format_label(label, formatter, padding_size), value, "\n"]
end
end
It expects the assertion error, the `padding_size`
for formatted content, the width (may be `:infinity`),
and the formatter callback function.
"""
def format_assertion_diff(assert_error, padding_size, width, formatter)
defp if_value(value, fun) do
if has_value?(value) do
fun.(value)
else
value
end
end
defp unless_value(value, fun) do
if has_value?(value) do
@no_value
else
fun.()
end
end
defp has_value?(value) do
value != @no_value
end
defp format_label(:note, _formatter, _padding_size), do: ""
defp format_label(label, formatter, padding_size) do
formatter.(:extra_info, String.pad_trailing("#{label}:", padding_size))
end
defp format_message(value, formatter) do
value = String.replace(value, "\n", "\n" <> @counter_padding)
formatter.(:error_info, value)
end
defp format_args(args, width) do
entries =
for {arg, i} <- Enum.with_index(args, 1) do
"""
# #{i}
#{inspect_multiline(arg, 9, width)}
"""
end
["\n" | entries]
end
@assertions [
:assert,
:assert_raise,
:assert_receive,
:assert_received,
:refute,
:refute_receive,
:refute_received
]
defp code_multiline({fun, _, [expr]}, padding_size) when fun in @assertions do
pad_multiline(Atom.to_string(fun) <> " " <> Macro.to_string(expr), padding_size)
end
defp code_multiline(expr, padding_size) do
pad_multiline(Macro.to_string(expr), padding_size)
end
defp inspect_multiline(expr, padding_size, width) do
width = if width == :infinity, do: width, else: width - padding_size
expr
|> Algebra.to_doc(%Inspect.Opts{width: width})
|> Algebra.group()
|> Algebra.nest(padding_size)
|> Algebra.format(width)
end
defp format_context(%{context: {:mailbox, _pins, []}}, _, _, _) do
def format_assertion_diff(%ExUnit.AssertionError{context: {:mailbox, _pins, []}}, _, _, _) do
[]
end
defp format_context(
%{left: left, context: {:mailbox, pins, mailbox}},
formatter,
padding_size,
width
) do
def format_assertion_diff(
%ExUnit.AssertionError{left: left, context: {:mailbox, pins, mailbox}},
padding_size,
width,
formatter
) do
formatted_mailbox =
for message <- mailbox do
{pattern, value, _warnings} =
@@ -413,14 +316,14 @@ defmodule ExUnit.Formatter do
[mailbox: Enum.join(formatted_mailbox, "\n")]
end
defp format_context(
%{left: left, right: right, context: context},
formatter,
padding_size,
width
) do
def format_assertion_diff(
%ExUnit.AssertionError{left: left, right: right, context: context},
padding_size,
width,
formatter
) do
{left, right, extras} = format_sides(left, right, context, formatter, padding_size, width)
[left: left, right: right] ++ extras
for {k, v} <- [left: left, right: right] ++ extras, has_value?(v), do: {k, v}
end
defp format_sides(left, right, context, formatter, padding_size, width) do
@@ -488,6 +391,121 @@ defmodule ExUnit.Formatter do
end
end
## Helpers
defp format_code(test, stack, formatter) do
if snippet = get_code(test, stack) do
" " <> formatter.(:extra_info, "code: ") <> snippet <> "\n"
else
""
end
end
defp get_code(%{module: module, name: name}, stack) do
info =
Enum.find_value(stack, fn
{^module, ^name, _, info} -> info
_ -> nil
end)
file = info[:file]
line = info[:line]
if line > 0 && file && File.exists?(file) do
file |> File.stream!() |> Enum.at(line - 1) |> String.trim()
end
rescue
_ -> nil
end
defp get_code(%{}, _) do
nil
end
defp blame_match(%{match?: true, node: node}, _formatter),
do: Macro.to_string(node)
defp blame_match(%{match?: false, node: node}, formatter),
do: formatter.(:blame_diff, Macro.to_string(node))
defp format_meta(fields, formatter, padding, padding_size) do
for {label, value} <- fields, has_value?(value) do
[padding, format_label(label, formatter, padding_size), value, "\n"]
end
end
defp if_value(value, fun) do
if has_value?(value) do
fun.(value)
else
value
end
end
defp if_value(value, do_fun, else_fun) do
if has_value?(value) do
do_fun.(value)
else
else_fun.()
end
end
defp has_value?(value) do
value != @no_value
end
defp format_label(:message, _formatter, _padding_size), do: ""
defp format_label(label, formatter, padding_size) do
formatter.(:extra_info, String.pad_trailing("#{label}:", padding_size))
end
defp format_message(value, formatter) do
value = String.replace(value, "\n", "\n" <> @counter_padding)
formatter.(:error_info, value)
end
defp format_args(args, width) do
entries =
for {arg, i} <- Enum.with_index(args, 1) do
"""
# #{i}
#{inspect_multiline(arg, 9, width)}
"""
end
["\n" | entries]
end
@assertions [
:assert,
:assert_raise,
:assert_receive,
:assert_received,
:refute,
:refute_receive,
:refute_received
]
defp code_multiline({fun, _, [expr]}, padding_size) when fun in @assertions do
pad_multiline(Atom.to_string(fun) <> " " <> Macro.to_string(expr), padding_size)
end
defp code_multiline(expr, padding_size) do
pad_multiline(Macro.to_string(expr), padding_size)
end
defp inspect_multiline(expr, padding_size, width) do
width = if width == :infinity, do: width, else: width - padding_size
expr
|> Algebra.to_doc(%Inspect.Opts{width: width})
|> Algebra.group()
|> Algebra.nest(padding_size)
|> Algebra.format(width)
end
defp format_stacktrace([], _case, _test, _color) do
""
end
+1 -1
View File
@@ -571,7 +571,7 @@ defmodule ExUnit.Runner do
end
defp shuffle(%{seed: 0}, list) do
Enum.reverse(list)
list
end
defp shuffle(%{seed: seed}, list) do
+16 -4
View File
@@ -22,8 +22,8 @@ defmodule ExUnit.Server do
end
end
def modules_loaded do
GenServer.call(@name, :modules_loaded, @timeout)
def modules_loaded(uniq?) do
GenServer.call(@name, {:modules_loaded, uniq?}, @timeout)
end
def take_async_modules(count) do
@@ -58,11 +58,23 @@ defmodule ExUnit.Server do
{:reply, state.sync_modules, %{state | sync_modules: [], loaded: System.monotonic_time()}}
end
def handle_call(:modules_loaded, _from, %{loaded: :done} = state) do
def handle_call({:modules_loaded, _}, _from, %{loaded: :done} = state) do
{:reply, 0, state}
end
def handle_call(:modules_loaded, _from, %{loaded: loaded} = state) when is_integer(loaded) do
def handle_call({:modules_loaded, uniq?}, _from, %{loaded: loaded} = state)
when is_integer(loaded) do
state =
if uniq? do
%{
state
| async_modules: Enum.uniq(state.async_modules),
sync_modules: Enum.uniq(state.sync_modules)
}
else
state
end
diff = System.convert_time_unit(System.monotonic_time() - loaded, :native, :microsecond)
{:reply, diff, take_modules(%{state | loaded: :done})}
end
+58 -63
View File
@@ -462,7 +462,10 @@ defmodule ExUnit.DocTestTest do
# doctest ExUnit.DocTest
doctest ExUnit.DocTestTest.GoodModule, import: true
doctest ExUnit.DocTestTest.SomewhatGoodModuleWithOnly, only: [one: 0], import: true
doctest ExUnit.DocTestTest.SomewhatGoodModuleWithOnly,
only: [:moduledoc, one: 0],
import: true
doctest ExUnit.DocTestTest.SomewhatGoodModuleWithExcept,
except: [:moduledoc, two: 0],
@@ -574,7 +577,7 @@ defmodule ExUnit.DocTestTest do
Doctest failed
doctest:
iex> :oops
"#Inspect<[]>"
#Inspect<[]>
code: inspect(:oops) === "#Inspect<[]>"
left: ":oops"
right: "#Inspect<[]>"
@@ -597,7 +600,7 @@ defmodule ExUnit.DocTestTest do
Doctest failed: expected exception WhatIsThis but got RuntimeError with message "oops"
doctest:
iex> raise "oops"
** (WhatIsThis) "oops"
** (WhatIsThis) oops
stacktrace:
test/ex_unit/doc_test_test.exs:#{starting_line + 18}: ExUnit.DocTestTest.Invalid (module)
"""
@@ -612,7 +615,7 @@ defmodule ExUnit.DocTestTest do
"oops"
doctest:
iex> raise "oops"
** (RuntimeError) "hello"
** (RuntimeError) hello
stacktrace:
test/ex_unit/doc_test_test.exs:#{starting_line + 21}: ExUnit.DocTestTest.Invalid (module)
"""
@@ -632,18 +635,17 @@ defmodule ExUnit.DocTestTest do
"""
assert output =~ """
9) doctest ExUnit.DocTestTest.Invalid.dedented_past_fence/0 (9) (ExUnit.DocTestTest.ActuallyCompiled)
9) doctest ExUnit.DocTestTest.Invalid.b/0 (9) (ExUnit.DocTestTest.ActuallyCompiled)
test/ex_unit/doc_test_test.exs:#{doctest_line}
Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 57}:5: unexpected token: "`" (column 5, code point U+0060)
#{line_placeholder(starting_line + 57)} |
#{starting_line + 57} | ```
#{line_placeholder(starting_line + 57)} | ^
Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 33}:6: syntax error before: '*'
#{line_placeholder(starting_line + 33)} |
#{starting_line + 33} | 1 + * 1
#{line_placeholder(starting_line + 33)} | ^
doctest:
iex> 1 + 2
3
```
iex> 1 + * 1
1
stacktrace:
test/ex_unit/doc_test_test.exs:#{starting_line + 56}: ExUnit.DocTestTest.Invalid (module)
test/ex_unit/doc_test_test.exs:#{starting_line + 33}: ExUnit.DocTestTest.Invalid (module)
"""
assert output =~ """
@@ -677,7 +679,22 @@ defmodule ExUnit.DocTestTest do
"""
assert output =~ """
12) doctest ExUnit.DocTestTest.Invalid.invalid_utf8/0 (12) (ExUnit.DocTestTest.ActuallyCompiled)
12) doctest ExUnit.DocTestTest.Invalid.dedented_past_fence/0 (12) (ExUnit.DocTestTest.ActuallyCompiled)
test/ex_unit/doc_test_test.exs:#{doctest_line}
Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 57}:5: unexpected token: "`" (column 5, code point U+0060)
#{line_placeholder(starting_line + 57)} |
#{starting_line + 57} | ```
#{line_placeholder(starting_line + 57)} | ^
doctest:
iex> 1 + 2
3
```
stacktrace:
test/ex_unit/doc_test_test.exs:#{starting_line + 56}: ExUnit.DocTestTest.Invalid (module)
"""
assert output =~ """
13) doctest ExUnit.DocTestTest.Invalid.invalid_utf8/0 (13) (ExUnit.DocTestTest.ActuallyCompiled)
test/ex_unit/doc_test_test.exs:#{doctest_line}
Doctest did not compile, got: (UnicodeConversionError) invalid encoding starting at <<255, 34, 41>>
stacktrace:
@@ -685,7 +702,7 @@ defmodule ExUnit.DocTestTest do
"""
assert output =~ """
13) doctest ExUnit.DocTestTest.Invalid.misplaced_opaque_type/0 (13) (ExUnit.DocTestTest.ActuallyCompiled)
14) doctest ExUnit.DocTestTest.Invalid.misplaced_opaque_type/0 (14) (ExUnit.DocTestTest.ActuallyCompiled)
test/ex_unit/doc_test_test.exs:#{doctest_line}
Doctest did not compile, got: (TokenMissingError) test/ex_unit/doc_test_test.exs:#{starting_line + 69}:7: missing terminator: } (for "{" starting at line #{starting_line + 69})
#{line_placeholder(starting_line + 69)} |
@@ -699,28 +716,8 @@ defmodule ExUnit.DocTestTest do
test/ex_unit/doc_test_test.exs:#{starting_line + 69}: ExUnit.DocTestTest.Invalid (module)
"""
assert output =~ "14) doctest ExUnit.DocTestTest.Invalid.raising_inspect/0"
assert output =~ "iex> ExUnit.DocTestTest.Haiku.new(:this, :is, {:not, :a, :haiku})"
assert output =~
"test/ex_unit/doc_test_test.exs:#{starting_line + 82}: ExUnit.DocTestTest.Invalid (module)"
assert output =~ """
15) doctest ExUnit.DocTestTest.Invalid.b/0 (15) (ExUnit.DocTestTest.ActuallyCompiled)
test/ex_unit/doc_test_test.exs:#{doctest_line}
Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 33}:6: syntax error before: '*'
#{line_placeholder(starting_line + 33)} |
#{starting_line + 33} | 1 + * 1
#{line_placeholder(starting_line + 33)} | ^
doctest:
iex> 1 + * 1
1
stacktrace:
test/ex_unit/doc_test_test.exs:#{starting_line + 33}: ExUnit.DocTestTest.Invalid (module)
"""
assert output =~ """
16) doctest ExUnit.DocTestTest.Invalid.t/0 (16) (ExUnit.DocTestTest.ActuallyCompiled)
15) doctest ExUnit.DocTestTest.Invalid.t/0 (15) (ExUnit.DocTestTest.ActuallyCompiled)
test/ex_unit/doc_test_test.exs:#{doctest_line}
Doctest did not compile, got: (SyntaxError) test/ex_unit/doc_test_test.exs:#{starting_line + 75}:6: syntax error before: '*'
#{line_placeholder(starting_line + 75)} |
@@ -732,6 +729,12 @@ defmodule ExUnit.DocTestTest do
stacktrace:
test/ex_unit/doc_test_test.exs:#{starting_line + 75}: ExUnit.DocTestTest.Invalid (module)
"""
assert output =~ "16) doctest ExUnit.DocTestTest.Invalid.raising_inspect/0"
assert output =~ "iex> ExUnit.DocTestTest.Haiku.new(:this, :is, {:not, :a, :haiku})"
assert output =~
"test/ex_unit/doc_test_test.exs:#{starting_line + 82}: ExUnit.DocTestTest.Invalid (module)"
end
test "pattern matching assertions in doctests" do
@@ -862,18 +865,17 @@ defmodule ExUnit.DocTestTest do
end
end
test "doctests type" do
defmodule DoctestType do
use ExUnit.Case
doctest ExUnit.DocTestTest.NoImport
test "doctests built-in tags" do
alias ExUnit.DocTestTest.NoImport
setup test do
assert test.test_type == :doctest
:ok
end
defmodule DoctestTags do
use ExUnit.Case, register: false
doctest NoImport
end
assert capture_io(fn -> ExUnit.run() end) =~ "2 doctests, 0 failures"
assert %ExUnit.TestModule{tests: [test1, test2]} = DoctestTags.__ex_unit__()
assert %{test_type: :doctest, doctest: NoImport, doctest_line: 129} = test1.tags
assert %{test_type: :doctest, doctest: NoImport, doctest_line: 132} = test2.tags
end
test "multiple exceptions in one test case is not supported" do
@@ -898,24 +900,7 @@ defmodule ExUnit.DocTestTest do
end
end
test "fails when testing single function not found" do
message = """
test/ex_unit/doc_test_test\.exs: undefined or private function given to doctest:
ExUnit.DocTestTest.SomewhatGoodModuleWithOnly.three/0
"""
assert_raise ExUnit.DocTest.Error, message, fn ->
defmodule NeverCompiled do
import ExUnit.DocTest
doctest ExUnit.DocTestTest.SomewhatGoodModuleWithOnly, only: [three: 0], import: true
end
end
end
test "fails when testing multiple functions not found" do
test "fails when testing functions are not found" do
message = """
test/ex_unit/doc_test_test\.exs: undefined or private functions given to doctest:
@@ -987,6 +972,16 @@ defmodule ExUnit.DocTestTest do
end
end
test "doctest direct invocation" do
defmodule Direct do
use ExUnit.Case, register: false
doctest ExUnit.DocTestTest.GoodModule, import: true
end
[head | _] = Direct.__ex_unit__().tests
assert apply(Direct, head.name, [%{}]) == {:ok, 2}
end
defp line_placeholder(line_number) do
digits =
line_number

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